Skip to content

Commit b56c167

Browse files
authored
Merge pull request #1022 from tnull/2026-08-fix-bitcoind-rest-reorg
Follow reorgs with bitcoind REST
2 parents 5e7250a + 2ab34e3 commit b56c167

2 files changed

Lines changed: 158 additions & 33 deletions

File tree

src/chain/bitcoind.rs

Lines changed: 100 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,12 @@ use crate::{Error, PersistedNodeMetrics};
4949
const CHAIN_POLLING_INTERVAL_SECS: u64 = 2;
5050
const CHAIN_POLLING_TIMEOUT_SECS: u64 = 10;
5151

52+
type BitcoindSpvClient =
53+
SpvClient<ChainPoller<Arc<BitcoindClient>, BitcoindClient>, Arc<ChainListener>>;
54+
5255
pub(super) struct BitcoindChainSource {
5356
api_client: Arc<BitcoindClient>,
57+
spv_client: tokio::sync::Mutex<Option<BitcoindSpvClient>>,
5458
latest_chain_tip: RwLock<Option<ValidatedBlockHeader>>,
5559
wallet_polling_status: Mutex<WalletSyncStatus>,
5660
fee_estimator: Arc<OnchainFeeEstimator>,
@@ -74,9 +78,11 @@ impl BitcoindChainSource {
7478
));
7579

7680
let latest_chain_tip = RwLock::new(None);
81+
let spv_client = tokio::sync::Mutex::new(None);
7782
let wallet_polling_status = Mutex::new(WalletSyncStatus::Completed);
7883
Self {
7984
api_client,
85+
spv_client,
8086
latest_chain_tip,
8187
wallet_polling_status,
8288
fee_estimator,
@@ -103,10 +109,12 @@ impl BitcoindChainSource {
103109
));
104110

105111
let latest_chain_tip = RwLock::new(None);
112+
let spv_client = tokio::sync::Mutex::new(None);
106113
let wallet_polling_status = Mutex::new(WalletSyncStatus::Completed);
107114

