Skip to content

Commit a08f1fd

Browse files
committed
Apply RBF replacements to the wallet before sync
bump_fee_rbf updated the payment store but not the BDK wallet, so a second bump before sync failed with InvalidPaymentId (or panicked on debug). Apply the replacement to the wallet immediately, with a seen-at after the replaced round. 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. Fixes #1117. AI assistance: OpenAI Codex, Claude Code.
1 parent 6173897 commit a08f1fd

2 files changed

Lines changed: 54 additions & 0 deletions

File tree

‎src/wallet/mod.rs‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2260,6 +2260,23 @@ impl Wallet {
22602260
ConfirmationStatus::Unconfirmed,
22612261
);
22622262

2263+
let seen_at = std::time::SystemTime::now()
2264+
.duration_since(std::time::UNIX_EPOCH)
2265+
.unwrap_or_default()
2266+
.as_secs();
2267+
let previous_seen_at = locked_wallet
2268+
.tx_details(txid)
2269+
.and_then(|details| match details.chain_position {
2270+
bdk_chain::ChainPosition::Unconfirmed { last_seen, .. } => last_seen,
2271+
_ => None,
2272+
})
2273+
.unwrap_or(0);
2274+
// BDK keeps the conflicting tx with the latest last-seen and breaks ties by txid, so a bump
2275+
// in the same second as the replaced round must still be seen strictly after it.
2276+
locked_wallet.apply_unconfirmed_txs([(
2277+
fee_bumped_tx.clone(),
2278+
seen_at.max(previous_seen_at.saturating_add(1)),
2279+
)]);
22632280
let change_set = locked_wallet.take_staged().unwrap_or_default();
22642281
drop(locked_wallet);
22652282
locked_persister.persist_changeset(change_set).await.map_err(|e| {

‎tests/integration_tests_rust.rs‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4667,6 +4667,43 @@ async fn fs_store_persistence_backwards_compatibility() {
46674667
node_new.stop().unwrap();
46684668
}
46694669

4670+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
4671+
async fn onchain_fee_bump_rbf_twice_before_sync() {
4672+
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
4673+
let chain_source = random_chain_source(&bitcoind, &electrsd);
4674+
let (node_a, node_b) = setup_two_nodes(&chain_source, false, false);
4675+
4676+
let addr_a = node_a.onchain_payment().new_address().unwrap();
4677+
let addr_b = node_b.onchain_payment().new_address().unwrap();
4678+
premine_and_distribute_funds(
4679+
&bitcoind.client,
4680+
&electrsd.client,
4681+
vec![addr_a.clone(), addr_b],
4682+
Amount::from_sat(500_000),
4683+
)
4684+
.await;
4685+
node_b.sync_wallets().unwrap();
4686+
let txid = node_b.onchain_payment().send_to_address(&addr_a, 100_000, None).unwrap();
4687+
wait_for_tx(&electrsd.client, txid).await;
4688+
node_b.sync_wallets().unwrap();
4689+
let payment_id = PaymentId(txid.to_byte_array());
4690+
let first_fee = node_b.payment(&payment_id).unwrap().unwrap().fee_paid_msat.unwrap();
4691+
4692+
let first_replacement = node_b.onchain_payment().bump_fee_rbf(payment_id, None).unwrap();
4693+
let second_replacement = node_b.onchain_payment().bump_fee_rbf(payment_id, None).unwrap();
4694+
assert_ne!(first_replacement, second_replacement);
4695+
let payment = node_b.payment(&payment_id).unwrap().unwrap();
4696+
assert!(payment.fee_paid_msat.unwrap() > first_fee);
4697+
assert!(
4698+
matches!(payment.kind, PaymentKind::Onchain { txid, .. } if txid == second_replacement)
4699+
);
4700+
wait_for_tx(&electrsd.client, second_replacement).await;
4701+
node_b.sync_wallets().unwrap();
4702+
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await;
4703+
node_b.sync_wallets().unwrap();
4704+
assert_eq!(node_b.payment(&payment_id).unwrap().unwrap().status, PaymentStatus::Succeeded);
4705+
}
4706+
46704707
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
46714708
async fn onchain_fee_bump_rbf() {
46724709
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();

0 commit comments

Comments
 (0)