diff --git a/src/wallet/error.rs b/src/wallet/error.rs index ddd07478..7a1b7e5d 100644 --- a/src/wallet/error.rs +++ b/src/wallet/error.rs @@ -328,8 +328,6 @@ pub enum BuildFeeBumpError { TransactionNotFound(Txid), /// Happens when trying to bump a transaction that is already confirmed TransactionConfirmed(Txid), - /// Trying to replace a tx that has a sequence >= `0xFFFFFFFE` - IrreplaceableTransaction(Txid), /// Node doesn't have data to estimate a fee rate FeeRateUnavailable, /// Input references an invalid output index in the previous transaction @@ -353,9 +351,6 @@ impl fmt::Display for BuildFeeBumpError { Self::TransactionConfirmed(txid) => { write!(f, "Transaction already confirmed with txid: {txid}") } - Self::IrreplaceableTransaction(txid) => { - write!(f, "Transaction can't be replaced with txid: {txid}") - } Self::FeeRateUnavailable => write!(f, "Fee rate unavailable"), Self::InvalidOutputIndex(op) => { write!(f, "A txin referenced an invalid output: {op}") diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index b4eb7709..ab0dd9f1 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -1551,9 +1551,14 @@ impl Wallet { /// Bump the fee of a transaction previously created with this wallet. /// - /// Returns an error if the transaction is already confirmed or doesn't explicitly signal - /// *replace by fee* (RBF). If the transaction can be fee bumped then it returns a [`TxBuilder`] - /// pre-populated with the inputs and outputs of the original transaction. + /// Returns an error if the transaction is already confirmed. If the transaction can be fee + /// bumped then it returns a [`TxBuilder`] pre-populated with the inputs and outputs of the + /// original transaction. + /// + /// Replacing an unconfirmed transaction does not require the original to signal opt-in RBF + /// (`nSequence` ≤ `0xFFFFFFFD`). Relay and mining acceptance depend on local policy; Bitcoin + /// Core 28+ defaults to full-RBF mempool relay, while other implementations or configs may + /// differ. /// /// ## Example /// @@ -1616,16 +1621,6 @@ impl Wallet { return Err(BuildFeeBumpError::TransactionConfirmed(txid)); } - if !tx - .input - .iter() - .any(|txin| txin.sequence.to_consensus_u32() <= 0xFFFFFFFD) - { - return Err(BuildFeeBumpError::IrreplaceableTransaction( - tx.compute_txid(), - )); - } - let fee = self .calculate_fee(&tx) .map_err(|_| BuildFeeBumpError::FeeRateUnavailable)?; diff --git a/tests/build_fee_bump.rs b/tests/build_fee_bump.rs index 4a243dfa..f918e12d 100644 --- a/tests/build_fee_bump.rs +++ b/tests/build_fee_bump.rs @@ -16,19 +16,39 @@ mod common; use common::*; #[test] -#[should_panic(expected = "IrreplaceableTransaction")] -fn test_bump_fee_irreplaceable_tx() { +fn test_bump_fee_tx_without_rbf_signaling() { let (mut wallet, _) = get_funded_wallet_wpkh(); let addr = wallet.next_unused_address(KeychainKind::External); let mut builder = wallet.build_tx(); builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); builder.set_exact_sequence(Sequence(0xFFFFFFFE)); let psbt = builder.finish().unwrap(); + let original_fee = check_fee!(wallet, psbt); let tx = psbt.extract_tx().expect("failed to extract tx"); let txid = tx.compute_txid(); insert_tx(&mut wallet, tx); - wallet.build_fee_bump(txid).unwrap().finish().unwrap(); + + let feerate = FeeRate::from_sat_per_kwu(625); + let mut builder = wallet + .build_fee_bump(txid) + .expect("fee bump without opt-in RBF"); + builder.fee_rate(feerate); + let psbt = builder.finish().expect("finish fee bump"); + let fee = check_fee!(wallet, psbt); + assert!( + fee > original_fee, + "fee bump must increase absolute fee: {fee} > {original_fee}" + ); + + let new_tx = psbt.clone().extract_tx().expect("failed to extract tx"); + assert_ne!( + new_tx.compute_txid(), + txid, + "replacement transaction must differ from the original" + ); + + assert_fee_rate!(psbt, fee, feerate, @add_signature); } #[test]