108115
Self {
109116
api_client,
117+
spv_client,
110118
latest_chain_tip,
111119
wallet_polling_status,
112120
fee_estimator,
@@ -210,7 +218,16 @@ impl BitcoindChainSource {
210218
)
211219
.await
212220
{
213-
Ok((_header_cache, chain_tip)) => {
221+
Ok((header_cache, chain_tip)) => {
222+
let spv_client = self.new_spv_client(
223+
chain_tip,
224+
header_cache,
225+
Arc::clone(&onchain_wallet),
226+
Arc::clone(&channel_manager),
227+
Arc::clone(&chain_monitor),
228+
Arc::clone(&output_sweeper),
229+
);
230+
*self.spv_client.lock().await = Some(spv_client);
214231
{
215232
let elapsed_ms = now.elapsed().map(|d| d.as_millis()).unwrap_or(0);
216233
log_info!(
@@ -415,19 +432,24 @@ impl BitcoindChainSource {
415432
&self, onchain_wallet: Arc<Wallet>, channel_manager: Arc<ChannelManager>,
416433
chain_monitor: Arc<ChainMonitor>, output_sweeper: Arc<Sweeper>,
417434
) -> Result<(), Error> {
418-
let latest_chain_tip_opt = self.latest_chain_tip.read().expect("lock").clone();
419-
let chain_tip =
420-
if let Some(tip) = latest_chain_tip_opt { tip } else { self.poll_chain_tip().await? };
421-
422-
let chain_poller = ChainPoller::new(Arc::clone(&self.api_client), self.config.network);
423-
let chain_listener = ChainListener {
424-
onchain_wallet: Arc::clone(&onchain_wallet),
425-
channel_manager: Arc::clone(&channel_manager),
426-
chain_monitor: Arc::clone(&chain_monitor),
427-
output_sweeper,
428-
};
429-
let mut spv_client =
430-
SpvClient::new(chain_tip, chain_poller, HeaderCache::new(), &chain_listener);
435+
let mut spv_client_lock = self.spv_client.lock().await;
436+
if spv_client_lock.is_none() {
437+
let latest_chain_tip_opt = self.latest_chain_tip.read().expect("lock").clone();
438+
let chain_tip = if let Some(tip) = latest_chain_tip_opt {
439+
tip
440+
} else {
441+
self.poll_chain_tip().await?
442+
};
443+
*spv_client_lock = Some(self.new_spv_client(
444+
chain_tip,
445+
HeaderCache::new(),
446+
Arc::clone(&onchain_wallet),
447+
Arc::clone(&channel_manager),
448+
chain_monitor,
449+
output_sweeper,
450+
));
451+
}
452+
let spv_client = spv_client_lock.as_mut().expect("initialized above");
431453

432454
let now = SystemTime::now();
433455
match spv_client.poll_best_tip().await {
@@ -442,6 +464,7 @@ impl BitcoindChainSource {
442464
return Err(Error::TxSyncFailed);
443465
},
444466
}
467+
drop(spv_client_lock);
445468

446469
let cur_height = channel_manager.current_best_block().height;
447470

@@ -485,6 +508,21 @@ impl BitcoindChainSource {
485508
Ok(())
486509
}
487510

511+
fn new_spv_client(
512+
&self, chain_tip: ValidatedBlockHeader, header_cache: HeaderCache,
513+
onchain_wallet: Arc<Wallet>, channel_manager: Arc<ChannelManager>,
514+
chain_monitor: Arc<ChainMonitor>, output_sweeper: Arc<Sweeper>,
515+
) -> BitcoindSpvClient {
516+
let chain_poller = ChainPoller::new(Arc::clone(&self.api_client), self.config.network);
517+
let chain_listener = Arc::new(ChainListener {
518+
onchain_wallet: Arc::downgrade(&onchain_wallet),
519+
channel_manager: Arc::downgrade(&channel_manager),
520+
chain_monitor: Arc::downgrade(&chain_monitor),
521+
output_sweeper: Arc::downgrade(&output_sweeper),
522+
});
523+
SpvClient::new(chain_tip, chain_poller, header_cache, chain_listener)
524+
}
525+
488526
pub(super) async fn update_fee_rate_estimates(&self) -> Result<(), Error> {
489527
macro_rules! get_fee_rate_update {
490528
($estimation_fut:expr) => {{
@@ -1280,8 +1318,13 @@ impl BlockSource for BitcoindClient {
12801318
BitcoindClient::Rpc { rpc_client, .. } => {
12811319
rpc_client.get_header(header_hash, height_hint).await
12821320
},
1283-
BitcoindClient::Rest { rest_client, .. } => {
1284-
rest_client.get_header(header_hash, height_hint).await
1321+
BitcoindClient::Rest { rest_client, rpc_client, .. } => {
1322+
match rest_client.get_header(header_hash, height_hint).await {
1323+
Err(e) if e.kind() == BlockSourceErrorKind::Persistent => {
1324+
rpc_client.get_header(header_hash, height_hint).await
1325+
},
1326+
result => result,
1327+
}
12851328
},
12861329
}
12871330
}
@@ -1462,34 +1505,59 @@ pub(crate) enum FeeRateEstimationMode {
14621505
}
14631506

14641507
pub(crate) struct ChainListener {
1465-
pub(crate) onchain_wallet: Arc<Wallet>,
1466-
pub(crate) channel_manager: Arc<ChannelManager>,
1467-
pub(crate) chain_monitor: Arc<ChainMonitor>,
1468-
pub(crate) output_sweeper: Arc<Sweeper>,
1508+
pub(crate) onchain_wallet: std::sync::Weak<Wallet>,
1509+
pub(crate) channel_manager: std::sync::Weak<ChannelManager>,
1510+
pub(crate) chain_monitor: std::sync::Weak<ChainMonitor>,
1511+
pub(crate) output_sweeper: std::sync::Weak<Sweeper>,
1512+
}
1513+
1514+
impl ChainListener {
1515+
fn upgrade(
1516+
&self,
1517+
) -> Option<(Arc<Wallet>, Arc<ChannelManager>, Arc<ChainMonitor>, Arc<Sweeper>)> {
1518+
Some((
1519+
self.onchain_wallet.upgrade()?,
1520+
self.channel_manager.upgrade()?,
1521+
self.chain_monitor.upgrade()?,
1522+
self.output_sweeper.upgrade()?,
1523+
))
1524+
}
14691525
}
14701526

14711527
impl Listen for ChainListener {
14721528
fn filtered_block_connected(
14731529
&self, header: &bitcoin::block::Header,
14741530
txdata: &lightning::chain::transaction::TransactionData, height: u32,
14751531
) {
1476-
self.onchain_wallet.filtered_block_connected(header, txdata, height);
1477-
self.channel_manager.filtered_block_connected(header, txdata, height);
1478-
self.chain_monitor.filtered_block_connected(header, txdata, height);
1479-
self.output_sweeper.filtered_block_connected(header, txdata, height);
1532+
if let Some((onchain_wallet, channel_manager, chain_monitor, output_sweeper)) =
1533+
self.upgrade()
1534+
{
1535+
onchain_wallet.filtered_block_connected(header, txdata, height);
1536+
channel_manager.filtered_block_connected(header, txdata, height);
1537+
chain_monitor.filtered_block_connected(header, txdata, height);
1538+
output_sweeper.filtered_block_connected(header, txdata, height);
1539+
}
14801540
}
14811541
fn block_connected(&self, block: &bitcoin::Block, height: u32) {
1482-
self.onchain_wallet.block_connected(block, height);
1483-
self.channel_manager.block_connected(block, height);
1484-
self.chain_monitor.block_connected(block, height);
1485-
self.output_sweeper.block_connected(block, height);
1542+
if let Some((onchain_wallet, channel_manager, chain_monitor, output_sweeper)) =
1543+
self.upgrade()
1544+
{
1545+
onchain_wallet.block_connected(block, height);
1546+
channel_manager.block_connected(block, height);
1547+
chain_monitor.block_connected(block, height);
1548+
output_sweeper.block_connected(block, height);
1549+
}
14861550
}
14871551

14881552
fn blocks_disconnected(&self, fork_point_block: lightning::chain::BlockLocator) {
1489-
self.onchain_wallet.blocks_disconnected(fork_point_block);
1490-
self.channel_manager.blocks_disconnected(fork_point_block);
1491-
self.chain_monitor.blocks_disconnected(fork_point_block);
1492-
self.output_sweeper.blocks_disconnected(fork_point_block);
1553+
if let Some((onchain_wallet, channel_manager, chain_monitor, output_sweeper)) =
1554+
self.upgrade()
1555+
{
1556+
onchain_wallet.blocks_disconnected(fork_point_block);
1557+
channel_manager.blocks_disconnected(fork_point_block);
1558+
chain_monitor.blocks_disconnected(fork_point_block);
1559+
output_sweeper.blocks_disconnected(fork_point_block);
1560+
}
14931561
}
14941562
}
14951563

tests/reorg_test.rs

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,74 @@ mod common;
22
use std::collections::HashMap;
33

44
use bitcoin::Amount;
5+
use electrsd::corepc_node::mtype::ChainTipsStatus;
56
use ldk_node::payment::{PaymentDirection, PaymentKind};
67
use ldk_node::{Event, LightningBalance, PendingSweepBalance};
78
use proptest::prelude::prop;
89
use proptest::proptest;
10+
use serde_json::json;
911

1012
use crate::common::{
1113
expect_event, exponential_backoff_poll, generate_blocks_and_wait, invalidate_blocks,
1214
open_channel, premine_and_distribute_funds, random_chain_source, random_config,
13-
setup_bitcoind_and_electrsd, setup_node, wait_for_outpoint_spend, wait_for_tx,
15+
setup_bitcoind_and_electrsd, setup_node, wait_for_outpoint_spend, wait_for_tx, TestChainSource,
1416
};
1517

18+
#[test]
19+
fn bitcoind_rest_follows_valid_reorg() {
20+
let rt = tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap();
21+
rt.block_on(async {
22+
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
23+
let node = setup_node(&TestChainSource::BitcoindRestSync(&bitcoind), random_config());
24+
let (bitcoind, electrs) = (&bitcoind.client, &electrsd.client);
25+
26+
generate_blocks_and_wait(bitcoind, electrs, 3).await;
27+
node.sync_wallets().unwrap();
28+
let original_tip = node.status().current_best_block;
29+
let fork_block_hash = bitcoind
30+
.get_block_hash((original_tip.height - 1) as u64)
31+
.expect("failed to get fork block hash")
32+
.block_hash()
33+
.expect("fork block hash should be present");
34+
35+
invalidate_blocks(bitcoind, 2);
36+
generate_blocks_and_wait(bitcoind, electrs, 3).await;
37+
let replacement_tip_hash =
38+
bitcoind.best_block_hash().expect("failed to get replacement tip");
39+
let replacement_tip_height =
40+
bitcoind.get_blockchain_info().expect("failed to get replacement tip height").blocks
41+
as u32;
42+
43+
let _: serde_json::Value = bitcoind
44+
.call("reconsiderblock", &[json!(fork_block_hash)])
45+
.expect("failed to reconsider original branch");
46+
let chain_tips = bitcoind
47+
.get_chain_tips()
48+
.expect("failed to get chain tips")
49+
.into_model()
50+
.expect("failed to parse chain tips")
51+
.0;
52+
assert!(chain_tips.iter().any(|tip| {
53+
tip.hash == original_tip.block_hash && tip.status == ChainTipsStatus::ValidFork
54+
}));
55+
assert!(chain_tips.iter().any(|tip| {
56+
tip.hash == replacement_tip_hash && tip.status == ChainTipsStatus::Active
57+
}));
58+
59+
node.sync_wallets()
60+
.expect("REST-backed node did not follow Bitcoin Core's replacement chain");
61+
let synced_tip = node.status().current_best_block;
62+
assert_eq!(
63+
synced_tip.block_hash, replacement_tip_hash,
64+
"REST-backed node did not follow Bitcoin Core's replacement chain"
65+
);
66+
assert_eq!(
67+
synced_tip.height, replacement_tip_height,
68+
"REST-backed node did not follow Bitcoin Core's replacement chain"
69+
);
70+
})
71+
}
72+
1673
async fn wait_for_pending_sweep_balance<F>(
1774
node: &ldk_node::Node, mut matches_balance: F,
1875
) -> PendingSweepBalance

0 commit comments

Comments
 (0)