From 7ae4f682a38b171620788d00e6424a696c9c2ee8 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Wed, 18 Jun 2025 20:51:16 +0000 Subject: [PATCH 1/7] Drop the need for fork headers when calling `Listen`'s disconnect The `Listen::block_disconnected` method is nice in that listeners learn about each block disconnected in series. Further, it included the header of the block that is being disconnected to allow the listeners to do some checking that the interface is being used correctly (namely, asserting that the header's block hash matches their current understanding of the best chain). However, this interface has some substantial drawbacks. Namely, the requirement that fork headers be passed in means that restarting with a new node that has no idea about a previous fork leaves us unable to replay the chain at all. Further, while when various listeners were initially written learning about each block disconnected in series seemed useful, but now we no longer rely on that anyway because the `Confirm` interface does not allow for it. Thus, here, we replace `Listen::block_disconnected` with a new `Listen::blocks_disconnected`, taking only information about the fork point/new best chain tip (in the form of its block hash and height) rather than information about previous fork blocks and only requiring a single call to complete multiple block disconnections during a reorg. We also swap to using a single `BestBlock` to describe the new chain tip, in anticipation of future extensions to `BestBlock`. This requires removing some assertions on block disconnection ordering, but because we now provide `lightning-block-sync` and expect users to use it when using the `Listen` interface, these assertions are much less critical. --- fuzz/src/full_stack.rs | 5 +-- lightning-block-sync/src/init.rs | 30 +++++++----------- lightning-block-sync/src/lib.rs | 20 ++++++------ lightning-block-sync/src/test_utils.rs | 16 ++++++---- lightning-liquidity/src/manager.rs | 9 ++---- lightning/src/chain/chainmonitor.rs | 15 +++++---- lightning/src/chain/channelmonitor.rs | 38 ++++++++++------------- lightning/src/chain/mod.rs | 20 ++++++------ lightning/src/ln/channelmanager.rs | 19 +++++------- lightning/src/ln/functional_test_utils.rs | 5 +-- lightning/src/util/sweep.rs | 24 ++------------ 11 files changed, 84 insertions(+), 117 deletions(-) diff --git a/fuzz/src/full_stack.rs b/fuzz/src/full_stack.rs index f6fa07199fa..135be0a4e30 100644 --- a/fuzz/src/full_stack.rs +++ b/fuzz/src/full_stack.rs @@ -344,8 +344,9 @@ impl<'a> MoneyLossDetector<'a> { self.header_hashes[self.height - 1].0, self.header_hashes[self.height].1, ); - self.manager.block_disconnected(&header, self.height as u32); - self.monitor.block_disconnected(&header, self.height as u32); + let best_block = BestBlock::new(header.prev_blockhash, self.height as u32 - 1); + self.manager.blocks_disconnected(best_block); + self.monitor.blocks_disconnected(best_block); self.height -= 1; let removal_height = self.height; self.txids_confirmed.retain(|_, height| removal_height != *height); diff --git a/lightning-block-sync/src/init.rs b/lightning-block-sync/src/init.rs index f71a72456dc..ef16dc31c8c 100644 --- a/lightning-block-sync/src/init.rs +++ b/lightning-block-sync/src/init.rs @@ -9,6 +9,7 @@ use bitcoin::hash_types::BlockHash; use bitcoin::network::Network; use lightning::chain; +use lightning::chain::BestBlock; use std::ops::Deref; @@ -230,8 +231,8 @@ impl<'a, L: chain::Listen + ?Sized> chain::Listen for DynamicChainListener<'a, L unreachable!() } - fn block_disconnected(&self, header: &Header, height: u32) { - self.0.block_disconnected(header, height) + fn blocks_disconnected(&self, new_best_block: BestBlock) { + self.0.blocks_disconnected(new_best_block) } } @@ -257,7 +258,7 @@ impl<'a, L: chain::Listen + ?Sized> chain::Listen for ChainListenerSet<'a, L> { } } - fn block_disconnected(&self, _header: &Header, _height: u32) { + fn blocks_disconnected(&self, _new_best_block: BestBlock) { unreachable!() } } @@ -300,19 +301,16 @@ mod tests { let fork_chain_3 = main_chain.fork_at_height(3); let listener_1 = MockChainListener::new() - .expect_block_disconnected(*fork_chain_1.at_height(4)) - .expect_block_disconnected(*fork_chain_1.at_height(3)) - .expect_block_disconnected(*fork_chain_1.at_height(2)) + .expect_blocks_disconnected(*fork_chain_1.at_height(2)) .expect_block_connected(*main_chain.at_height(2)) .expect_block_connected(*main_chain.at_height(3)) .expect_block_connected(*main_chain.at_height(4)); let listener_2 = MockChainListener::new() - .expect_block_disconnected(*fork_chain_2.at_height(4)) - .expect_block_disconnected(*fork_chain_2.at_height(3)) + .expect_blocks_disconnected(*fork_chain_2.at_height(3)) .expect_block_connected(*main_chain.at_height(3)) .expect_block_connected(*main_chain.at_height(4)); let listener_3 = MockChainListener::new() - .expect_block_disconnected(*fork_chain_3.at_height(4)) + .expect_blocks_disconnected(*fork_chain_3.at_height(4)) .expect_block_connected(*main_chain.at_height(4)); let listeners = vec![ @@ -337,23 +335,17 @@ mod tests { let fork_chain_3 = fork_chain_2.fork_at_height(3); let listener_1 = MockChainListener::new() - .expect_block_disconnected(*fork_chain_1.at_height(4)) - .expect_block_disconnected(*fork_chain_1.at_height(3)) - .expect_block_disconnected(*fork_chain_1.at_height(2)) + .expect_blocks_disconnected(*fork_chain_1.at_height(2)) .expect_block_connected(*main_chain.at_height(2)) .expect_block_connected(*main_chain.at_height(3)) .expect_block_connected(*main_chain.at_height(4)); let listener_2 = MockChainListener::new() - .expect_block_disconnected(*fork_chain_2.at_height(4)) - .expect_block_disconnected(*fork_chain_2.at_height(3)) - .expect_block_disconnected(*fork_chain_2.at_height(2)) + .expect_blocks_disconnected(*fork_chain_2.at_height(2)) .expect_block_connected(*main_chain.at_height(2)) .expect_block_connected(*main_chain.at_height(3)) .expect_block_connected(*main_chain.at_height(4)); let listener_3 = MockChainListener::new() - .expect_block_disconnected(*fork_chain_3.at_height(4)) - .expect_block_disconnected(*fork_chain_3.at_height(3)) - .expect_block_disconnected(*fork_chain_3.at_height(2)) + .expect_blocks_disconnected(*fork_chain_3.at_height(2)) .expect_block_connected(*main_chain.at_height(2)) .expect_block_connected(*main_chain.at_height(3)) .expect_block_connected(*main_chain.at_height(4)); @@ -380,7 +372,7 @@ mod tests { let old_tip = fork_chain.tip(); let listener = MockChainListener::new() - .expect_block_disconnected(*old_tip) + .expect_blocks_disconnected(*old_tip) .expect_block_connected(*new_tip); let listeners = vec![(old_tip.block_hash, &listener as &dyn chain::Listen)]; diff --git a/lightning-block-sync/src/lib.rs b/lightning-block-sync/src/lib.rs index 3f981cd8786..2c5370efe58 100644 --- a/lightning-block-sync/src/lib.rs +++ b/lightning-block-sync/src/lib.rs @@ -49,7 +49,7 @@ use bitcoin::hash_types::BlockHash; use bitcoin::pow::Work; use lightning::chain; -use lightning::chain::Listen; +use lightning::chain::{BestBlock, Listen}; use std::future::Future; use std::ops::Deref; @@ -398,12 +398,15 @@ where } /// Notifies the chain listeners of disconnected blocks. - fn disconnect_blocks(&mut self, mut disconnected_blocks: Vec) { - for header in disconnected_blocks.drain(..) { + fn disconnect_blocks(&mut self, disconnected_blocks: Vec) { + for header in disconnected_blocks.iter() { if let Some(cached_header) = self.header_cache.block_disconnected(&header.block_hash) { - assert_eq!(cached_header, header); + assert_eq!(cached_header, *header); } - self.chain_listener.block_disconnected(&header.header, header.height); + } + if let Some(block) = disconnected_blocks.last() { + let best_block = BestBlock::new(block.header.prev_blockhash, block.height - 1); + self.chain_listener.blocks_disconnected(best_block); } } @@ -615,7 +618,7 @@ mod chain_notifier_tests { let new_tip = fork_chain.tip(); let old_tip = main_chain.tip(); let chain_listener = &MockChainListener::new() - .expect_block_disconnected(*old_tip) + .expect_blocks_disconnected(*old_tip) .expect_block_connected(*new_tip); let mut notifier = ChainNotifier { header_cache: &mut main_chain.header_cache(0..=2), chain_listener }; @@ -635,8 +638,7 @@ mod chain_notifier_tests { let new_tip = fork_chain.tip(); let old_tip = main_chain.tip(); let chain_listener = &MockChainListener::new() - .expect_block_disconnected(*old_tip) - .expect_block_disconnected(*main_chain.at_height(2)) + .expect_blocks_disconnected(*main_chain.at_height(2)) .expect_block_connected(*new_tip); let mut notifier = ChainNotifier { header_cache: &mut main_chain.header_cache(0..=3), chain_listener }; @@ -656,7 +658,7 @@ mod chain_notifier_tests { let new_tip = fork_chain.tip(); let old_tip = main_chain.tip(); let chain_listener = &MockChainListener::new() - .expect_block_disconnected(*old_tip) + .expect_blocks_disconnected(*old_tip) .expect_block_connected(*fork_chain.at_height(2)) .expect_block_connected(*new_tip); let mut notifier = diff --git a/lightning-block-sync/src/test_utils.rs b/lightning-block-sync/src/test_utils.rs index 098f1a8769a..2b15f3c81e5 100644 --- a/lightning-block-sync/src/test_utils.rs +++ b/lightning-block-sync/src/test_utils.rs @@ -13,6 +13,7 @@ use bitcoin::transaction; use bitcoin::Transaction; use lightning::chain; +use lightning::chain::BestBlock; use std::cell::RefCell; use std::collections::VecDeque; @@ -203,7 +204,7 @@ impl chain::Listen for NullChainListener { &self, _header: &Header, _txdata: &chain::transaction::TransactionData, _height: u32, ) { } - fn block_disconnected(&self, _header: &Header, _height: u32) {} + fn blocks_disconnected(&self, _new_best_block: BestBlock) {} } pub struct MockChainListener { @@ -231,7 +232,7 @@ impl MockChainListener { self } - pub fn expect_block_disconnected(self, block: BlockHeaderData) -> Self { + pub fn expect_blocks_disconnected(self, block: BlockHeaderData) -> Self { self.expected_blocks_disconnected.borrow_mut().push_back(block); self } @@ -264,14 +265,17 @@ impl chain::Listen for MockChainListener { } } - fn block_disconnected(&self, header: &Header, height: u32) { + fn blocks_disconnected(&self, new_best_block: BestBlock) { match self.expected_blocks_disconnected.borrow_mut().pop_front() { None => { - panic!("Unexpected block disconnected: {:?}", header.block_hash()); + panic!( + "Unexpected block(s) disconnect to {} at height {}", + new_best_block.block_hash, new_best_block.height, + ); }, Some(expected_block) => { - assert_eq!(header.block_hash(), expected_block.header.block_hash()); - assert_eq!(height, expected_block.height); + assert_eq!(new_best_block.block_hash, expected_block.header.prev_blockhash); + assert_eq!(new_best_block.height, expected_block.height - 1); }, } } diff --git a/lightning-liquidity/src/manager.rs b/lightning-liquidity/src/manager.rs index f4cce6855cd..308fa216c92 100644 --- a/lightning-liquidity/src/manager.rs +++ b/lightning-liquidity/src/manager.rs @@ -583,14 +583,9 @@ where self.best_block_updated(header, height); } - fn block_disconnected(&self, header: &bitcoin::block::Header, height: u32) { - let new_height = height - 1; + fn blocks_disconnected(&self, new_best_block: BestBlock) { if let Some(best_block) = self.best_block.write().unwrap().as_mut() { - assert_eq!(best_block.block_hash, header.block_hash(), - "Blocks must be disconnected in chain-order - the disconnected header must be the last connected header"); - assert_eq!(best_block.height, height, - "Blocks must be disconnected in chain-order - the disconnected block must have the correct height"); - *best_block = BestBlock::new(header.prev_blockhash, new_height) + *best_block = new_best_block; } // TODO: Call block_disconnected on all sub-modules that require it, e.g., LSPS1MessageHandler. diff --git a/lightning/src/chain/chainmonitor.rs b/lightning/src/chain/chainmonitor.rs index 46ede6fd850..27cc56b4d99 100644 --- a/lightning/src/chain/chainmonitor.rs +++ b/lightning/src/chain/chainmonitor.rs @@ -33,7 +33,7 @@ use crate::chain::channelmonitor::{ WithChannelMonitor, }; use crate::chain::transaction::{OutPoint, TransactionData}; -use crate::chain::{ChannelMonitorUpdateStatus, Filter, WatchedOutput}; +use crate::chain::{BestBlock, ChannelMonitorUpdateStatus, Filter, WatchedOutput}; use crate::events::{self, Event, EventHandler, ReplayEvent}; use crate::ln::channel_state::ChannelDetails; use crate::ln::msgs::{self, BaseMessageHandler, Init, MessageSendEvent}; @@ -910,18 +910,17 @@ where self.event_notifier.notify(); } - fn block_disconnected(&self, header: &Header, height: u32) { + fn blocks_disconnected(&self, new_best_block: BestBlock) { let monitor_states = self.monitors.read().unwrap(); log_debug!( self.logger, - "Latest block {} at height {} removed via block_disconnected", - header.block_hash(), - height + "Block(s) removed to height {} via blocks_disconnected. New best block is {}", + new_best_block.height, + new_best_block.block_hash, ); for monitor_state in monitor_states.values() { - monitor_state.monitor.block_disconnected( - header, - height, + monitor_state.monitor.blocks_disconnected( + new_best_block, &*self.broadcaster, &*self.fee_estimator, &self.logger, diff --git a/lightning/src/chain/channelmonitor.rs b/lightning/src/chain/channelmonitor.rs index 54f170bcbe3..0ff14fcb59c 100644 --- a/lightning/src/chain/channelmonitor.rs +++ b/lightning/src/chain/channelmonitor.rs @@ -2099,14 +2099,8 @@ impl ChannelMonitor { /// Determines if the disconnected block contained any transactions of interest and updates /// appropriately. - #[rustfmt::skip] - pub fn block_disconnected( - &self, - header: &Header, - height: u32, - broadcaster: B, - fee_estimator: F, - logger: &L, + pub fn blocks_disconnected( + &self, new_best_block: BestBlock, broadcaster: B, fee_estimator: F, logger: &L, ) where B::Target: BroadcasterInterface, F::Target: FeeEstimator, @@ -2114,8 +2108,7 @@ impl ChannelMonitor { { let mut inner = self.inner.lock().unwrap(); let logger = WithChannelMonitor::from_impl(logger, &*inner, None); - inner.block_disconnected( - header, height, broadcaster, fee_estimator, &logger) + inner.blocks_disconnected(new_best_block, broadcaster, fee_estimator, &logger) } /// Processes transactions confirmed in a block with the given header and height, returning new @@ -2149,10 +2142,10 @@ impl ChannelMonitor { /// Processes a transaction that was reorganized out of the chain. /// - /// Used instead of [`block_disconnected`] by clients that are notified of transactions rather + /// Used instead of [`blocks_disconnected`] by clients that are notified of transactions rather /// than blocks. See [`chain::Confirm`] for calling expectations. /// - /// [`block_disconnected`]: Self::block_disconnected + /// [`blocks_disconnected`]: Self::blocks_disconnected #[rustfmt::skip] pub fn transaction_unconfirmed( &self, @@ -4591,12 +4584,12 @@ impl ChannelMonitorImpl { !unmatured_htlcs.contains(&&source), "An unmature HTLC transaction conflicts with a maturing one; failed to \ call either transaction_unconfirmed for the conflicting transaction \ - or block_disconnected for a block containing it."); + or blocks_disconnected for a block containing it."); debug_assert!( !matured_htlcs.contains(&source), "A matured HTLC transaction conflicts with a maturing one; failed to \ call either transaction_unconfirmed for the conflicting transaction \ - or block_disconnected for a block containing it."); + or blocks_disconnected for a block containing it."); matured_htlcs.push(source.clone()); } @@ -4734,26 +4727,27 @@ impl ChannelMonitorImpl { } #[rustfmt::skip] - fn block_disconnected( - &mut self, header: &Header, height: u32, broadcaster: B, fee_estimator: F, logger: &WithChannelMonitor + fn blocks_disconnected( + &mut self, new_best_block: BestBlock, broadcaster: B, fee_estimator: F, logger: &WithChannelMonitor ) where B::Target: BroadcasterInterface, F::Target: FeeEstimator, L::Target: Logger, { - log_trace!(logger, "Block {} at height {} disconnected", header.block_hash(), height); + let new_height = new_best_block.height; + log_trace!(logger, "Block(s) disconnected to height {}", new_height); //We may discard: //- htlc update there as failure-trigger tx (revoked commitment tx, non-revoked commitment tx, HTLC-timeout tx) has been disconnected //- maturing spendable output has transaction paying us has been disconnected - self.onchain_events_awaiting_threshold_conf.retain(|ref entry| entry.height < height); + self.onchain_events_awaiting_threshold_conf.retain(|ref entry| entry.height <= new_height); let bounded_fee_estimator = LowerBoundedFeeEstimator::new(fee_estimator); let conf_target = self.closure_conf_target(); self.onchain_tx_handler.block_disconnected( - height, broadcaster, conf_target, &self.destination_script, &bounded_fee_estimator, logger + new_height + 1, broadcaster, conf_target, &self.destination_script, &bounded_fee_estimator, logger ); - self.best_block = BestBlock::new(header.prev_blockhash, height - 1); + self.best_block = new_best_block; } #[rustfmt::skip] @@ -5198,8 +5192,8 @@ where self.0.block_connected(header, txdata, height, &*self.1, &*self.2, &self.3); } - fn block_disconnected(&self, header: &Header, height: u32) { - self.0.block_disconnected(header, height, &*self.1, &*self.2, &self.3); + fn blocks_disconnected(&self, new_best_block: BestBlock) { + self.0.blocks_disconnected(new_best_block, &*self.1, &*self.2, &self.3); } } diff --git a/lightning/src/chain/mod.rs b/lightning/src/chain/mod.rs index c16ee2519f7..3783281354d 100644 --- a/lightning/src/chain/mod.rs +++ b/lightning/src/chain/mod.rs @@ -84,8 +84,10 @@ pub trait Listen { self.filtered_block_connected(&block.header, &txdata, height); } - /// Notifies the listener that a block was removed at the given height. - fn block_disconnected(&self, header: &Header, height: u32); + /// Notifies the listener that one or more blocks were removed in anticipation of a reorg. + /// + /// Indicates the new best tip is the provided [`BestBlock`]. + fn blocks_disconnected(&self, new_best_block: BestBlock); } /// The `Confirm` trait is used to notify LDK when relevant transactions have been confirmed on @@ -272,7 +274,7 @@ pub trait Watch { /// /// Implementations are responsible for watching the chain for the funding transaction along /// with any spends of outputs returned by [`get_outputs_to_watch`]. In practice, this means - /// calling [`block_connected`] and [`block_disconnected`] on the monitor. + /// calling [`block_connected`] and [`blocks_disconnected`] on the monitor. /// /// A return of `Err(())` indicates that the channel should immediately be force-closed without /// broadcasting the funding transaction. @@ -282,7 +284,7 @@ pub trait Watch { /// /// [`get_outputs_to_watch`]: channelmonitor::ChannelMonitor::get_outputs_to_watch /// [`block_connected`]: channelmonitor::ChannelMonitor::block_connected - /// [`block_disconnected`]: channelmonitor::ChannelMonitor::block_disconnected + /// [`blocks_disconnected`]: channelmonitor::ChannelMonitor::blocks_disconnected fn watch_channel( &self, channel_id: ChannelId, monitor: ChannelMonitor, ) -> Result; @@ -393,8 +395,8 @@ impl Listen for dyn core::ops::Deref { (**self).filtered_block_connected(header, txdata, height); } - fn block_disconnected(&self, header: &Header, height: u32) { - (**self).block_disconnected(header, height); + fn blocks_disconnected(&self, new_best_block: BestBlock) { + (**self).blocks_disconnected(new_best_block); } } @@ -408,9 +410,9 @@ where self.1.filtered_block_connected(header, txdata, height); } - fn block_disconnected(&self, header: &Header, height: u32) { - self.0.block_disconnected(header, height); - self.1.block_disconnected(header, height); + fn blocks_disconnected(&self, new_best_block: BestBlock) { + self.0.blocks_disconnected(new_best_block); + self.1.blocks_disconnected(new_best_block); } } diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index d99d1fb82a4..209af742d17 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -3726,12 +3726,12 @@ where /// Non-proportional fees are fixed according to our risk using the provided fee estimator. /// /// Users need to notify the new `ChannelManager` when a new block is connected or - /// disconnected using its [`block_connected`] and [`block_disconnected`] methods, starting + /// disconnected using its [`block_connected`] and [`blocks_disconnected`] methods, starting /// from after [`params.best_block.block_hash`]. See [`chain::Listen`] and [`chain::Confirm`] for /// more details. /// /// [`block_connected`]: chain::Listen::block_connected - /// [`block_disconnected`]: chain::Listen::block_disconnected + /// [`blocks_disconnected`]: chain::Listen::blocks_disconnected /// [`params.best_block.block_hash`]: chain::BestBlock::block_hash #[rustfmt::skip] pub fn new( @@ -12021,26 +12021,21 @@ where self.best_block_updated(header, height); } - fn block_disconnected(&self, header: &Header, height: u32) { + fn blocks_disconnected(&self, new_best_block: BestBlock) { let _persistence_guard = PersistenceNotifierGuard::optionally_notify_skipping_background_events( self, || -> NotifyOption { NotifyOption::DoPersist }, ); - let new_height = height - 1; { let mut best_block = self.best_block.write().unwrap(); - assert_eq!(best_block.block_hash, header.block_hash(), - "Blocks must be disconnected in chain-order - the disconnected header must be the last connected header"); - assert_eq!(best_block.height, height, - "Blocks must be disconnected in chain-order - the disconnected block must have the correct height"); - *best_block = BestBlock::new(header.prev_blockhash, new_height) + *best_block = new_best_block; } - self.do_chain_event(Some(new_height), |channel| { + self.do_chain_event(Some(new_best_block.height), |channel| { channel.best_block_updated( - new_height, - header.time, + new_best_block.height, + 0, self.chain_hash, &self.node_signer, &self.default_configuration, diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index ce794cd7cb5..a5ff48ad33f 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -350,8 +350,9 @@ pub fn disconnect_blocks<'a, 'b, 'c, 'd>(node: &'a Node<'b, 'c, 'd>, count: u32) match *node.connect_style.borrow() { ConnectStyle::FullBlockViaListen => { - node.chain_monitor.chain_monitor.block_disconnected(&orig.0.header, orig.1); - Listen::block_disconnected(node.node, &orig.0.header, orig.1); + let best_block = BestBlock::new(orig.0.header.prev_blockhash, orig.1 - 1); + node.chain_monitor.chain_monitor.blocks_disconnected(best_block); + Listen::blocks_disconnected(node.node, best_block); }, ConnectStyle::BestBlockFirstSkippingBlocks|ConnectStyle::TransactionsFirstSkippingBlocks| ConnectStyle::HighlyRedundantTransactionsFirstSkippingBlocks|ConnectStyle::TransactionsDuplicativelyFirstSkippingBlocks => { diff --git a/lightning/src/util/sweep.rs b/lightning/src/util/sweep.rs index cea8dd76031..c1435f18815 100644 --- a/lightning/src/util/sweep.rs +++ b/lightning/src/util/sweep.rs @@ -280,16 +280,6 @@ impl OutputSpendStatus { } } - fn confirmation_hash(&self) -> Option { - match self { - Self::PendingInitialBroadcast { .. } => None, - Self::PendingFirstConfirmation { .. } => None, - Self::PendingThresholdConfirmations { confirmation_hash, .. } => { - Some(*confirmation_hash) - }, - } - } - fn latest_spending_tx(&self) -> Option<&Transaction> { match self { Self::PendingInitialBroadcast { .. } => None, @@ -679,21 +669,13 @@ where }); } - fn block_disconnected(&self, header: &Header, height: u32) { + fn blocks_disconnected(&self, new_best_block: BestBlock) { let mut state_lock = self.sweeper_state.lock().unwrap(); - let new_height = height - 1; - let block_hash = header.block_hash(); - - assert_eq!(state_lock.best_block.block_hash, block_hash, - "Blocks must be disconnected in chain-order - the disconnected header must be the last connected header"); - assert_eq!(state_lock.best_block.height, height, - "Blocks must be disconnected in chain-order - the disconnected block must have the correct height"); - state_lock.best_block = BestBlock::new(header.prev_blockhash, new_height); + state_lock.best_block = new_best_block; for output_info in state_lock.outputs.iter_mut() { - if output_info.status.confirmation_hash() == Some(block_hash) { - debug_assert_eq!(output_info.status.confirmation_height(), Some(height)); + if output_info.status.confirmation_height() > Some(new_best_block.height) { output_info.status.unconfirmed(); } } From 3b99b559594226d2dcdbbc4d83f2825d99770e64 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Tue, 1 Jul 2025 20:56:30 +0000 Subject: [PATCH 2/7] f correct panic message on blocks_disconnected --- lightning/src/chain/channelmonitor.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lightning/src/chain/channelmonitor.rs b/lightning/src/chain/channelmonitor.rs index 0ff14fcb59c..bb4c3741a9d 100644 --- a/lightning/src/chain/channelmonitor.rs +++ b/lightning/src/chain/channelmonitor.rs @@ -4584,12 +4584,12 @@ impl ChannelMonitorImpl { !unmatured_htlcs.contains(&&source), "An unmature HTLC transaction conflicts with a maturing one; failed to \ call either transaction_unconfirmed for the conflicting transaction \ - or blocks_disconnected for a block containing it."); + or blocks_disconnected for a before below it."); debug_assert!( !matured_htlcs.contains(&source), "A matured HTLC transaction conflicts with a maturing one; failed to \ call either transaction_unconfirmed for the conflicting transaction \ - or blocks_disconnected for a block containing it."); + or blocks_disconnected for a block before it."); matured_htlcs.push(source.clone()); } From 7ff5313ae58c17c4f984b5a11a9ee92826811b4a Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Tue, 1 Jul 2025 20:59:32 +0000 Subject: [PATCH 3/7] Use similar `blocks_disconnected` semantics in `OnchainTxHandler` `OnchainTxHandler` is an internal struct and doesn't implement `Listen`, but its still nice to have its API mirror the `Listen` API so that internal code all looks similar. --- lightning/src/chain/channelmonitor.rs | 8 ++++---- lightning/src/chain/onchaintx.rs | 22 ++++++++++------------ 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/lightning/src/chain/channelmonitor.rs b/lightning/src/chain/channelmonitor.rs index bb4c3741a9d..abb0a2552cb 100644 --- a/lightning/src/chain/channelmonitor.rs +++ b/lightning/src/chain/channelmonitor.rs @@ -4367,8 +4367,8 @@ impl ChannelMonitorImpl { log_trace!(logger, "Best block re-orged, replaced with new block {} at height {}", block_hash, height); self.onchain_events_awaiting_threshold_conf.retain(|ref entry| entry.height <= height); let conf_target = self.closure_conf_target(); - self.onchain_tx_handler.block_disconnected( - height + 1, broadcaster, conf_target, &self.destination_script, fee_estimator, logger, + self.onchain_tx_handler.blocks_disconnected( + height, broadcaster, conf_target, &self.destination_script, fee_estimator, logger, ); Vec::new() } else { Vec::new() } @@ -4743,8 +4743,8 @@ impl ChannelMonitorImpl { let bounded_fee_estimator = LowerBoundedFeeEstimator::new(fee_estimator); let conf_target = self.closure_conf_target(); - self.onchain_tx_handler.block_disconnected( - new_height + 1, broadcaster, conf_target, &self.destination_script, &bounded_fee_estimator, logger + self.onchain_tx_handler.blocks_disconnected( + new_height, broadcaster, conf_target, &self.destination_script, &bounded_fee_estimator, logger ); self.best_block = new_best_block; diff --git a/lightning/src/chain/onchaintx.rs b/lightning/src/chain/onchaintx.rs index fd0f0d9abf5..9254cf50e7b 100644 --- a/lightning/src/chain/onchaintx.rs +++ b/lightning/src/chain/onchaintx.rs @@ -1112,15 +1112,15 @@ impl OnchainTxHandler { } if let Some(height) = height { - self.block_disconnected( - height, broadcaster, conf_target, destination_script, fee_estimator, logger, + self.blocks_disconnected( + height - 1, broadcaster, conf_target, destination_script, fee_estimator, logger, ); } } #[rustfmt::skip] - pub(super) fn block_disconnected( - &mut self, height: u32, broadcaster: B, conf_target: ConfirmationTarget, + pub(super) fn blocks_disconnected( + &mut self, new_best_height: u32, broadcaster: B, conf_target: ConfirmationTarget, destination_script: &Script, fee_estimator: &LowerBoundedFeeEstimator, logger: &L, ) where B::Target: BroadcasterInterface, @@ -1130,14 +1130,14 @@ impl OnchainTxHandler { let onchain_events_awaiting_threshold_conf = self.onchain_events_awaiting_threshold_conf.drain(..).collect::>(); for entry in onchain_events_awaiting_threshold_conf { - if entry.height >= height { + if entry.height > new_best_height { //- our claim tx on a commitment tx output //- resurect outpoint back in its claimable set and regenerate tx match entry.event { OnchainEvent::ContentiousOutpoint { package } => { if let Some(pending_claim) = self.claimable_outpoints.get(package.outpoints()[0]) { if let Some(request) = self.pending_claim_requests.get_mut(&pending_claim.0) { - assert!(request.merge_package(package, height).is_ok()); + assert!(request.merge_package(package, new_best_height + 1).is_ok()); // Using a HashMap guarantee us than if we have multiple outpoints getting // resurrected only one bump claim tx is going to be broadcast bump_candidates.insert(pending_claim.clone(), request.clone()); @@ -1151,10 +1151,8 @@ impl OnchainTxHandler { } } for ((_claim_id, _), ref mut request) in bump_candidates.iter_mut() { - // `height` is the height being disconnected, so our `current_height` is 1 lower. - let current_height = height - 1; if let Some((new_timer, new_feerate, bump_claim)) = self.generate_claim( - current_height, &request, &FeerateStrategy::ForceBump, conf_target, + new_best_height, &request, &FeerateStrategy::ForceBump, conf_target, destination_script, fee_estimator, logger ) { request.set_timer(new_timer); @@ -1188,9 +1186,9 @@ impl OnchainTxHandler { // right now if one of the outpoint get disconnected, just erase whole pending claim request. let mut remove_request = Vec::new(); self.claimable_outpoints.retain(|_, ref v| - if v.1 >= height { - remove_request.push(v.0.clone()); - false + if v.1 > new_best_height { + remove_request.push(v.0.clone()); + false } else { true }); for req in remove_request { self.pending_claim_requests.remove(&req); From 9fc599ff1d3fd345cb446a72293d38d03600ad6c Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Wed, 18 Jun 2025 20:58:36 +0000 Subject: [PATCH 4/7] Add more robust functional test of `Listen::blocks_disconnected` Now that the `Listen` interface allows blocks to be disconnected in batches rather than one at a time, we should test this. Here we add a new `ConnectStyle` for the functional test framework which tests doing so. --- lightning/src/ln/functional_test_utils.rs | 17 +++++++++++++++-- lightning/src/ln/functional_tests.rs | 14 +++++++++----- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index a5ff48ad33f..f4df2e98b24 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -171,6 +171,9 @@ pub enum ConnectStyle { /// Provides the full block via the `chain::Listen` interface. In the current code this is /// equivalent to `TransactionsFirst` with some additional assertions. FullBlockViaListen, + /// Provides the full block via the `chain::Listen` interface, condensing multiple block + /// disconnections into a single `blocks_disconnected` call. + FullBlockDisconnectionsSkippingViaListen, } impl ConnectStyle { @@ -185,6 +188,7 @@ impl ConnectStyle { ConnectStyle::HighlyRedundantTransactionsFirstSkippingBlocks => true, ConnectStyle::TransactionsFirstReorgsOnlyTip => true, ConnectStyle::FullBlockViaListen => false, + ConnectStyle::FullBlockDisconnectionsSkippingViaListen => false, } } @@ -199,6 +203,7 @@ impl ConnectStyle { ConnectStyle::HighlyRedundantTransactionsFirstSkippingBlocks => false, ConnectStyle::TransactionsFirstReorgsOnlyTip => false, ConnectStyle::FullBlockViaListen => false, + ConnectStyle::FullBlockDisconnectionsSkippingViaListen => false, } } @@ -206,7 +211,7 @@ impl ConnectStyle { use core::hash::{BuildHasher, Hasher}; // Get a random value using the only std API to do so - the DefaultHasher let rand_val = std::collections::hash_map::RandomState::new().build_hasher().finish(); - let res = match rand_val % 9 { + let res = match rand_val % 10 { 0 => ConnectStyle::BestBlockFirst, 1 => ConnectStyle::BestBlockFirstSkippingBlocks, 2 => ConnectStyle::BestBlockFirstReorgsOnlyTip, @@ -216,6 +221,7 @@ impl ConnectStyle { 6 => ConnectStyle::HighlyRedundantTransactionsFirstSkippingBlocks, 7 => ConnectStyle::TransactionsFirstReorgsOnlyTip, 8 => ConnectStyle::FullBlockViaListen, + 9 => ConnectStyle::FullBlockDisconnectionsSkippingViaListen, _ => unreachable!(), }; eprintln!("Using Block Connection Style: {:?}", res); @@ -319,7 +325,7 @@ fn do_connect_block_without_consistency_checks<'a, 'b, 'c, 'd>(node: &'a Node<'b node.node.transactions_confirmed(&block.header, &txdata, height); node.node.best_block_updated(&block.header, height); }, - ConnectStyle::FullBlockViaListen => { + ConnectStyle::FullBlockViaListen|ConnectStyle::FullBlockDisconnectionsSkippingViaListen => { node.chain_monitor.chain_monitor.block_connected(&block, height); node.node.block_connected(&block, height); } @@ -354,6 +360,13 @@ pub fn disconnect_blocks<'a, 'b, 'c, 'd>(node: &'a Node<'b, 'c, 'd>, count: u32) node.chain_monitor.chain_monitor.blocks_disconnected(best_block); Listen::blocks_disconnected(node.node, best_block); }, + ConnectStyle::FullBlockDisconnectionsSkippingViaListen => { + if i == count - 1 { + let best_block = BestBlock::new(orig.0.header.prev_blockhash, orig.1 - 1); + node.chain_monitor.chain_monitor.blocks_disconnected(best_block); + Listen::blocks_disconnected(node.node, best_block); + } + }, ConnectStyle::BestBlockFirstSkippingBlocks|ConnectStyle::TransactionsFirstSkippingBlocks| ConnectStyle::HighlyRedundantTransactionsFirstSkippingBlocks|ConnectStyle::TransactionsDuplicativelyFirstSkippingBlocks => { if i == count - 1 { diff --git a/lightning/src/ln/functional_tests.rs b/lightning/src/ln/functional_tests.rs index 60aa21bc44e..b9d39165cf4 100644 --- a/lightning/src/ln/functional_tests.rs +++ b/lightning/src/ln/functional_tests.rs @@ -2745,11 +2745,15 @@ pub fn test_htlc_ignore_latest_remote_commitment() { let node_a_id = nodes[0].node.get_our_node_id(); let node_b_id = nodes[1].node.get_our_node_id(); - if *nodes[1].connect_style.borrow() == ConnectStyle::FullBlockViaListen { - // We rely on the ability to connect a block redundantly, which isn't allowed via - // `chain::Listen`, so we never run the test if we randomly get assigned that - // connect_style. - return; + match *nodes[1].connect_style.borrow() { + ConnectStyle::FullBlockViaListen + | ConnectStyle::FullBlockDisconnectionsSkippingViaListen => { + // We rely on the ability to connect a block redundantly, which isn't allowed via + // `chain::Listen`, so we never run the test if we randomly get assigned that + // connect_style. + return; + }, + _ => {}, } let funding_tx = create_announced_chan_between_nodes(&nodes, 0, 1).3; let error_message = "Channel force-closed"; From 9385161107a5f413a9e222fa2ea456befad7e10f Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Thu, 19 Jun 2025 15:55:04 +0000 Subject: [PATCH 5/7] Don't pass a latest-block-time to `Channel` unless we have one When calling `Channel::best_block_updated` we pass it the timestamp of the block we're connecting so that it can track the highest timestamp it has seen. However, in some cases, we don't actually have a timestamp to pass, which `Channel::best_block_updated` will happily ignore as it always takes the `max` of its existing value. Thus, we really should pass a `None` to ensure the API is understandable, which we do here. --- lightning/src/ln/channel.rs | 18 +++++++++--------- lightning/src/ln/channelmanager.rs | 23 ++++++++++++++++++++--- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index fb58b51d4dc..adccde87017 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -9291,8 +9291,8 @@ where /// May return some HTLCs (and their payment_hash) which have timed out and should be failed /// back. pub fn best_block_updated( - &mut self, height: u32, highest_header_time: u32, chain_hash: ChainHash, node_signer: &NS, - user_config: &UserConfig, logger: &L, + &mut self, height: u32, highest_header_time: Option, chain_hash: ChainHash, + node_signer: &NS, user_config: &UserConfig, logger: &L, ) -> Result where NS::Target: NodeSigner, @@ -9308,7 +9308,7 @@ where #[rustfmt::skip] fn do_best_block_updated( - &mut self, height: u32, highest_header_time: u32, + &mut self, height: u32, highest_header_time: Option, chain_node_signer: Option<(ChainHash, &NS, &UserConfig)>, logger: &L ) -> Result<(Option, Vec<(HTLCSource, PaymentHash)>, Option), ClosureReason> where @@ -9332,7 +9332,9 @@ where } }); - self.context.update_time_counter = cmp::max(self.context.update_time_counter, highest_header_time); + if let Some(time) = highest_header_time { + self.context.update_time_counter = cmp::max(self.context.update_time_counter, time); + } // Check if the funding transaction was unconfirmed let funding_tx_confirmations = self.funding.get_funding_tx_confirmations(height); @@ -9482,12 +9484,10 @@ where // We handle the funding disconnection by calling best_block_updated with a height one // below where our funding was connected, implying a reorg back to conf_height - 1. let reorg_height = funding.funding_tx_confirmation_height - 1; - // We use the time field to bump the current time we set on channel updates if its - // larger. If we don't know that time has moved forward, we can just set it to the last - // time we saw and it will be ignored. - let best_time = self.context.update_time_counter; - match self.do_best_block_updated(reorg_height, best_time, None::<(ChainHash, &&dyn NodeSigner, &UserConfig)>, logger) { + let signer_config = None::<(ChainHash, &&dyn NodeSigner, &UserConfig)>; + let res = self.do_best_block_updated(reorg_height, None, signer_config, logger); + match res { Ok((channel_ready, timed_out_htlcs, announcement_sigs)) => { assert!(channel_ready.is_none(), "We can't generate a funding with 0 confirmations?"); assert!(timed_out_htlcs.is_empty(), "We can't have accepted HTLCs with a timeout before our funding confirmation?"); diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 209af742d17..8dace935716 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -12035,7 +12035,7 @@ where self.do_chain_event(Some(new_best_block.height), |channel| { channel.best_block_updated( new_best_block.height, - 0, + None, self.chain_hash, &self.node_signer, &self.default_configuration, @@ -12085,7 +12085,17 @@ where let last_best_block_height = self.best_block.read().unwrap().height; if height < last_best_block_height { let timestamp = self.highest_seen_timestamp.load(Ordering::Acquire); - self.do_chain_event(Some(last_best_block_height), |channel| channel.best_block_updated(last_best_block_height, timestamp as u32, self.chain_hash, &self.node_signer, &self.default_configuration, &&WithChannelContext::from(&self.logger, &channel.context, None))); + let do_update = |channel: &mut FundedChannel| { + channel.best_block_updated( + last_best_block_height, + Some(timestamp as u32), + self.chain_hash, + &self.node_signer, + &self.default_configuration, + &&WithChannelContext::from(&self.logger, &channel.context, None) + ) + }; + self.do_chain_event(Some(last_best_block_height), do_update); } } @@ -12145,7 +12155,14 @@ where } } - channel.best_block_updated(height, header.time, self.chain_hash, &self.node_signer, &self.default_configuration, &&WithChannelContext::from(&self.logger, &channel.context, None)) + channel.best_block_updated( + height, + Some(header.time), + self.chain_hash, + &self.node_signer, + &self.default_configuration, + &&WithChannelContext::from(&self.logger, &channel.context, None) + ) }); macro_rules! max_time { From 4ae8fcc3adcf20e065a3ad87fcc80c5fb860a532 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Wed, 25 Jun 2025 18:31:41 +0000 Subject: [PATCH 6/7] Add further additional documentation to `Listen` `Listen` is somewhat quiet on high-level use and even requirements, which we document further here. --- lightning/src/chain/mod.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/lightning/src/chain/mod.rs b/lightning/src/chain/mod.rs index 3783281354d..eda0fab1752 100644 --- a/lightning/src/chain/mod.rs +++ b/lightning/src/chain/mod.rs @@ -71,8 +71,19 @@ impl_writeable_tlv_based!(BestBlock, { /// when needed. /// /// By using [`Listen::filtered_block_connected`] this interface supports clients fetching the -/// entire header chain and only blocks with matching transaction data using BIP 157 filters or +/// entire block chain and only blocks with matching transaction data using BIP 157 filters or /// other similar filtering. +/// +/// Each block must be connected in chain order with one (or more, if using the [`Filter`] +/// interface and a registration occurred during the block processing) call to either +/// [`Listen::block_connected`] or [`Listen::filtered_block_connected`] for each block. +/// +/// In case of a reorg, you must call [`Listen::blocks_disconnected`] once (or more, in +/// reverse-chain order) with information on the "fork point" block, i.e. the highest block which +/// is in both forks. +/// +/// Note that most implementations take a [`BestBlock`] on construction and blocks only need to be +/// applied starting from that point. pub trait Listen { /// Notifies the listener that a block was added at the given height, with the transaction data /// possibly filtered. From a6985ebe1c4fe9913c7bca765edfd9225056f7c0 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Fri, 27 Jun 2025 21:01:11 +0000 Subject: [PATCH 7/7] f clean up docs --- lightning/src/chain/mod.rs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/lightning/src/chain/mod.rs b/lightning/src/chain/mod.rs index eda0fab1752..56cb5765b2b 100644 --- a/lightning/src/chain/mod.rs +++ b/lightning/src/chain/mod.rs @@ -71,16 +71,19 @@ impl_writeable_tlv_based!(BestBlock, { /// when needed. /// /// By using [`Listen::filtered_block_connected`] this interface supports clients fetching the -/// entire block chain and only blocks with matching transaction data using BIP 157 filters or +/// entire header chain and only blocks with matching transaction data using BIP 157 filters or /// other similar filtering. /// -/// Each block must be connected in chain order with one (or more, if using the [`Filter`] -/// interface and a registration occurred during the block processing) call to either -/// [`Listen::block_connected`] or [`Listen::filtered_block_connected`] for each block. +/// Each block must be connected in chain order with one call to either +/// [`Listen::block_connected`] or [`Listen::filtered_block_connected`]. If a call to the +/// [`Filter`] interface was made during block processing and further transaction(s) from the same +/// block now match the filter, a second call to [`Listen::filtered_block_connected`] should be +/// made immediately for the same block (prior to any other calls to the [`Listen`] interface). /// -/// In case of a reorg, you must call [`Listen::blocks_disconnected`] once (or more, in -/// reverse-chain order) with information on the "fork point" block, i.e. the highest block which -/// is in both forks. +/// In case of a reorg, you must call [`Listen::blocks_disconnected`] once with information on the +/// "fork point" block, i.e. the highest block that is in both forks. For backwards compatibility, +/// you may instead walk the chain backwards, calling `blocks_disconnected` for each block which is +/// disconnected in a reorg. /// /// Note that most implementations take a [`BestBlock`] on construction and blocks only need to be /// applied starting from that point.