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
32 changes: 31 additions & 1 deletion src/wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -539,8 +539,13 @@ impl Wallet {
// Collect all conflict txids
let mut conflict_txids: Vec<Txid> =
conflicts.iter().map(|(_, conflict_txid)| *conflict_txid).collect();
if let Some(previous) = self.pending_payment_store.get(&payment_id).await? {
conflict_txids.extend(previous.conflicting_txids);
}

conflict_txids.push(txid);
conflict_txids.sort_unstable();
conflict_txids.dedup();
Comment on lines +542 to +548

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can this ever add anything?

// The payment already exists in the store at this point: `bump_fee_rbf`
// updates the payment store with the replacement txid before the next sync
// cycle, and an id resolved through the candidate history comes from a
Expand Down Expand Up @@ -2084,6 +2089,13 @@ impl Wallet {
},
};

let mut conflicts = self
.pending_payment_store
.get(&payment_id)
.await?
.map(|p| p.conflicting_txids)
.unwrap_or_default();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is independent of #1117. A session of mine found this a little while ago, so I took the opportunity to clean it up. Could you base your PR on https://github.com/jkczyz/ldk-node/commits/2026-09-rbf-middle-round-conflict-list? Then you can drop this and the push/sort/dedup below.

Worth updating your commit's message to include something like:

Once the bump applies the replacement to the wallet, sync no longer emits TxReplaced for the replaced transaction (BDK derives events from a before/after diff of the canonical set), so nothing else populates conflicting_txids after a bump until a later eviction or confirmation flips the canonical set.


let mut locked_persister = self.persister.lock().await;
let mut locked_wallet = self.inner.lock().expect("lock");

Expand Down Expand Up @@ -2260,8 +2272,26 @@ impl Wallet {
ConfirmationStatus::Unconfirmed,
);

conflicts.push(txid);
conflicts.sort_unstable();
conflicts.dedup();
let pending_payment_store =
self.create_pending_payment_from_tx(new_payment.clone(), Vec::new());
self.create_pending_payment_from_tx(new_payment.clone(), conflicts);
let seen_at = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let previous_seen_at = locked_wallet
.tx_details(txid)
.and_then(|details| match details.chain_position {
bdk_chain::ChainPosition::Unconfirmed { last_seen, .. } => last_seen,
_ => None,
})
.unwrap_or(0);
locked_wallet.apply_unconfirmed_txs([(
fee_bumped_tx.clone(),
seen_at.max(previous_seen_at.saturating_add(1)),
)]);
Comment on lines +2291 to +2294

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Worth adding a one-line comment on why + 1 is needed.

let change_set = locked_wallet.take_staged().unwrap_or_default();
drop(locked_wallet);
locked_persister.persist_changeset(change_set).await.map_err(|e| {
Expand Down
37 changes: 37 additions & 0 deletions tests/integration_tests_rust.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4730,6 +4730,43 @@ async fn fs_store_persistence_backwards_compatibility() {
node_new.stop().unwrap();
}

#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn onchain_fee_bump_rbf_twice_before_sync() {
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
let chain_source = random_chain_source(&bitcoind, &electrsd);
let (node_a, node_b) = setup_two_nodes(&chain_source, false, false);

let addr_a = node_a.onchain_payment().new_address().unwrap();
let addr_b = node_b.onchain_payment().new_address().unwrap();
premine_and_distribute_funds(
&bitcoind.client,
&electrsd.client,
vec![addr_a.clone(), addr_b],
Amount::from_sat(500_000),
)
.await;
node_b.sync_wallets().unwrap();
let txid = node_b.onchain_payment().send_to_address(&addr_a, 100_000, None).unwrap();
wait_for_tx(&electrsd.client, txid).await;
node_b.sync_wallets().unwrap();
let payment_id = PaymentId(txid.to_byte_array());
let first_fee = node_b.payment(&payment_id).unwrap().unwrap().fee_paid_msat.unwrap();

let first_replacement = node_b.onchain_payment().bump_fee_rbf(payment_id, None).unwrap();
let second_replacement = node_b.onchain_payment().bump_fee_rbf(payment_id, None).unwrap();
assert_ne!(first_replacement, second_replacement);
let payment = node_b.payment(&payment_id).unwrap().unwrap();
assert!(payment.fee_paid_msat.unwrap() > first_fee);
assert!(
matches!(payment.kind, PaymentKind::Onchain { txid, .. } if txid == second_replacement)
);
wait_for_tx(&electrsd.client, second_replacement).await;
node_b.sync_wallets().unwrap();
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await;
node_b.sync_wallets().unwrap();
assert_eq!(node_b.payment(&payment_id).unwrap().unwrap().status, PaymentStatus::Succeeded);
}

#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn onchain_fee_bump_rbf() {
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
Expand Down
Loading