Skip to content

Commit a650b78

Browse files
committed
Handle on-chain CBF reorgs in wallet payment tracking
1 parent 131d2de commit a650b78

File tree

3 files changed

+157
-19
lines changed

3 files changed

+157
-19
lines changed

src/chain/cbf.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -598,10 +598,11 @@ impl CbfChainSource {
598598
vec![&*channel_manager, &*chain_monitor, &*output_sweeper];
599599

600600
// Process any queued reorg events before the regular sync.
601-
// A reorg invalidates our last synced height since blocks may have changed.
601+
// A reorg invalidates our last synced heights since blocks may have changed.
602602
let pending_reorgs = std::mem::take(&mut *self.reorg_queue.lock().unwrap());
603603
if !pending_reorgs.is_empty() {
604604
*self.last_lightning_synced_height.lock().unwrap() = None;
605+
*self.last_onchain_synced_height.lock().unwrap() = None;
605606
}
606607
for reorg in &pending_reorgs {
607608
let reorg_set: HashSet<BlockHash> = reorg.reorganized.iter().copied().collect();

src/wallet/mod.rs

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -318,23 +318,61 @@ impl Wallet {
318318

319319
for mut payment in pending_payments {
320320
match payment.details.kind {
321-
PaymentKind::Onchain {
322-
status: ConfirmationStatus::Confirmed { height, .. },
323-
..
324-
} => {
321+
PaymentKind::Onchain { txid, .. } => {
322+
let current_confirmation_status = locked_wallet
323+
.tx_details(txid)
324+
.map(|tx_details| match tx_details.chain_position {
325+
bdk_chain::ChainPosition::Confirmed { anchor, .. } => {
326+
ConfirmationStatus::Confirmed {
327+
block_hash: anchor.block_id.hash,
328+
height: anchor.block_id.height,
329+
timestamp: anchor.confirmation_time,
330+
}
331+
},
332+
bdk_chain::ChainPosition::Unconfirmed { .. } => {
333+
ConfirmationStatus::Unconfirmed
334+
},
335+
});
325336
let payment_id = payment.details.id;
326-
if new_tip.height >= height + ANTI_REORG_DELAY - 1 {
327-
payment.details.status = PaymentStatus::Succeeded;
328-
self.payment_store.insert_or_update(payment.details)?;
329-
self.pending_payment_store.remove(&payment_id)?;
337+
match current_confirmation_status {
338+
Some(ConfirmationStatus::Confirmed {
339+
block_hash,
340+
height,
341+
timestamp,
342+
}) => {
343+
payment.details.kind = PaymentKind::Onchain {
344+
txid,
345+
status: ConfirmationStatus::Confirmed {
346+
block_hash,
347+
height,
348+
timestamp,
349+
},
350+
};
351+
if new_tip.height >= height + ANTI_REORG_DELAY - 1 {
352+
payment.details.status = PaymentStatus::Succeeded;
353+
self.payment_store.insert_or_update(payment.details)?;
354+
self.pending_payment_store.remove(&payment_id)?;
355+
} else {
356+
self.payment_store
357+
.insert_or_update(payment.details.clone())?;
358+
self.pending_payment_store.insert_or_update(payment)?;
359+
}
360+
},
361+
Some(ConfirmationStatus::Unconfirmed) | None => {
362+
payment.details.kind = PaymentKind::Onchain {
363+
txid,
364+
status: ConfirmationStatus::Unconfirmed,
365+
};
366+
payment.details.status = PaymentStatus::Pending;
367+
if payment.details.direction == PaymentDirection::Outbound {
368+
unconfirmed_outbound_txids.push(txid);
369+
}
370+
self.payment_store
371+
.insert_or_update(payment.details.clone())?;
372+
self.pending_payment_store.insert_or_update(payment)?;
373+
},
330374
}
331375
},
332-
PaymentKind::Onchain {
333-
txid,
334-
status: ConfirmationStatus::Unconfirmed,
335-
} if payment.details.direction == PaymentDirection::Outbound => {
336-
unconfirmed_outbound_txids.push(txid);
337-
},
338376
_ => {},
339377
}
340378
}

tests/integration_tests_rust.rs

Lines changed: 103 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,11 @@ use common::{
2121
expect_channel_pending_event, expect_channel_ready_event, expect_channel_ready_events,
2222
expect_event, expect_payment_claimable_event, expect_payment_received_event,
2323
expect_payment_successful_event, expect_splice_pending_event, generate_blocks_and_wait,
24-
open_channel, open_channel_push_amt, open_channel_with_all, premine_and_distribute_funds,
25-
premine_blocks, prepare_rbf, random_chain_source, random_config, random_listening_addresses,
26-
setup_bitcoind_and_electrsd, setup_builder, setup_node, setup_two_nodes, splice_in_with_all,
27-
wait_for_cbf_sync, wait_for_tx, TestChainSource, TestStoreType, TestSyncStore,
24+
invalidate_blocks, open_channel, open_channel_push_amt, open_channel_with_all,
25+
premine_and_distribute_funds, premine_blocks, prepare_rbf, random_chain_source, random_config,
26+
random_listening_addresses, setup_bitcoind_and_electrsd, setup_builder, setup_node,
27+
setup_two_nodes, splice_in_with_all, wait_for_block, wait_for_cbf_sync, wait_for_tx,
28+
TestChainSource, TestStoreType, TestSyncStore,
2829
};
2930
use ldk_node::config::{AsyncPaymentsRole, EsploraSyncConfig};
3031
use ldk_node::entropy::NodeEntropy;
@@ -2877,3 +2878,101 @@ async fn repeated_manual_sync_cbf() {
28772878

28782879
node.stop().unwrap();
28792880
}
2881+
2882+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
2883+
async fn onchain_wallet_sync_cbf_after_reorg() {
2884+
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
2885+
let chain_source = TestChainSource::Cbf(&bitcoind);
2886+
let node = setup_node(&chain_source, random_config(true));
2887+
2888+
let first_addr = node.onchain_payment().new_address().unwrap();
2889+
let first_amount_sat = 100_000;
2890+
premine_and_distribute_funds(
2891+
&bitcoind.client,
2892+
&electrsd.client,
2893+
vec![first_addr],
2894+
Amount::from_sat(first_amount_sat),
2895+
)
2896+
.await;
2897+
2898+
wait_for_cbf_sync(&node).await;
2899+
assert_eq!(node.list_balances().spendable_onchain_balance_sats, first_amount_sat);
2900+
2901+
// Advance the tip so the reorg happens below our synced checkpoint.
2902+
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 2).await;
2903+
wait_for_cbf_sync(&node).await;
2904+
2905+
// Replace the last two blocks with a different branch that has no wallet activity.
2906+
invalidate_blocks(&bitcoind.client, 2);
2907+
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 2).await;
2908+
wait_for_cbf_sync(&node).await;
2909+
2910+
let second_addr = node.onchain_payment().new_address().unwrap();
2911+
let second_amount_sat = 50_000;
2912+
distribute_funds_unconfirmed(
2913+
&bitcoind.client,
2914+
&electrsd.client,
2915+
vec![second_addr],
2916+
Amount::from_sat(second_amount_sat),
2917+
)
2918+
.await;
2919+
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 1).await;
2920+
wait_for_cbf_sync(&node).await;
2921+
2922+
assert_eq!(
2923+
node.list_balances().spendable_onchain_balance_sats,
2924+
first_amount_sat + second_amount_sat
2925+
);
2926+
assert_eq!(
2927+
node.list_payments_with_filter(|p| {
2928+
p.direction == PaymentDirection::Inbound
2929+
&& matches!(p.kind, PaymentKind::Onchain { .. })
2930+
})
2931+
.len(),
2932+
2
2933+
);
2934+
2935+
node.stop().unwrap();
2936+
}
2937+
2938+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
2939+
async fn onchain_wallet_sync_cbf_reorgs_out_confirmed_receive() {
2940+
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
2941+
let chain_source = TestChainSource::Cbf(&bitcoind);
2942+
let node = setup_node(&chain_source, random_config(true));
2943+
2944+
let addr = node.onchain_payment().new_address().unwrap();
2945+
premine_blocks(&bitcoind.client, &electrsd.client).await;
2946+
let cur_height = bitcoind.client.get_blockchain_info().unwrap().blocks as usize;
2947+
let reward_block_hash =
2948+
bitcoind.client.generate_to_address(1, &addr).unwrap().0.pop().unwrap().parse().unwrap();
2949+
wait_for_block(&electrsd.client, cur_height + 1).await;
2950+
let reward_block = bitcoind.client.get_block(reward_block_hash).unwrap();
2951+
let txid = reward_block.txdata[0].compute_txid();
2952+
wait_for_cbf_sync(&node).await;
2953+
assert_eq!(node.list_balances().total_onchain_balance_sats, 5_000_000_000);
2954+
assert_eq!(node.list_balances().spendable_onchain_balance_sats, 0);
2955+
2956+
let payment_id = PaymentId(txid.to_byte_array());
2957+
let payment = node.payment(&payment_id).unwrap();
2958+
assert_eq!(payment.status, PaymentStatus::Pending);
2959+
match payment.kind {
2960+
PaymentKind::Onchain { status: ConfirmationStatus::Confirmed { .. }, .. } => {},
2961+
other => panic!("Unexpected payment state before reorg: {:?}", other),
2962+
}
2963+
2964+
invalidate_blocks(&bitcoind.client, 1);
2965+
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 2).await;
2966+
wait_for_cbf_sync(&node).await;
2967+
2968+
assert_eq!(node.list_balances().total_onchain_balance_sats, 0);
2969+
assert_eq!(node.list_balances().spendable_onchain_balance_sats, 0);
2970+
let payment = node.payment(&payment_id).unwrap();
2971+
assert_eq!(payment.status, PaymentStatus::Pending);
2972+
match payment.kind {
2973+
PaymentKind::Onchain { status: ConfirmationStatus::Unconfirmed, .. } => {},
2974+
other => panic!("Unexpected payment state after reorg: {:?}", other),
2975+
}
2976+
2977+
node.stop().unwrap();
2978+
}

0 commit comments

Comments
 (0)