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
5 changes: 0 additions & 5 deletions src/wallet/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -333,8 +333,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
Expand All @@ -358,9 +356,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}")
Expand Down
20 changes: 7 additions & 13 deletions src/wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1589,9 +1589,13 @@ 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.
///
/// This builder does not require the original transaction to signal opt-in RBF
/// (`nSequence` <= `0xFFFFFFFD`). Whether the resulting replacement is accepted
/// for relay or mining depends on the local policy of the broadcasting node.
///
/// ## Example
///
Expand Down Expand Up @@ -1654,16 +1658,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)?;
Expand Down
38 changes: 35 additions & 3 deletions tests/build_fee_bump.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,51 @@ 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");
assert!(!tx.is_explicitly_rbf());
let original_prevouts = tx
.input
.iter()
.map(|txin| txin.previous_output)
.collect::<Vec<_>>();
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");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be worth asserting the two properties this test is covering: that the original does not explicitly signal RBF, and that the replacement shares a prevout with it?

The current assertions only check that the fee increased and the txid changed.

Maybe something like this before moving tx into the wallet:

let tx = psbt.extract_tx().expect("failed to extract tx");
assert!(!tx.is_explicitly_rbf());
let original_prevouts = tx
    .input
    .iter()
    .map(|txin| txin.previous_output)
    .collect::<Vec<_>>();
let txid = tx.compute_txid();
insert_tx(&mut wallet, tx);

And this after extracting new_tx:

assert!(
    new_tx
        .input
        .iter()
        .any(|txin| original_prevouts.contains(&txin.previous_output)),
);

assert!(
new_tx
.input
.iter()
.any(|txin| original_prevouts.contains(&txin.previous_output)),
);
assert_ne!(
new_tx.compute_txid(),
txid,
"replacement transaction must differ from the original"
);

assert_fee_rate!(psbt, fee, feerate, @add_signature);
}

#[test]
Expand Down