Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 11 additions & 9 deletions zingolib/src/lightclient/send.rs
Original file line number Diff line number Diff line change
Expand Up @@ -515,7 +515,7 @@
};
let highest_refund_address_index = wallet.highest_refund_address_index();
let calculated_txids = wallet
.calculate_transactions(proposal, sending_account)
.calculate_transactions(proposal, sending_account, None)
.await
.map_err(|e| {
wallet.truncate_refund_addresses(highest_refund_address_index);
Expand Down Expand Up @@ -564,7 +564,8 @@
.wallet()
.write()
.await
.calculate_transactions(proposal, shielding_account)
// A Shield never carries an OP_RETURN payload.
.calculate_transactions(proposal, shielding_account, None)
.await
.map_err(SendError::CalculateShieldError)?;

Expand Down Expand Up @@ -644,7 +645,7 @@
};
match retargeted {
Ok(proposal) => wallet
.calculate_transactions(proposal, sending_account)
.calculate_transactions(proposal, sending_account, None)
.await
.map_err(SendError::CalculateSendError),
Err(e) => Err(e),
Expand All @@ -660,9 +661,10 @@
} else {
Ok(proposal)
};
match retargeted {

Check warning on line 664 in zingolib/src/lightclient/send.rs

View workflow job for this annotation

GitHub Actions / cargo-checkmate / Cargo Checkmate (format)

Diff in /home/runner/work/zingolib/zingolib/zingolib/src/lightclient/send.rs
Ok(proposal) => wallet
.calculate_transactions(proposal, shielding_account)
// A Shield never carries an OP_RETURN payload.
.calculate_transactions(proposal, shielding_account, None)
.await
.map_err(SendError::CalculateShieldError),
Err(e) => Err(e),
Expand Down Expand Up @@ -1093,7 +1095,7 @@
.wallet()
.write()
.await
.calculate_transactions(proposal, zip32::AccountId::ZERO)
.calculate_transactions(proposal, zip32::AccountId::ZERO, None)
.await
.unwrap();
assert_eq!(txids.len(), 1);
Expand Down Expand Up @@ -1158,7 +1160,7 @@
.wallet()
.write()
.await
.calculate_transactions(proposal, zip32::AccountId::ZERO)
.calculate_transactions(proposal, zip32::AccountId::ZERO, None)
.await
.unwrap();
assert_eq!(txids.len(), 1);
Expand Down Expand Up @@ -1232,7 +1234,7 @@
.wallet()
.write()
.await
.calculate_transactions(proposal, zip32::AccountId::ZERO)
.calculate_transactions(proposal, zip32::AccountId::ZERO, None)
.await
.unwrap();
assert_eq!(txids.len(), 1);
Expand Down Expand Up @@ -1324,7 +1326,7 @@
.wallet()
.write()
.await
.calculate_transactions(proposal, zip32::AccountId::ZERO)
.calculate_transactions(proposal, zip32::AccountId::ZERO, None)
.await
.unwrap();
assert_eq!(txids.len(), 1);
Expand Down Expand Up @@ -1416,7 +1418,7 @@
.wallet()
.write()
.await
.calculate_transactions(proposal, zip32::AccountId::ZERO)
.calculate_transactions(proposal, zip32::AccountId::ZERO, None)
.await
.unwrap();
assert_eq!(txids.len(), 2, "the ZIP-320 pair builds as two steps");
Expand Down
2 changes: 1 addition & 1 deletion zingolib/src/testutils/mock_indexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -939,7 +939,7 @@ pub async fn faucet_funding_transaction(receivers: Vec<(&str, u64, Option<&str>)
.wallet()
.write()
.await
.calculate_transactions(proposal, zip32::AccountId::ZERO)
.calculate_transactions(proposal, zip32::AccountId::ZERO, None)
.await
.expect("the faucet's funding transaction builds");
assert_eq!(txids.len(), 1, "funding sends are single-step");
Expand Down
1 change: 1 addition & 0 deletions zingolib/src/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ pub mod disk;
pub mod keys;
pub mod locks;
pub mod migration;
pub mod op_return;
pub mod output;
pub mod propose;
pub mod send;
Expand Down
70 changes: 70 additions & 0 deletions zingolib/src/wallet/op_return.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
//! OP_RETURN (null-data) payload support for the spend path.
//!
//! Lets a caller attach a single OP_RETURN (null-data) output to the
//! transaction a send produces, so a THORChain/MAYAChain swap memo can be
//! written on chain.

use thiserror::Error;

/// The maximum size, in bytes, of an OP_RETURN payload that will relay on
/// the network. Mirrors the limit the transaction builder enforces, so an
/// oversized payload is rejected at construction with a typed error rather
/// than at build time.
pub const MAX_OP_RETURN_BYTES: usize = 80;

/// A validated OP_RETURN payload: at most [`MAX_OP_RETURN_BYTES`] bytes.
///
/// Construct via [`OpReturnData::new`]; the length invariant is enforced
/// once, at the boundary, so the build layer can treat the bytes as safe.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct OpReturnData(Vec<u8>);

impl OpReturnData {
/// Validate `data` and wrap it. Fails if it exceeds the relay limit.
pub fn new(data: Vec<u8>) -> Result<Self, OpReturnDataError> {
if data.len() > MAX_OP_RETURN_BYTES {
return Err(OpReturnDataError::TooLong {
len: data.len(),
max: MAX_OP_RETURN_BYTES,
});
}
Ok(Self(data))
}

/// The validated payload bytes, ready to hand to
/// `Builder::add_transparent_null_data_output`.
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
}

/// Error constructing an [`OpReturnData`].
#[derive(Debug, Error, PartialEq, Eq)]
pub enum OpReturnDataError {
/// Payload exceeds the OP_RETURN relay limit.
#[error("OP_RETURN payload is {len} bytes, exceeds the {max}-byte limit")]
TooLong { len: usize, max: usize },
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn accepts_payload_at_the_limit() {
let data = vec![0u8; MAX_OP_RETURN_BYTES];
assert!(OpReturnData::new(data).is_ok());
}

#[test]
fn rejects_payload_over_the_limit() {
let data = vec![0u8; MAX_OP_RETURN_BYTES + 1];
assert_eq!(
OpReturnData::new(data),
Err(OpReturnDataError::TooLong {
len: MAX_OP_RETURN_BYTES + 1,
max: MAX_OP_RETURN_BYTES,
})
);
}
}
57 changes: 50 additions & 7 deletions zingolib/src/wallet/send.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,33 @@ use zcash_protocol::{ShieldedPool, TxId};

use super::LightWallet;
use super::error::{CalculateTransactionError, KeyError};
use super::op_return::OpReturnData;

impl LightWallet {
/// Creates and stores transaction from the given `proposal`, returning the txids for each calculated transaction.
/// Creates and stores transactions from the given `proposal`, returning the txids for each calculated transaction.
///
/// When `op_return` is `Some`, the payload rides the final transaction
/// of the proposal — the ZIP-320 TEX exposure step, or the single step
/// otherwise — which is built by driving the transaction builder
/// directly. When `None`, the whole proposal is built by
/// `zcash_client_backend`.
pub(crate) async fn calculate_transactions<NoteRef>(
&mut self,
proposal: Proposal<zip317::FeeRule, NoteRef>,
sending_account: zip32::AccountId,
op_return: Option<OpReturnData>,
) -> Result<NonEmpty<TxId>, CalculateTransactionError<NoteRef>> {
let calculated_txids = match proposal.steps().len() {
1 => {
self.create_proposed_transactions(proposal, sending_account)
.await?
}
1 => match op_return {
None => {
self.create_proposed_transactions(proposal, sending_account)
.await?
}
Some(op_return) => {
self.build_proposal_with_op_return(proposal, sending_account, op_return)
.await?
}
},
2 if proposal.steps()[1]
.transaction_request()
.payments()
Expand All @@ -44,8 +58,16 @@ impl LightWallet {
)
}) =>
{
self.create_proposed_transactions(proposal, sending_account)
.await?
match op_return {
None => {
self.create_proposed_transactions(proposal, sending_account)
.await?
}
Some(op_return) => {
self.build_proposal_with_op_return(proposal, sending_account, op_return)
.await?
}
}
}
_ => return Err(CalculateTransactionError::NonTexMultiStep),
};
Expand All @@ -54,6 +76,27 @@ impl LightWallet {
Ok(calculated_txids)
}

/// Builds a proposal whose final transaction carries an OP_RETURN
/// payload.
///
/// `zcash_client_backend::create_proposed_transactions` exposes no hook
/// to add an output, so the payload-carrying step is built by driving
/// `zcash_primitives`' transaction builder directly: it reuses the
/// inputs, recipient outputs, and change the proposal already fixed,
/// then adds the null-data output via
/// `Builder::add_transparent_null_data_output` before proving and
/// signing. Fee correctness holds because the ZIP-317 `FeeRule`
/// accounts for the null-data output's serialized size and the
/// builder's value-balance check rejects a mismatched fee.
async fn build_proposal_with_op_return<NoteRef>(
&mut self,
_proposal: Proposal<zip317::FeeRule, NoteRef>,
_sending_account: zip32::AccountId,
_op_return: OpReturnData,
) -> Result<NonEmpty<TxId>, CalculateTransactionError<NoteRef>> {
todo!("build the payload-carrying step via the transaction builder")
}

async fn create_proposed_transactions<NoteRef>(
&mut self,
proposal: Proposal<zcash_primitives::transaction::fees::zip317::FeeRule, NoteRef>,
Expand Down
Loading