Skip to content
Open
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
23 changes: 23 additions & 0 deletions src/wallet/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,17 @@ pub enum CreateTxError {
MissingNonWitnessUtxo(OutPoint),
/// Miniscript PSBT error
MiniscriptPsbt(MiniscriptPsbtError),
/// OP_RETURN data payload exceeds the 80-byte standardness limit
///
/// Bitcoin Core enforces a maximum `scriptPubKey` size of 83 bytes for data carrier outputs
/// (`MAX_OP_RETURN_RELAY`), which constrains the data payload to at most 80 bytes.
/// See <https://github.com/bitcoin/bitcoin/blob/master/src/policy/policy.h>
OpReturnInvalidDataSize(usize),
/// Transaction already contains an OP_RETURN output
///
/// Bitcoin standardness rules allow at most one OP_RETURN output per transaction.
/// See <https://github.com/bitcoin/bitcoin/blob/master/src/policy/policy.cpp>
MultipleOpReturnOutputs,
}

impl fmt::Display for CreateTxError {
Expand Down Expand Up @@ -281,6 +292,18 @@ impl fmt::Display for CreateTxError {
CreateTxError::MiniscriptPsbt(err) => {
write!(f, "Miniscript PSBT error: {err}")
}
CreateTxError::OpReturnInvalidDataSize(size) => {
write!(
f,
"OP_RETURN data payload is {size} bytes; maximum allowed by standardness rules is 80"
)
}
CreateTxError::MultipleOpReturnOutputs => {
write!(
f,
"Transaction already contains an OP_RETURN output; standardness rules allow at most one"
)
}
}
}
}
Expand Down
45 changes: 44 additions & 1 deletion src/wallet/tx_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -680,13 +680,56 @@ impl<'a, Cs> TxBuilder<'a, Cs> {
self
}

/// Add data as an output, using OP_RETURN
/// Add data as an output, using OP_RETURN.
///
/// # Deprecation
///
/// This method does not enforce Bitcoin standardness rules. Use [`try_add_data`] instead,
/// which returns an error if the data exceeds 80 bytes or if an OP_RETURN output is already
/// present.
///
/// [`try_add_data`]: Self::try_add_data
#[deprecated(since = "3.1.0", note = "use `try_add_data` instead")]
pub fn add_data<T: AsRef<PushBytes>>(&mut self, data: &T) -> &mut Self {
let script = ScriptBuf::new_op_return(data);
self.add_recipient(script, Amount::ZERO);
self
}

/// Add data as an output, using OP_RETURN, enforcing Bitcoin standardness rules.
///
/// Returns an error if:
/// - `data` exceeds 80 bytes ([`CreateTxError::OpReturnInvalidDataSize`]). Bitcoin Core's
/// `MAX_OP_RETURN_RELAY` limits the `scriptPubKey` to 83 bytes, which constrains the data
/// payload to at most 80 bytes.
/// - A recipient with an OP_RETURN script is already present
/// ([`CreateTxError::MultipleOpReturnOutputs`]). Standardness rules allow at most one
/// OP_RETURN output per transaction.
pub fn try_add_data<T: AsRef<PushBytes>>(
&mut self,
data: &T,
) -> Result<&mut Self, CreateTxError> {
const MAX_OP_RETURN_DATA_BYTES: usize = 80;

let bytes = data.as_ref();
if bytes.len() > MAX_OP_RETURN_DATA_BYTES {
return Err(CreateTxError::OpReturnInvalidDataSize(bytes.len()));
}

if self
.params
.recipients
.iter()
.any(|(script, _)| script.is_op_return())
{
return Err(CreateTxError::MultipleOpReturnOutputs);
}

let script = ScriptBuf::new_op_return(bytes);
self.add_recipient(script, Amount::ZERO);
Ok(self)
}

/// Sets the address to *drain* excess coins to.
///
/// Usually, when there are excess coins they are sent to a change address generated by the
Expand Down
45 changes: 43 additions & 2 deletions tests/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2624,8 +2624,8 @@ fn test_fee_rate_sign_no_grinding_high_r() {
builder
.drain_to(addr.script_pubkey())
.drain_wallet()
.fee_rate(fee_rate)
.add_data(&data);
.fee_rate(fee_rate);
builder.try_add_data(&data).unwrap();
let mut psbt = builder.finish().unwrap();
let fee = check_fee!(wallet, psbt);
let (op_return_vout, _) = psbt
Expand Down Expand Up @@ -3013,3 +3013,44 @@ fn test_tx_ordering_untouched_preserves_insertion_ordering_bnb_success() {
"UTXOs should be ordered with required first, then selected"
);
}

#[test]
fn test_try_add_data_valid() {
let (mut wallet, _) = get_funded_wallet_wpkh();
let addr = wallet.next_unused_address(KeychainKind::External);
let data = PushBytesBuf::try_from(vec![0u8; 80]).unwrap();

let mut builder = wallet.build_tx();
builder.add_recipient(addr.script_pubkey(), Amount::from_sat(1000));
assert!(builder.try_add_data(&data).is_ok());
assert!(builder.finish().is_ok());
}

#[test]
fn test_try_add_data_too_large() {
let (mut wallet, _) = get_funded_wallet_wpkh();
let addr = wallet.next_unused_address(KeychainKind::External);
let data = PushBytesBuf::try_from(vec![0u8; 81]).unwrap();

let mut builder = wallet.build_tx();
builder.add_recipient(addr.script_pubkey(), Amount::from_sat(1000));
assert_matches!(
builder.try_add_data(&data),
Err(CreateTxError::OpReturnInvalidDataSize(81))
);
}

#[test]
fn test_try_add_data_multiple_op_return() {
let (mut wallet, _) = get_funded_wallet_wpkh();
let addr = wallet.next_unused_address(KeychainKind::External);
let data = PushBytesBuf::try_from(vec![0u8; 4]).unwrap();

let mut builder = wallet.build_tx();
builder.add_recipient(addr.script_pubkey(), Amount::from_sat(1000));
builder.try_add_data(&data).unwrap();
assert_matches!(
builder.try_add_data(&data),
Err(CreateTxError::MultipleOpReturnOutputs)
);
}