Skip to content
Merged
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
5 changes: 3 additions & 2 deletions fuzz/src/full_stack.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -343,8 +343,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);
Expand Down
30 changes: 11 additions & 19 deletions lightning-block-sync/src/init.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@ use bitcoin::hash_types::BlockHash;
use bitcoin::network::Network;

use lightning::chain;
use lightning::chain::BestBlock;

use std::ops::Deref;

Expand DownExpand Up@@ -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, fork_point: BestBlock) {
self.0.blocks_disconnected(fork_point)
}
}

Expand All@@ -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, _fork_point: BestBlock) {
unreachable!()
}
}
Expand DownExpand Up@@ -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(1))
.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(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_blocks_disconnected(*fork_chain_3.at_height(3))
.expect_block_connected(*main_chain.at_height(4));

let listeners = vec![
Expand All@@ -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(1))
.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(1))
.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(1))
.expect_block_connected(*main_chain.at_height(2))
.expect_block_connected(*main_chain.at_height(3))
.expect_block_connected(*main_chain.at_height(4));
Expand All@@ -380,7 +372,7 @@ mod tests {
let old_tip = fork_chain.tip();

let listener = MockChainListener::new()
.expect_block_disconnected(*old_tip)
.expect_blocks_disconnected(*fork_chain.at_height(1))
.expect_block_connected(*new_tip);

let listeners = vec![(old_tip.block_hash, &listener as &dyn chain::Listen)];
Expand Down
20 changes: 11 additions & 9 deletions lightning-block-sync/src/lib.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
Expand DownExpand Up@@ -398,12 +398,15 @@ where
}

/// Notifies the chain listeners of disconnected blocks.
fn disconnect_blocks(&mut self, mut disconnected_blocks: Vec<ValidatedBlockHeader>) {
for header in disconnected_blocks.drain(..) {
fn disconnect_blocks(&mut self, disconnected_blocks: Vec<ValidatedBlockHeader>) {
for header in disconnected_blocks.iter() {
if let Some(cached_header) = self.header_cache.block_disconnected(&header.block_hash) {

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.

Should we change the Cache API accordingly?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

We could, but it seems somewhat disjoint from this PR, given there's further work to do in lightning-block-sync anyway, so I kinda wanted to keep it as small as can be.

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.

Mhh, might be good to document somewhere what steps you deem left before you'd consider the transition to this new approach complete. I guess all of them should land before the next release then, to not end up with a half-migrated codebase in the release?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

I mean the API/stuff in lightning in in a perfectly fine state after this PR, and lightning-block-sync just deserves a bit of efficiency in dropping a vec and improving the cache afterwards. This PR obviously doesn't accomplish the goals that #3600 set out to, but is just a step towards it (the "complicated" step that requires making sure all the lightning-internal stuff isn't broken by changing the API).

AFAIU, to get to the point that bitcoind (and all) syncs support swapping to a new chain during a reorg and don't fetch a pile of redundant blocks we need to:

  • make BestBlock contain a list of 6(ish?) block hashes, not one
  • use that list in lightning-block-sync (and eventually lightning-transaction-sync) to do the sync without leaning on the cache
  • expand the runtime caching to cache partial chains across clients in the init sync logic
  • (eventually) ensure lightning-block-sync doesn't have any spare vecs or whatever lying around.

@tnulltnullJul 3, 2025

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.

I still think it would make sense to open an issue for this larger refactoring you're planning, as there are connected aspects to discuss that don't really belong in this PR.

make BestBlock contain a list of 6(ish?) block hashes, not one

For example, how do we imagine this to work exactly, especially for the Confirm/lightning-transaction-sync case?

For Esplora, we currently retrieve the tip hash, retrieve the header and the block status (including the hight), before we call best_block_updated, which is already 3 costly RTTs. If we now would require to track the 6 most-recent hashes, we'd need to make at least 5 more subsequent calls (bringing this it at least 8 RTTs per sync round) to retrieve the hashes at [(height-5)..(height-1)]. But, given that these are individual calls, there is no way to tell if any reorg happened during some of these calls, so they are inherently race-y.

For Electrum, the results would be very similar for the polling client in its current form. Note we eventually want to switch the client to a streaming version making use of Electum's subscription model though, and requiring 'last 6 blocks' would probably require us to resort to costly polling again.

If we'd otherwise extend the client to start tracking more of the chain state (i.e., actually tracking a local chain 6-suffix) across rounds this would complicate the client logic quite a bit and make it even more susceptible to race conditions.

TLDR: I still maintain all of this would be/is resulting in a major refactor across our chain syncing crates and logic, and it would be great to discuss what we imagine to be involved and the corresponding trade-offs before we just jump into it heads first.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

We can definitely make it optional (and only used for full-node-sync) if we think its a major issue. Of course the issue becomes that you cannot (even today) safely transition from an esplora/electrum-synced node to a full-chain-sync one, but addressing that really requires a substantial rework of how we think about chain sync, so maybe its not worth worrying about that.

Of course, storing the last 6 blocks is pretty trivial for a full-node-sync client, and the point is really just to expose the thing that the sync driver gave us, so its not like we'd use it in Listen/Confirm implementations.

My understanding is that this would address the restart-with-different-node case that #3600 wanted to address, even if its optional and just used by the full-node-syncing logic?

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.

but addressing that really requires a substantial rework of how we think about chain sync, so maybe its not worth worrying about that.

Hmm, given that we even recently had users inquire whether they could switch chain sources at runtime, I do expect that we will need to tackle this eventually, as there will be users wanting to switch chain sources, and it's very unintuitive to either prohibit that, have it one-way, or even document the risks that might come from it.

Of course, storing the last 6 blocks is pretty trivial for a full-node-sync client, and the point is really just to expose the thing that the sync driver gave us, so its not like we'd use it in Listen/Confirm implementations.

So we would only do it when it's convenient, which means we could switch bitcoind nodes, but still not between chain sources?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Hmm, given that we even recently had users inquire whether they could switch chain sources at runtime, I do expect that we will need to tackle this eventually, as there will be users wanting to switch chain sources, and it's very unintuitive to either prohibit that, have it one-way, or even document the risks that might come from it.

Sure, I agree, hence why I suggested moving to storing several recent blocks rather than only one. However, as you note it might not be super practical, so we're kinda stuck.

I think, basically, if we did that plus required transactions_confirmed calls come before best_block_updated calls (which may we're doing anyway, cc #3867 (comment)) then moving from one chain source to another is totally fine, even at runtime (as long as you don't miss transactions).

So we would only do it when it's convenient, which means we could switch bitcoind nodes, but still not between chain sources?

I mean I'm not sure what else we can do? If its too impractical to do it on some chain sources we can't really make it required, and if its not available switching chain sources isn't going to be 100% reliable.

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() {

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.

If we're now happy to use the fork point, why are we keeping a Vec of all disconnected blocks around at all? Shouldn't we now be able to convert this to the new behavior entirely? Seems ChainDifference::common_ancestor would already provide what we need, no?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Its needed by the cache, which I didn't want to change in this PR - #3876 (comment)

let fork_point = BestBlock::new(block.header.prev_blockhash, block.height - 1);
self.chain_listener.blocks_disconnected(fork_point);
}
}

Expand DownExpand Up@@ -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(*fork_chain.at_height(1))
.expect_block_connected(*new_tip);
let mut notifier =
ChainNotifier { header_cache: &mut main_chain.header_cache(0..=2), chain_listener };
Expand All@@ -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(1))
.expect_block_connected(*new_tip);
let mut notifier =
ChainNotifier { header_cache: &mut main_chain.header_cache(0..=3), chain_listener };
Expand All@@ -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(*fork_chain.at_height(1))
.expect_block_connected(*fork_chain.at_height(2))
.expect_block_connected(*new_tip);
let mut notifier =
Expand Down
20 changes: 12 additions & 8 deletions lightning-block-sync/src/test_utils.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
Expand DownExpand Up@@ -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, _fork_point: BestBlock) {}
}

pub struct MockChainListener {
Expand DownExpand Up@@ -231,8 +232,8 @@ impl MockChainListener {
self
}

pub fn expect_block_disconnected(self, block: BlockHeaderData) -> Self {
self.expected_blocks_disconnected.borrow_mut().push_back(block);
pub fn expect_blocks_disconnected(self, fork_point: BlockHeaderData) -> Self {
self.expected_blocks_disconnected.borrow_mut().push_back(fork_point);
self
}
}
Expand DownExpand Up@@ -264,14 +265,17 @@ impl chain::Listen for MockChainListener {
}
}

fn block_disconnected(&self, header: &Header, height: u32) {
fn blocks_disconnected(&self, fork_point: BestBlock) {
match self.expected_blocks_disconnected.borrow_mut().pop_front() {
None => {
panic!("Unexpected block disconnected: {:?}", header.block_hash());
panic!(
"Unexpected block(s) disconnected {} at height {}",
fork_point.block_hash, fork_point.height,
);
},
Some(expected_block) => {
assert_eq!(header.block_hash(), expected_block.header.block_hash());
assert_eq!(height, expected_block.height);
Some(expected) => {
assert_eq!(fork_point.block_hash, expected.header.block_hash());
assert_eq!(fork_point.height, expected.height);
},
}
}
Expand Down
11 changes: 4 additions & 7 deletions lightning-liquidity/src/manager.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -767,14 +767,11 @@ 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, fork_point: 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");
Comment thread
TheBlueMatt marked this conversation as resolved.
*best_block = BestBlock::new(header.prev_blockhash, new_height)
assert!(best_block.height > fork_point.height,
"Blocks disconnected must indicate disconnection from the current best height, i.e. the new chain tip must be lower than the previous best height");
*best_block = fork_point;
}

// TODO: Call block_disconnected on all sub-modules that require it, e.g., LSPS1MessageHandler.
Expand Down
15 changes: 7 additions & 8 deletions lightning/src/chain/chainmonitor.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,7 +35,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;
#[cfg(peer_storage)]
Expand DownExpand Up@@ -1008,18 +1008,17 @@ where
self.event_notifier.notify();
}

fn block_disconnected(&self, header: &Header, height: u32) {
fn blocks_disconnected(&self, fork_point: 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 {}",
fork_point.height,
fork_point.block_hash,
);
for monitor_state in monitor_states.values() {
monitor_state.monitor.block_disconnected(
header,
height,
monitor_state.monitor.blocks_disconnected(
fork_point,
&*self.broadcaster,
&*self.fee_estimator,
&self.logger,
Expand Down
48 changes: 22 additions & 26 deletions lightning/src/chain/channelmonitor.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -2297,23 +2297,16 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitor<Signer> {

/// Determines if the disconnected block contained any transactions of interest and updates
/// appropriately.
#[rustfmt::skip]
pub fn block_disconnected<B: Deref, F: Deref, L: Deref>(
&self,
header: &Header,
height: u32,
broadcaster: B,
fee_estimator: F,
logger: &L,
pub fn blocks_disconnected<B: Deref, F: Deref, L: Deref>(
&self, fork_point: BestBlock, broadcaster: B, fee_estimator: F, logger: &L,
) where
B::Target: BroadcasterInterface,
F::Target: FeeEstimator,
L::Target: Logger,
{
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(fork_point, broadcaster, fee_estimator, &logger)
}

/// Processes transactions confirmed in a block with the given header and height, returning new
Expand DownExpand Up@@ -2347,10 +2340,10 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitor<Signer> {

/// 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<B: Deref, F: Deref, L: Deref>(
&self,
Expand DownExpand Up@@ -5113,8 +5106,8 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
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() }
Expand DownExpand Up@@ -5441,12 +5434,12 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
!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 before 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 before it.");
matured_htlcs.push(source.clone());
}

Expand DownExpand Up@@ -5594,23 +5587,26 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
}

#[rustfmt::skip]
fn block_disconnected<B: Deref, F: Deref, L: Deref>(
&mut self, header: &Header, height: u32, broadcaster: B, fee_estimator: F, logger: &WithChannelMonitor<L>
fn blocks_disconnected<B: Deref, F: Deref, L: Deref>(
&mut self, fork_point: BestBlock, broadcaster: B, fee_estimator: F, logger: &WithChannelMonitor<L>
) where B::Target: BroadcasterInterface,
F::Target: FeeEstimator,
L::Target: Logger,
{
log_trace!(logger, "Block {} at height {} disconnected", header.block_hash(), height);
let new_height = fork_point.height;
log_trace!(logger, "Block(s) disconnected to height {}", new_height);
assert!(self.best_block.height > fork_point.height,
"Blocks disconnected must indicate disconnection from the current best height, i.e. the new chain tip must be lower than the previous best 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);

// TODO: Replace with `take_if` once our MSRV is >= 1.80.
let mut should_broadcast_commitment = false;
if let Some((_, conf_height)) = self.alternative_funding_confirmed.as_ref() {
if *conf_height == height {
if *conf_height > new_height {
self.alternative_funding_confirmed.take();
if self.holder_tx_signed || self.funding_spend_seen {
// Cancel any previous claims that are no longer valid as they stemmed from a
Expand All@@ -5626,8 +5622,8 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {

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
self.onchain_tx_handler.blocks_disconnected(
new_height, &broadcaster, conf_target, &self.destination_script, &bounded_fee_estimator, logger
);

// Only attempt to broadcast the new commitment after the `block_disconnected` call above so that
Expand All@@ -5636,7 +5632,7 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
self.queue_latest_holder_commitment_txn_for_broadcast(&broadcaster, &bounded_fee_estimator, logger);
}

self.best_block = BestBlock::new(header.prev_blockhash, height - 1);
self.best_block = fork_point;
}

#[rustfmt::skip]
Expand DownExpand Up@@ -6110,8 +6106,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, fork_point: BestBlock) {
self.0.blocks_disconnected(fork_point, &*self.1, &*self.2, &self.3);
}
}

Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Drop the need for fork headers when calling Listen's disconnect by TheBlueMatt · Pull Request #3876 · lightningdevkit/rust-lightning · GitHub
Skip to content
Merged
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
5 changes: 3 additions & 2 deletions fuzz/src/full_stack.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -343,8 +343,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);
Expand Down
30 changes: 11 additions & 19 deletions lightning-block-sync/src/init.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@ use bitcoin::hash_types::BlockHash;
use bitcoin::network::Network;

use lightning::chain;
use lightning::chain::BestBlock;

use std::ops::Deref;

Expand DownExpand Up@@ -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, fork_point: BestBlock) {
self.0.blocks_disconnected(fork_point)
}
}

Expand All@@ -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, _fork_point: BestBlock) {
unreachable!()
}
}
Expand DownExpand Up@@ -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(1))
.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(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_blocks_disconnected(*fork_chain_3.at_height(3))
.expect_block_connected(*main_chain.at_height(4));

let listeners = vec![
Expand All@@ -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(1))
.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(1))
.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(1))
.expect_block_connected(*main_chain.at_height(2))
.expect_block_connected(*main_chain.at_height(3))
.expect_block_connected(*main_chain.at_height(4));
Expand All@@ -380,7 +372,7 @@ mod tests {
let old_tip = fork_chain.tip();

let listener = MockChainListener::new()
.expect_block_disconnected(*old_tip)
.expect_blocks_disconnected(*fork_chain.at_height(1))
.expect_block_connected(*new_tip);

let listeners = vec![(old_tip.block_hash, &listener as &dyn chain::Listen)];
Expand Down
20 changes: 11 additions & 9 deletions lightning-block-sync/src/lib.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
Expand DownExpand Up@@ -398,12 +398,15 @@ where
}

/// Notifies the chain listeners of disconnected blocks.
fn disconnect_blocks(&mut self, mut disconnected_blocks: Vec<ValidatedBlockHeader>) {
for header in disconnected_blocks.drain(..) {
fn disconnect_blocks(&mut self, disconnected_blocks: Vec<ValidatedBlockHeader>) {
for header in disconnected_blocks.iter() {
if let Some(cached_header) = self.header_cache.block_disconnected(&header.block_hash) {

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.

Should we change the Cache API accordingly?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

We could, but it seems somewhat disjoint from this PR, given there's further work to do in lightning-block-sync anyway, so I kinda wanted to keep it as small as can be.

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.

Mhh, might be good to document somewhere what steps you deem left before you'd consider the transition to this new approach complete. I guess all of them should land before the next release then, to not end up with a half-migrated codebase in the release?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

I mean the API/stuff in lightning in in a perfectly fine state after this PR, and lightning-block-sync just deserves a bit of efficiency in dropping a vec and improving the cache afterwards. This PR obviously doesn't accomplish the goals that #3600 set out to, but is just a step towards it (the "complicated" step that requires making sure all the lightning-internal stuff isn't broken by changing the API).

AFAIU, to get to the point that bitcoind (and all) syncs support swapping to a new chain during a reorg and don't fetch a pile of redundant blocks we need to:

  • make BestBlock contain a list of 6(ish?) block hashes, not one
  • use that list in lightning-block-sync (and eventually lightning-transaction-sync) to do the sync without leaning on the cache
  • expand the runtime caching to cache partial chains across clients in the init sync logic
  • (eventually) ensure lightning-block-sync doesn't have any spare vecs or whatever lying around.

@tnulltnullJul 3, 2025

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.

I still think it would make sense to open an issue for this larger refactoring you're planning, as there are connected aspects to discuss that don't really belong in this PR.

make BestBlock contain a list of 6(ish?) block hashes, not one

For example, how do we imagine this to work exactly, especially for the Confirm/lightning-transaction-sync case?

For Esplora, we currently retrieve the tip hash, retrieve the header and the block status (including the hight), before we call best_block_updated, which is already 3 costly RTTs. If we now would require to track the 6 most-recent hashes, we'd need to make at least 5 more subsequent calls (bringing this it at least 8 RTTs per sync round) to retrieve the hashes at [(height-5)..(height-1)]. But, given that these are individual calls, there is no way to tell if any reorg happened during some of these calls, so they are inherently race-y.

For Electrum, the results would be very similar for the polling client in its current form. Note we eventually want to switch the client to a streaming version making use of Electum's subscription model though, and requiring 'last 6 blocks' would probably require us to resort to costly polling again.

If we'd otherwise extend the client to start tracking more of the chain state (i.e., actually tracking a local chain 6-suffix) across rounds this would complicate the client logic quite a bit and make it even more susceptible to race conditions.

TLDR: I still maintain all of this would be/is resulting in a major refactor across our chain syncing crates and logic, and it would be great to discuss what we imagine to be involved and the corresponding trade-offs before we just jump into it heads first.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

We can definitely make it optional (and only used for full-node-sync) if we think its a major issue. Of course the issue becomes that you cannot (even today) safely transition from an esplora/electrum-synced node to a full-chain-sync one, but addressing that really requires a substantial rework of how we think about chain sync, so maybe its not worth worrying about that.

Of course, storing the last 6 blocks is pretty trivial for a full-node-sync client, and the point is really just to expose the thing that the sync driver gave us, so its not like we'd use it in Listen/Confirm implementations.

My understanding is that this would address the restart-with-different-node case that #3600 wanted to address, even if its optional and just used by the full-node-syncing logic?

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.

but addressing that really requires a substantial rework of how we think about chain sync, so maybe its not worth worrying about that.

Hmm, given that we even recently had users inquire whether they could switch chain sources at runtime, I do expect that we will need to tackle this eventually, as there will be users wanting to switch chain sources, and it's very unintuitive to either prohibit that, have it one-way, or even document the risks that might come from it.

Of course, storing the last 6 blocks is pretty trivial for a full-node-sync client, and the point is really just to expose the thing that the sync driver gave us, so its not like we'd use it in Listen/Confirm implementations.

So we would only do it when it's convenient, which means we could switch bitcoind nodes, but still not between chain sources?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Hmm, given that we even recently had users inquire whether they could switch chain sources at runtime, I do expect that we will need to tackle this eventually, as there will be users wanting to switch chain sources, and it's very unintuitive to either prohibit that, have it one-way, or even document the risks that might come from it.

Sure, I agree, hence why I suggested moving to storing several recent blocks rather than only one. However, as you note it might not be super practical, so we're kinda stuck.

I think, basically, if we did that plus required transactions_confirmed calls come before best_block_updated calls (which may we're doing anyway, cc #3867 (comment)) then moving from one chain source to another is totally fine, even at runtime (as long as you don't miss transactions).

So we would only do it when it's convenient, which means we could switch bitcoind nodes, but still not between chain sources?

I mean I'm not sure what else we can do? If its too impractical to do it on some chain sources we can't really make it required, and if its not available switching chain sources isn't going to be 100% reliable.

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() {

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.

If we're now happy to use the fork point, why are we keeping a Vec of all disconnected blocks around at all? Shouldn't we now be able to convert this to the new behavior entirely? Seems ChainDifference::common_ancestor would already provide what we need, no?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Its needed by the cache, which I didn't want to change in this PR - #3876 (comment)

let fork_point = BestBlock::new(block.header.prev_blockhash, block.height - 1);
self.chain_listener.blocks_disconnected(fork_point);
}
}

Expand DownExpand Up@@ -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(*fork_chain.at_height(1))
.expect_block_connected(*new_tip);
let mut notifier =
ChainNotifier { header_cache: &mut main_chain.header_cache(0..=2), chain_listener };
Expand All@@ -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(1))
.expect_block_connected(*new_tip);
let mut notifier =
ChainNotifier { header_cache: &mut main_chain.header_cache(0..=3), chain_listener };
Expand All@@ -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(*fork_chain.at_height(1))
.expect_block_connected(*fork_chain.at_height(2))
.expect_block_connected(*new_tip);
let mut notifier =
Expand Down
20 changes: 12 additions & 8 deletions lightning-block-sync/src/test_utils.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
Expand DownExpand Up@@ -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, _fork_point: BestBlock) {}
}

pub struct MockChainListener {
Expand DownExpand Up@@ -231,8 +232,8 @@ impl MockChainListener {
self
}

pub fn expect_block_disconnected(self, block: BlockHeaderData) -> Self {
self.expected_blocks_disconnected.borrow_mut().push_back(block);
pub fn expect_blocks_disconnected(self, fork_point: BlockHeaderData) -> Self {
self.expected_blocks_disconnected.borrow_mut().push_back(fork_point);
self
}
}
Expand DownExpand Up@@ -264,14 +265,17 @@ impl chain::Listen for MockChainListener {
}
}

fn block_disconnected(&self, header: &Header, height: u32) {
fn blocks_disconnected(&self, fork_point: BestBlock) {
match self.expected_blocks_disconnected.borrow_mut().pop_front() {
None => {
panic!("Unexpected block disconnected: {:?}", header.block_hash());
panic!(
"Unexpected block(s) disconnected {} at height {}",
fork_point.block_hash, fork_point.height,
);
},
Some(expected_block) => {
assert_eq!(header.block_hash(), expected_block.header.block_hash());
assert_eq!(height, expected_block.height);
Some(expected) => {
assert_eq!(fork_point.block_hash, expected.header.block_hash());
assert_eq!(fork_point.height, expected.height);
},
}
}
Expand Down
11 changes: 4 additions & 7 deletions lightning-liquidity/src/manager.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -767,14 +767,11 @@ 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, fork_point: 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");
Comment thread
TheBlueMatt marked this conversation as resolved.
*best_block = BestBlock::new(header.prev_blockhash, new_height)
assert!(best_block.height > fork_point.height,
"Blocks disconnected must indicate disconnection from the current best height, i.e. the new chain tip must be lower than the previous best height");
*best_block = fork_point;
}

// TODO: Call block_disconnected on all sub-modules that require it, e.g., LSPS1MessageHandler.
Expand Down
15 changes: 7 additions & 8 deletions lightning/src/chain/chainmonitor.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,7 +35,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;
#[cfg(peer_storage)]
Expand DownExpand Up@@ -1008,18 +1008,17 @@ where
self.event_notifier.notify();
}

fn block_disconnected(&self, header: &Header, height: u32) {
fn blocks_disconnected(&self, fork_point: 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 {}",
fork_point.height,
fork_point.block_hash,
);
for monitor_state in monitor_states.values() {
monitor_state.monitor.block_disconnected(
header,
height,
monitor_state.monitor.blocks_disconnected(
fork_point,
&*self.broadcaster,
&*self.fee_estimator,
&self.logger,
Expand Down
48 changes: 22 additions & 26 deletions lightning/src/chain/channelmonitor.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -2297,23 +2297,16 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitor<Signer> {

/// Determines if the disconnected block contained any transactions of interest and updates
/// appropriately.
#[rustfmt::skip]
pub fn block_disconnected<B: Deref, F: Deref, L: Deref>(
&self,
header: &Header,
height: u32,
broadcaster: B,
fee_estimator: F,
logger: &L,
pub fn blocks_disconnected<B: Deref, F: Deref, L: Deref>(
&self, fork_point: BestBlock, broadcaster: B, fee_estimator: F, logger: &L,
) where
B::Target: BroadcasterInterface,
F::Target: FeeEstimator,
L::Target: Logger,
{
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(fork_point, broadcaster, fee_estimator, &logger)
}

/// Processes transactions confirmed in a block with the given header and height, returning new
Expand DownExpand Up@@ -2347,10 +2340,10 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitor<Signer> {

/// 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<B: Deref, F: Deref, L: Deref>(
&self,
Expand DownExpand Up@@ -5113,8 +5106,8 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
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() }
Expand DownExpand Up@@ -5441,12 +5434,12 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
!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 before 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 before it.");
matured_htlcs.push(source.clone());
}

Expand DownExpand Up@@ -5594,23 +5587,26 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
}

#[rustfmt::skip]
fn block_disconnected<B: Deref, F: Deref, L: Deref>(
&mut self, header: &Header, height: u32, broadcaster: B, fee_estimator: F, logger: &WithChannelMonitor<L>
fn blocks_disconnected<B: Deref, F: Deref, L: Deref>(
&mut self, fork_point: BestBlock, broadcaster: B, fee_estimator: F, logger: &WithChannelMonitor<L>
) where B::Target: BroadcasterInterface,
F::Target: FeeEstimator,
L::Target: Logger,
{
log_trace!(logger, "Block {} at height {} disconnected", header.block_hash(), height);
let new_height = fork_point.height;
log_trace!(logger, "Block(s) disconnected to height {}", new_height);
assert!(self.best_block.height > fork_point.height,
"Blocks disconnected must indicate disconnection from the current best height, i.e. the new chain tip must be lower than the previous best 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);

// TODO: Replace with `take_if` once our MSRV is >= 1.80.
let mut should_broadcast_commitment = false;
if let Some((_, conf_height)) = self.alternative_funding_confirmed.as_ref() {
if *conf_height == height {
if *conf_height > new_height {
self.alternative_funding_confirmed.take();
if self.holder_tx_signed || self.funding_spend_seen {
// Cancel any previous claims that are no longer valid as they stemmed from a
Expand All@@ -5626,8 +5622,8 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {

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
self.onchain_tx_handler.blocks_disconnected(
new_height, &broadcaster, conf_target, &self.destination_script, &bounded_fee_estimator, logger
);

// Only attempt to broadcast the new commitment after the `block_disconnected` call above so that
Expand All@@ -5636,7 +5632,7 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
self.queue_latest_holder_commitment_txn_for_broadcast(&broadcaster, &bounded_fee_estimator, logger);
}

self.best_block = BestBlock::new(header.prev_blockhash, height - 1);
self.best_block = fork_point;
}

#[rustfmt::skip]
Expand DownExpand Up@@ -6110,8 +6106,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, fork_point: BestBlock) {
self.0.blocks_disconnected(fork_point, &*self.1, &*self.2, &self.3);
}
}

Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Drop the need for fork headers when calling Listen's disconnect by TheBlueMatt · Pull Request #3876 · lightningdevkit/rust-lightning · GitHub
Skip to content
Merged
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
5 changes: 3 additions & 2 deletions fuzz/src/full_stack.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -343,8 +343,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);
Expand Down
30 changes: 11 additions & 19 deletions lightning-block-sync/src/init.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@ use bitcoin::hash_types::BlockHash;
use bitcoin::network::Network;

use lightning::chain;
use lightning::chain::BestBlock;

use std::ops::Deref;

Expand DownExpand Up@@ -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, fork_point: BestBlock) {
self.0.blocks_disconnected(fork_point)
}
}

Expand All@@ -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, _fork_point: BestBlock) {
unreachable!()
}
}
Expand DownExpand Up@@ -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(1))
.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(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_blocks_disconnected(*fork_chain_3.at_height(3))
.expect_block_connected(*main_chain.at_height(4));

let listeners = vec![
Expand All@@ -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(1))
.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(1))
.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(1))
.expect_block_connected(*main_chain.at_height(2))
.expect_block_connected(*main_chain.at_height(3))
.expect_block_connected(*main_chain.at_height(4));
Expand All@@ -380,7 +372,7 @@ mod tests {
let old_tip = fork_chain.tip();

let listener = MockChainListener::new()
.expect_block_disconnected(*old_tip)
.expect_blocks_disconnected(*fork_chain.at_height(1))
.expect_block_connected(*new_tip);

let listeners = vec![(old_tip.block_hash, &listener as &dyn chain::Listen)];
Expand Down
20 changes: 11 additions & 9 deletions lightning-block-sync/src/lib.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
Expand DownExpand Up@@ -398,12 +398,15 @@ where
}

/// Notifies the chain listeners of disconnected blocks.
fn disconnect_blocks(&mut self, mut disconnected_blocks: Vec<ValidatedBlockHeader>) {
for header in disconnected_blocks.drain(..) {
fn disconnect_blocks(&mut self, disconnected_blocks: Vec<ValidatedBlockHeader>) {
for header in disconnected_blocks.iter() {
if let Some(cached_header) = self.header_cache.block_disconnected(&header.block_hash) {

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.

Should we change the Cache API accordingly?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

We could, but it seems somewhat disjoint from this PR, given there's further work to do in lightning-block-sync anyway, so I kinda wanted to keep it as small as can be.

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.

Mhh, might be good to document somewhere what steps you deem left before you'd consider the transition to this new approach complete. I guess all of them should land before the next release then, to not end up with a half-migrated codebase in the release?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

I mean the API/stuff in lightning in in a perfectly fine state after this PR, and lightning-block-sync just deserves a bit of efficiency in dropping a vec and improving the cache afterwards. This PR obviously doesn't accomplish the goals that #3600 set out to, but is just a step towards it (the "complicated" step that requires making sure all the lightning-internal stuff isn't broken by changing the API).

AFAIU, to get to the point that bitcoind (and all) syncs support swapping to a new chain during a reorg and don't fetch a pile of redundant blocks we need to:

  • make BestBlock contain a list of 6(ish?) block hashes, not one
  • use that list in lightning-block-sync (and eventually lightning-transaction-sync) to do the sync without leaning on the cache
  • expand the runtime caching to cache partial chains across clients in the init sync logic
  • (eventually) ensure lightning-block-sync doesn't have any spare vecs or whatever lying around.

@tnulltnullJul 3, 2025

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.

I still think it would make sense to open an issue for this larger refactoring you're planning, as there are connected aspects to discuss that don't really belong in this PR.

make BestBlock contain a list of 6(ish?) block hashes, not one

For example, how do we imagine this to work exactly, especially for the Confirm/lightning-transaction-sync case?

For Esplora, we currently retrieve the tip hash, retrieve the header and the block status (including the hight), before we call best_block_updated, which is already 3 costly RTTs. If we now would require to track the 6 most-recent hashes, we'd need to make at least 5 more subsequent calls (bringing this it at least 8 RTTs per sync round) to retrieve the hashes at [(height-5)..(height-1)]. But, given that these are individual calls, there is no way to tell if any reorg happened during some of these calls, so they are inherently race-y.

For Electrum, the results would be very similar for the polling client in its current form. Note we eventually want to switch the client to a streaming version making use of Electum's subscription model though, and requiring 'last 6 blocks' would probably require us to resort to costly polling again.

If we'd otherwise extend the client to start tracking more of the chain state (i.e., actually tracking a local chain 6-suffix) across rounds this would complicate the client logic quite a bit and make it even more susceptible to race conditions.

TLDR: I still maintain all of this would be/is resulting in a major refactor across our chain syncing crates and logic, and it would be great to discuss what we imagine to be involved and the corresponding trade-offs before we just jump into it heads first.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

We can definitely make it optional (and only used for full-node-sync) if we think its a major issue. Of course the issue becomes that you cannot (even today) safely transition from an esplora/electrum-synced node to a full-chain-sync one, but addressing that really requires a substantial rework of how we think about chain sync, so maybe its not worth worrying about that.

Of course, storing the last 6 blocks is pretty trivial for a full-node-sync client, and the point is really just to expose the thing that the sync driver gave us, so its not like we'd use it in Listen/Confirm implementations.

My understanding is that this would address the restart-with-different-node case that #3600 wanted to address, even if its optional and just used by the full-node-syncing logic?

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.

but addressing that really requires a substantial rework of how we think about chain sync, so maybe its not worth worrying about that.

Hmm, given that we even recently had users inquire whether they could switch chain sources at runtime, I do expect that we will need to tackle this eventually, as there will be users wanting to switch chain sources, and it's very unintuitive to either prohibit that, have it one-way, or even document the risks that might come from it.

Of course, storing the last 6 blocks is pretty trivial for a full-node-sync client, and the point is really just to expose the thing that the sync driver gave us, so its not like we'd use it in Listen/Confirm implementations.

So we would only do it when it's convenient, which means we could switch bitcoind nodes, but still not between chain sources?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Hmm, given that we even recently had users inquire whether they could switch chain sources at runtime, I do expect that we will need to tackle this eventually, as there will be users wanting to switch chain sources, and it's very unintuitive to either prohibit that, have it one-way, or even document the risks that might come from it.

Sure, I agree, hence why I suggested moving to storing several recent blocks rather than only one. However, as you note it might not be super practical, so we're kinda stuck.

I think, basically, if we did that plus required transactions_confirmed calls come before best_block_updated calls (which may we're doing anyway, cc #3867 (comment)) then moving from one chain source to another is totally fine, even at runtime (as long as you don't miss transactions).

So we would only do it when it's convenient, which means we could switch bitcoind nodes, but still not between chain sources?

I mean I'm not sure what else we can do? If its too impractical to do it on some chain sources we can't really make it required, and if its not available switching chain sources isn't going to be 100% reliable.

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() {

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.

If we're now happy to use the fork point, why are we keeping a Vec of all disconnected blocks around at all? Shouldn't we now be able to convert this to the new behavior entirely? Seems ChainDifference::common_ancestor would already provide what we need, no?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Its needed by the cache, which I didn't want to change in this PR - #3876 (comment)

let fork_point = BestBlock::new(block.header.prev_blockhash, block.height - 1);
self.chain_listener.blocks_disconnected(fork_point);
}
}

Expand DownExpand Up@@ -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(*fork_chain.at_height(1))
.expect_block_connected(*new_tip);
let mut notifier =
ChainNotifier { header_cache: &mut main_chain.header_cache(0..=2), chain_listener };
Expand All@@ -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(1))
.expect_block_connected(*new_tip);
let mut notifier =
ChainNotifier { header_cache: &mut main_chain.header_cache(0..=3), chain_listener };
Expand All@@ -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(*fork_chain.at_height(1))
.expect_block_connected(*fork_chain.at_height(2))
.expect_block_connected(*new_tip);
let mut notifier =
Expand Down
20 changes: 12 additions & 8 deletions lightning-block-sync/src/test_utils.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
Expand DownExpand Up@@ -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, _fork_point: BestBlock) {}
}

pub struct MockChainListener {
Expand DownExpand Up@@ -231,8 +232,8 @@ impl MockChainListener {
self
}

pub fn expect_block_disconnected(self, block: BlockHeaderData) -> Self {
self.expected_blocks_disconnected.borrow_mut().push_back(block);
pub fn expect_blocks_disconnected(self, fork_point: BlockHeaderData) -> Self {
self.expected_blocks_disconnected.borrow_mut().push_back(fork_point);
self
}
}
Expand DownExpand Up@@ -264,14 +265,17 @@ impl chain::Listen for MockChainListener {
}
}

fn block_disconnected(&self, header: &Header, height: u32) {
fn blocks_disconnected(&self, fork_point: BestBlock) {
match self.expected_blocks_disconnected.borrow_mut().pop_front() {
None => {
panic!("Unexpected block disconnected: {:?}", header.block_hash());
panic!(
"Unexpected block(s) disconnected {} at height {}",
fork_point.block_hash, fork_point.height,
);
},
Some(expected_block) => {
assert_eq!(header.block_hash(), expected_block.header.block_hash());
assert_eq!(height, expected_block.height);
Some(expected) => {
assert_eq!(fork_point.block_hash, expected.header.block_hash());
assert_eq!(fork_point.height, expected.height);
},
}
}
Expand Down
11 changes: 4 additions & 7 deletions lightning-liquidity/src/manager.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -767,14 +767,11 @@ 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, fork_point: 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");
Comment thread
TheBlueMatt marked this conversation as resolved.
*best_block = BestBlock::new(header.prev_blockhash, new_height)
assert!(best_block.height > fork_point.height,
"Blocks disconnected must indicate disconnection from the current best height, i.e. the new chain tip must be lower than the previous best height");
*best_block = fork_point;
}

// TODO: Call block_disconnected on all sub-modules that require it, e.g., LSPS1MessageHandler.
Expand Down
15 changes: 7 additions & 8 deletions lightning/src/chain/chainmonitor.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,7 +35,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;
#[cfg(peer_storage)]
Expand DownExpand Up@@ -1008,18 +1008,17 @@ where
self.event_notifier.notify();
}

fn block_disconnected(&self, header: &Header, height: u32) {
fn blocks_disconnected(&self, fork_point: 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 {}",
fork_point.height,
fork_point.block_hash,
);
for monitor_state in monitor_states.values() {
monitor_state.monitor.block_disconnected(
header,
height,
monitor_state.monitor.blocks_disconnected(
fork_point,
&*self.broadcaster,
&*self.fee_estimator,
&self.logger,
Expand Down
48 changes: 22 additions & 26 deletions lightning/src/chain/channelmonitor.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -2297,23 +2297,16 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitor<Signer> {

/// Determines if the disconnected block contained any transactions of interest and updates
/// appropriately.
#[rustfmt::skip]
pub fn block_disconnected<B: Deref, F: Deref, L: Deref>(
&self,
header: &Header,
height: u32,
broadcaster: B,
fee_estimator: F,
logger: &L,
pub fn blocks_disconnected<B: Deref, F: Deref, L: Deref>(
&self, fork_point: BestBlock, broadcaster: B, fee_estimator: F, logger: &L,
) where
B::Target: BroadcasterInterface,
F::Target: FeeEstimator,
L::Target: Logger,
{
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(fork_point, broadcaster, fee_estimator, &logger)
}

/// Processes transactions confirmed in a block with the given header and height, returning new
Expand DownExpand Up@@ -2347,10 +2340,10 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitor<Signer> {

/// 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<B: Deref, F: Deref, L: Deref>(
&self,
Expand DownExpand Up@@ -5113,8 +5106,8 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
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() }
Expand DownExpand Up@@ -5441,12 +5434,12 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
!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 before 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 before it.");
matured_htlcs.push(source.clone());
}

Expand DownExpand Up@@ -5594,23 +5587,26 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
}

#[rustfmt::skip]
fn block_disconnected<B: Deref, F: Deref, L: Deref>(
&mut self, header: &Header, height: u32, broadcaster: B, fee_estimator: F, logger: &WithChannelMonitor<L>
fn blocks_disconnected<B: Deref, F: Deref, L: Deref>(
&mut self, fork_point: BestBlock, broadcaster: B, fee_estimator: F, logger: &WithChannelMonitor<L>
) where B::Target: BroadcasterInterface,
F::Target: FeeEstimator,
L::Target: Logger,
{
log_trace!(logger, "Block {} at height {} disconnected", header.block_hash(), height);
let new_height = fork_point.height;
log_trace!(logger, "Block(s) disconnected to height {}", new_height);
assert!(self.best_block.height > fork_point.height,
"Blocks disconnected must indicate disconnection from the current best height, i.e. the new chain tip must be lower than the previous best 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);

// TODO: Replace with `take_if` once our MSRV is >= 1.80.
let mut should_broadcast_commitment = false;
if let Some((_, conf_height)) = self.alternative_funding_confirmed.as_ref() {
if *conf_height == height {
if *conf_height > new_height {
self.alternative_funding_confirmed.take();
if self.holder_tx_signed || self.funding_spend_seen {
// Cancel any previous claims that are no longer valid as they stemmed from a
Expand All@@ -5626,8 +5622,8 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {

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
self.onchain_tx_handler.blocks_disconnected(
new_height, &broadcaster, conf_target, &self.destination_script, &bounded_fee_estimator, logger
);

// Only attempt to broadcast the new commitment after the `block_disconnected` call above so that
Expand All@@ -5636,7 +5632,7 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
self.queue_latest_holder_commitment_txn_for_broadcast(&broadcaster, &bounded_fee_estimator, logger);
}

self.best_block = BestBlock::new(header.prev_blockhash, height - 1);
self.best_block = fork_point;
}

#[rustfmt::skip]
Expand DownExpand Up@@ -6110,8 +6106,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, fork_point: BestBlock) {
self.0.blocks_disconnected(fork_point, &*self.1, &*self.2, &self.3);
}
}

Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Drop the need for fork headers when calling Listen's disconnect by TheBlueMatt · Pull Request #3876 · lightningdevkit/rust-lightning · GitHub
Skip to content
Merged
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
5 changes: 3 additions & 2 deletions fuzz/src/full_stack.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -343,8 +343,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);
Expand Down
30 changes: 11 additions & 19 deletions lightning-block-sync/src/init.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@ use bitcoin::hash_types::BlockHash;
use bitcoin::network::Network;

use lightning::chain;
use lightning::chain::BestBlock;

use std::ops::Deref;

Expand DownExpand Up@@ -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, fork_point: BestBlock) {
self.0.blocks_disconnected(fork_point)
}
}

Expand All@@ -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, _fork_point: BestBlock) {
unreachable!()
}
}
Expand DownExpand Up@@ -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(1))
.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(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_blocks_disconnected(*fork_chain_3.at_height(3))
.expect_block_connected(*main_chain.at_height(4));

let listeners = vec![
Expand All@@ -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(1))
.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(1))
.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(1))
.expect_block_connected(*main_chain.at_height(2))
.expect_block_connected(*main_chain.at_height(3))
.expect_block_connected(*main_chain.at_height(4));
Expand All@@ -380,7 +372,7 @@ mod tests {
let old_tip = fork_chain.tip();

let listener = MockChainListener::new()
.expect_block_disconnected(*old_tip)
.expect_blocks_disconnected(*fork_chain.at_height(1))
.expect_block_connected(*new_tip);

let listeners = vec![(old_tip.block_hash, &listener as &dyn chain::Listen)];
Expand Down
20 changes: 11 additions & 9 deletions lightning-block-sync/src/lib.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
Expand DownExpand Up@@ -398,12 +398,15 @@ where
}

/// Notifies the chain listeners of disconnected blocks.
fn disconnect_blocks(&mut self, mut disconnected_blocks: Vec<ValidatedBlockHeader>) {
for header in disconnected_blocks.drain(..) {
fn disconnect_blocks(&mut self, disconnected_blocks: Vec<ValidatedBlockHeader>) {
for header in disconnected_blocks.iter() {
if let Some(cached_header) = self.header_cache.block_disconnected(&header.block_hash) {

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.

Should we change the Cache API accordingly?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

We could, but it seems somewhat disjoint from this PR, given there's further work to do in lightning-block-sync anyway, so I kinda wanted to keep it as small as can be.

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.

Mhh, might be good to document somewhere what steps you deem left before you'd consider the transition to this new approach complete. I guess all of them should land before the next release then, to not end up with a half-migrated codebase in the release?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

I mean the API/stuff in lightning in in a perfectly fine state after this PR, and lightning-block-sync just deserves a bit of efficiency in dropping a vec and improving the cache afterwards. This PR obviously doesn't accomplish the goals that #3600 set out to, but is just a step towards it (the "complicated" step that requires making sure all the lightning-internal stuff isn't broken by changing the API).

AFAIU, to get to the point that bitcoind (and all) syncs support swapping to a new chain during a reorg and don't fetch a pile of redundant blocks we need to:

  • make BestBlock contain a list of 6(ish?) block hashes, not one
  • use that list in lightning-block-sync (and eventually lightning-transaction-sync) to do the sync without leaning on the cache
  • expand the runtime caching to cache partial chains across clients in the init sync logic
  • (eventually) ensure lightning-block-sync doesn't have any spare vecs or whatever lying around.

@tnulltnullJul 3, 2025

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.

I still think it would make sense to open an issue for this larger refactoring you're planning, as there are connected aspects to discuss that don't really belong in this PR.

make BestBlock contain a list of 6(ish?) block hashes, not one

For example, how do we imagine this to work exactly, especially for the Confirm/lightning-transaction-sync case?

For Esplora, we currently retrieve the tip hash, retrieve the header and the block status (including the hight), before we call best_block_updated, which is already 3 costly RTTs. If we now would require to track the 6 most-recent hashes, we'd need to make at least 5 more subsequent calls (bringing this it at least 8 RTTs per sync round) to retrieve the hashes at [(height-5)..(height-1)]. But, given that these are individual calls, there is no way to tell if any reorg happened during some of these calls, so they are inherently race-y.

For Electrum, the results would be very similar for the polling client in its current form. Note we eventually want to switch the client to a streaming version making use of Electum's subscription model though, and requiring 'last 6 blocks' would probably require us to resort to costly polling again.

If we'd otherwise extend the client to start tracking more of the chain state (i.e., actually tracking a local chain 6-suffix) across rounds this would complicate the client logic quite a bit and make it even more susceptible to race conditions.

TLDR: I still maintain all of this would be/is resulting in a major refactor across our chain syncing crates and logic, and it would be great to discuss what we imagine to be involved and the corresponding trade-offs before we just jump into it heads first.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

We can definitely make it optional (and only used for full-node-sync) if we think its a major issue. Of course the issue becomes that you cannot (even today) safely transition from an esplora/electrum-synced node to a full-chain-sync one, but addressing that really requires a substantial rework of how we think about chain sync, so maybe its not worth worrying about that.

Of course, storing the last 6 blocks is pretty trivial for a full-node-sync client, and the point is really just to expose the thing that the sync driver gave us, so its not like we'd use it in Listen/Confirm implementations.

My understanding is that this would address the restart-with-different-node case that #3600 wanted to address, even if its optional and just used by the full-node-syncing logic?

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.

but addressing that really requires a substantial rework of how we think about chain sync, so maybe its not worth worrying about that.

Hmm, given that we even recently had users inquire whether they could switch chain sources at runtime, I do expect that we will need to tackle this eventually, as there will be users wanting to switch chain sources, and it's very unintuitive to either prohibit that, have it one-way, or even document the risks that might come from it.

Of course, storing the last 6 blocks is pretty trivial for a full-node-sync client, and the point is really just to expose the thing that the sync driver gave us, so its not like we'd use it in Listen/Confirm implementations.

So we would only do it when it's convenient, which means we could switch bitcoind nodes, but still not between chain sources?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Hmm, given that we even recently had users inquire whether they could switch chain sources at runtime, I do expect that we will need to tackle this eventually, as there will be users wanting to switch chain sources, and it's very unintuitive to either prohibit that, have it one-way, or even document the risks that might come from it.

Sure, I agree, hence why I suggested moving to storing several recent blocks rather than only one. However, as you note it might not be super practical, so we're kinda stuck.

I think, basically, if we did that plus required transactions_confirmed calls come before best_block_updated calls (which may we're doing anyway, cc #3867 (comment)) then moving from one chain source to another is totally fine, even at runtime (as long as you don't miss transactions).

So we would only do it when it's convenient, which means we could switch bitcoind nodes, but still not between chain sources?

I mean I'm not sure what else we can do? If its too impractical to do it on some chain sources we can't really make it required, and if its not available switching chain sources isn't going to be 100% reliable.

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() {

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.

If we're now happy to use the fork point, why are we keeping a Vec of all disconnected blocks around at all? Shouldn't we now be able to convert this to the new behavior entirely? Seems ChainDifference::common_ancestor would already provide what we need, no?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Its needed by the cache, which I didn't want to change in this PR - #3876 (comment)

let fork_point = BestBlock::new(block.header.prev_blockhash, block.height - 1);
self.chain_listener.blocks_disconnected(fork_point);
}
}

Expand DownExpand Up@@ -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(*fork_chain.at_height(1))
.expect_block_connected(*new_tip);
let mut notifier =
ChainNotifier { header_cache: &mut main_chain.header_cache(0..=2), chain_listener };
Expand All@@ -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(1))
.expect_block_connected(*new_tip);
let mut notifier =
ChainNotifier { header_cache: &mut main_chain.header_cache(0..=3), chain_listener };
Expand All@@ -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(*fork_chain.at_height(1))
.expect_block_connected(*fork_chain.at_height(2))
.expect_block_connected(*new_tip);
let mut notifier =
Expand Down
20 changes: 12 additions & 8 deletions lightning-block-sync/src/test_utils.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
Expand DownExpand Up@@ -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, _fork_point: BestBlock) {}
}

pub struct MockChainListener {
Expand DownExpand Up@@ -231,8 +232,8 @@ impl MockChainListener {
self
}

pub fn expect_block_disconnected(self, block: BlockHeaderData) -> Self {
self.expected_blocks_disconnected.borrow_mut().push_back(block);
pub fn expect_blocks_disconnected(self, fork_point: BlockHeaderData) -> Self {
self.expected_blocks_disconnected.borrow_mut().push_back(fork_point);
self
}
}
Expand DownExpand Up@@ -264,14 +265,17 @@ impl chain::Listen for MockChainListener {
}
}

fn block_disconnected(&self, header: &Header, height: u32) {
fn blocks_disconnected(&self, fork_point: BestBlock) {
match self.expected_blocks_disconnected.borrow_mut().pop_front() {
None => {
panic!("Unexpected block disconnected: {:?}", header.block_hash());
panic!(
"Unexpected block(s) disconnected {} at height {}",
fork_point.block_hash, fork_point.height,
);
},
Some(expected_block) => {
assert_eq!(header.block_hash(), expected_block.header.block_hash());
assert_eq!(height, expected_block.height);
Some(expected) => {
assert_eq!(fork_point.block_hash, expected.header.block_hash());
assert_eq!(fork_point.height, expected.height);
},
}
}
Expand Down
11 changes: 4 additions & 7 deletions lightning-liquidity/src/manager.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -767,14 +767,11 @@ 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, fork_point: 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");
Comment thread
TheBlueMatt marked this conversation as resolved.
*best_block = BestBlock::new(header.prev_blockhash, new_height)
assert!(best_block.height > fork_point.height,
"Blocks disconnected must indicate disconnection from the current best height, i.e. the new chain tip must be lower than the previous best height");
*best_block = fork_point;
}

// TODO: Call block_disconnected on all sub-modules that require it, e.g., LSPS1MessageHandler.
Expand Down
15 changes: 7 additions & 8 deletions lightning/src/chain/chainmonitor.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,7 +35,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;
#[cfg(peer_storage)]
Expand DownExpand Up@@ -1008,18 +1008,17 @@ where
self.event_notifier.notify();
}

fn block_disconnected(&self, header: &Header, height: u32) {
fn blocks_disconnected(&self, fork_point: 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 {}",
fork_point.height,
fork_point.block_hash,
);
for monitor_state in monitor_states.values() {
monitor_state.monitor.block_disconnected(
header,
height,
monitor_state.monitor.blocks_disconnected(
fork_point,
&*self.broadcaster,
&*self.fee_estimator,
&self.logger,
Expand Down
48 changes: 22 additions & 26 deletions lightning/src/chain/channelmonitor.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -2297,23 +2297,16 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitor<Signer> {

/// Determines if the disconnected block contained any transactions of interest and updates
/// appropriately.
#[rustfmt::skip]
pub fn block_disconnected<B: Deref, F: Deref, L: Deref>(
&self,
header: &Header,
height: u32,
broadcaster: B,
fee_estimator: F,
logger: &L,
pub fn blocks_disconnected<B: Deref, F: Deref, L: Deref>(
&self, fork_point: BestBlock, broadcaster: B, fee_estimator: F, logger: &L,
) where
B::Target: BroadcasterInterface,
F::Target: FeeEstimator,
L::Target: Logger,
{
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(fork_point, broadcaster, fee_estimator, &logger)
}

/// Processes transactions confirmed in a block with the given header and height, returning new
Expand DownExpand Up@@ -2347,10 +2340,10 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitor<Signer> {

/// 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<B: Deref, F: Deref, L: Deref>(
&self,
Expand DownExpand Up@@ -5113,8 +5106,8 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
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() }
Expand DownExpand Up@@ -5441,12 +5434,12 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
!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 before 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 before it.");
matured_htlcs.push(source.clone());
}

Expand DownExpand Up@@ -5594,23 +5587,26 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
}

#[rustfmt::skip]
fn block_disconnected<B: Deref, F: Deref, L: Deref>(
&mut self, header: &Header, height: u32, broadcaster: B, fee_estimator: F, logger: &WithChannelMonitor<L>
fn blocks_disconnected<B: Deref, F: Deref, L: Deref>(
&mut self, fork_point: BestBlock, broadcaster: B, fee_estimator: F, logger: &WithChannelMonitor<L>
) where B::Target: BroadcasterInterface,
F::Target: FeeEstimator,
L::Target: Logger,
{
log_trace!(logger, "Block {} at height {} disconnected", header.block_hash(), height);
let new_height = fork_point.height;
log_trace!(logger, "Block(s) disconnected to height {}", new_height);
assert!(self.best_block.height > fork_point.height,
"Blocks disconnected must indicate disconnection from the current best height, i.e. the new chain tip must be lower than the previous best 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);

// TODO: Replace with `take_if` once our MSRV is >= 1.80.
let mut should_broadcast_commitment = false;
if let Some((_, conf_height)) = self.alternative_funding_confirmed.as_ref() {
if *conf_height == height {
if *conf_height > new_height {
self.alternative_funding_confirmed.take();
if self.holder_tx_signed || self.funding_spend_seen {
// Cancel any previous claims that are no longer valid as they stemmed from a
Expand All@@ -5626,8 +5622,8 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {

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
self.onchain_tx_handler.blocks_disconnected(
new_height, &broadcaster, conf_target, &self.destination_script, &bounded_fee_estimator, logger
);

// Only attempt to broadcast the new commitment after the `block_disconnected` call above so that
Expand All@@ -5636,7 +5632,7 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
self.queue_latest_holder_commitment_txn_for_broadcast(&broadcaster, &bounded_fee_estimator, logger);
}

self.best_block = BestBlock::new(header.prev_blockhash, height - 1);
self.best_block = fork_point;
}

#[rustfmt::skip]
Expand DownExpand Up@@ -6110,8 +6106,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, fork_point: BestBlock) {
self.0.blocks_disconnected(fork_point, &*self.1, &*self.2, &self.3);
}
}

Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Drop the need for fork headers when calling Listen's disconnect by TheBlueMatt · Pull Request #3876 · lightningdevkit/rust-lightning · GitHub
Skip to content
Merged
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
5 changes: 3 additions & 2 deletions fuzz/src/full_stack.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -343,8 +343,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);
Expand Down
30 changes: 11 additions & 19 deletions lightning-block-sync/src/init.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@ use bitcoin::hash_types::BlockHash;
use bitcoin::network::Network;

use lightning::chain;
use lightning::chain::BestBlock;

use std::ops::Deref;

Expand DownExpand Up@@ -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, fork_point: BestBlock) {
self.0.blocks_disconnected(fork_point)
}
}

Expand All@@ -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, _fork_point: BestBlock) {
unreachable!()
}
}
Expand DownExpand Up@@ -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(1))
.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(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_blocks_disconnected(*fork_chain_3.at_height(3))
.expect_block_connected(*main_chain.at_height(4));

let listeners = vec![
Expand All@@ -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(1))
.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(1))
.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(1))
.expect_block_connected(*main_chain.at_height(2))
.expect_block_connected(*main_chain.at_height(3))
.expect_block_connected(*main_chain.at_height(4));
Expand All@@ -380,7 +372,7 @@ mod tests {
let old_tip = fork_chain.tip();

let listener = MockChainListener::new()
.expect_block_disconnected(*old_tip)
.expect_blocks_disconnected(*fork_chain.at_height(1))
.expect_block_connected(*new_tip);

let listeners = vec![(old_tip.block_hash, &listener as &dyn chain::Listen)];
Expand Down
20 changes: 11 additions & 9 deletions lightning-block-sync/src/lib.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
Expand DownExpand Up@@ -398,12 +398,15 @@ where
}

/// Notifies the chain listeners of disconnected blocks.
fn disconnect_blocks(&mut self, mut disconnected_blocks: Vec<ValidatedBlockHeader>) {
for header in disconnected_blocks.drain(..) {
fn disconnect_blocks(&mut self, disconnected_blocks: Vec<ValidatedBlockHeader>) {
for header in disconnected_blocks.iter() {
if let Some(cached_header) = self.header_cache.block_disconnected(&header.block_hash) {

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.

Should we change the Cache API accordingly?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

We could, but it seems somewhat disjoint from this PR, given there's further work to do in lightning-block-sync anyway, so I kinda wanted to keep it as small as can be.

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.

Mhh, might be good to document somewhere what steps you deem left before you'd consider the transition to this new approach complete. I guess all of them should land before the next release then, to not end up with a half-migrated codebase in the release?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

I mean the API/stuff in lightning in in a perfectly fine state after this PR, and lightning-block-sync just deserves a bit of efficiency in dropping a vec and improving the cache afterwards. This PR obviously doesn't accomplish the goals that #3600 set out to, but is just a step towards it (the "complicated" step that requires making sure all the lightning-internal stuff isn't broken by changing the API).

AFAIU, to get to the point that bitcoind (and all) syncs support swapping to a new chain during a reorg and don't fetch a pile of redundant blocks we need to:

  • make BestBlock contain a list of 6(ish?) block hashes, not one
  • use that list in lightning-block-sync (and eventually lightning-transaction-sync) to do the sync without leaning on the cache
  • expand the runtime caching to cache partial chains across clients in the init sync logic
  • (eventually) ensure lightning-block-sync doesn't have any spare vecs or whatever lying around.

@tnulltnullJul 3, 2025

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.

I still think it would make sense to open an issue for this larger refactoring you're planning, as there are connected aspects to discuss that don't really belong in this PR.

make BestBlock contain a list of 6(ish?) block hashes, not one

For example, how do we imagine this to work exactly, especially for the Confirm/lightning-transaction-sync case?

For Esplora, we currently retrieve the tip hash, retrieve the header and the block status (including the hight), before we call best_block_updated, which is already 3 costly RTTs. If we now would require to track the 6 most-recent hashes, we'd need to make at least 5 more subsequent calls (bringing this it at least 8 RTTs per sync round) to retrieve the hashes at [(height-5)..(height-1)]. But, given that these are individual calls, there is no way to tell if any reorg happened during some of these calls, so they are inherently race-y.

For Electrum, the results would be very similar for the polling client in its current form. Note we eventually want to switch the client to a streaming version making use of Electum's subscription model though, and requiring 'last 6 blocks' would probably require us to resort to costly polling again.

If we'd otherwise extend the client to start tracking more of the chain state (i.e., actually tracking a local chain 6-suffix) across rounds this would complicate the client logic quite a bit and make it even more susceptible to race conditions.

TLDR: I still maintain all of this would be/is resulting in a major refactor across our chain syncing crates and logic, and it would be great to discuss what we imagine to be involved and the corresponding trade-offs before we just jump into it heads first.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

We can definitely make it optional (and only used for full-node-sync) if we think its a major issue. Of course the issue becomes that you cannot (even today) safely transition from an esplora/electrum-synced node to a full-chain-sync one, but addressing that really requires a substantial rework of how we think about chain sync, so maybe its not worth worrying about that.

Of course, storing the last 6 blocks is pretty trivial for a full-node-sync client, and the point is really just to expose the thing that the sync driver gave us, so its not like we'd use it in Listen/Confirm implementations.

My understanding is that this would address the restart-with-different-node case that #3600 wanted to address, even if its optional and just used by the full-node-syncing logic?

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.

but addressing that really requires a substantial rework of how we think about chain sync, so maybe its not worth worrying about that.

Hmm, given that we even recently had users inquire whether they could switch chain sources at runtime, I do expect that we will need to tackle this eventually, as there will be users wanting to switch chain sources, and it's very unintuitive to either prohibit that, have it one-way, or even document the risks that might come from it.

Of course, storing the last 6 blocks is pretty trivial for a full-node-sync client, and the point is really just to expose the thing that the sync driver gave us, so its not like we'd use it in Listen/Confirm implementations.

So we would only do it when it's convenient, which means we could switch bitcoind nodes, but still not between chain sources?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Hmm, given that we even recently had users inquire whether they could switch chain sources at runtime, I do expect that we will need to tackle this eventually, as there will be users wanting to switch chain sources, and it's very unintuitive to either prohibit that, have it one-way, or even document the risks that might come from it.

Sure, I agree, hence why I suggested moving to storing several recent blocks rather than only one. However, as you note it might not be super practical, so we're kinda stuck.

I think, basically, if we did that plus required transactions_confirmed calls come before best_block_updated calls (which may we're doing anyway, cc #3867 (comment)) then moving from one chain source to another is totally fine, even at runtime (as long as you don't miss transactions).

So we would only do it when it's convenient, which means we could switch bitcoind nodes, but still not between chain sources?

I mean I'm not sure what else we can do? If its too impractical to do it on some chain sources we can't really make it required, and if its not available switching chain sources isn't going to be 100% reliable.

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() {

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.

If we're now happy to use the fork point, why are we keeping a Vec of all disconnected blocks around at all? Shouldn't we now be able to convert this to the new behavior entirely? Seems ChainDifference::common_ancestor would already provide what we need, no?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Its needed by the cache, which I didn't want to change in this PR - #3876 (comment)

let fork_point = BestBlock::new(block.header.prev_blockhash, block.height - 1);
self.chain_listener.blocks_disconnected(fork_point);
}
}

Expand DownExpand Up@@ -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(*fork_chain.at_height(1))
.expect_block_connected(*new_tip);
let mut notifier =
ChainNotifier { header_cache: &mut main_chain.header_cache(0..=2), chain_listener };
Expand All@@ -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(1))
.expect_block_connected(*new_tip);
let mut notifier =
ChainNotifier { header_cache: &mut main_chain.header_cache(0..=3), chain_listener };
Expand All@@ -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(*fork_chain.at_height(1))
.expect_block_connected(*fork_chain.at_height(2))
.expect_block_connected(*new_tip);
let mut notifier =
Expand Down
20 changes: 12 additions & 8 deletions lightning-block-sync/src/test_utils.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
Expand DownExpand Up@@ -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, _fork_point: BestBlock) {}
}

pub struct MockChainListener {
Expand DownExpand Up@@ -231,8 +232,8 @@ impl MockChainListener {
self
}

pub fn expect_block_disconnected(self, block: BlockHeaderData) -> Self {
self.expected_blocks_disconnected.borrow_mut().push_back(block);
pub fn expect_blocks_disconnected(self, fork_point: BlockHeaderData) -> Self {
self.expected_blocks_disconnected.borrow_mut().push_back(fork_point);
self
}
}
Expand DownExpand Up@@ -264,14 +265,17 @@ impl chain::Listen for MockChainListener {
}
}

fn block_disconnected(&self, header: &Header, height: u32) {
fn blocks_disconnected(&self, fork_point: BestBlock) {
match self.expected_blocks_disconnected.borrow_mut().pop_front() {
None => {
panic!("Unexpected block disconnected: {:?}", header.block_hash());
panic!(
"Unexpected block(s) disconnected {} at height {}",
fork_point.block_hash, fork_point.height,
);
},
Some(expected_block) => {
assert_eq!(header.block_hash(), expected_block.header.block_hash());
assert_eq!(height, expected_block.height);
Some(expected) => {
assert_eq!(fork_point.block_hash, expected.header.block_hash());
assert_eq!(fork_point.height, expected.height);
},
}
}
Expand Down
11 changes: 4 additions & 7 deletions lightning-liquidity/src/manager.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -767,14 +767,11 @@ 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, fork_point: 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");
Comment thread
TheBlueMatt marked this conversation as resolved.
*best_block = BestBlock::new(header.prev_blockhash, new_height)
assert!(best_block.height > fork_point.height,
"Blocks disconnected must indicate disconnection from the current best height, i.e. the new chain tip must be lower than the previous best height");
*best_block = fork_point;
}

// TODO: Call block_disconnected on all sub-modules that require it, e.g., LSPS1MessageHandler.
Expand Down
15 changes: 7 additions & 8 deletions lightning/src/chain/chainmonitor.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,7 +35,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;
#[cfg(peer_storage)]
Expand DownExpand Up@@ -1008,18 +1008,17 @@ where
self.event_notifier.notify();
}

fn block_disconnected(&self, header: &Header, height: u32) {
fn blocks_disconnected(&self, fork_point: 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 {}",
fork_point.height,
fork_point.block_hash,
);
for monitor_state in monitor_states.values() {
monitor_state.monitor.block_disconnected(
header,
height,
monitor_state.monitor.blocks_disconnected(
fork_point,
&*self.broadcaster,
&*self.fee_estimator,
&self.logger,
Expand Down
48 changes: 22 additions & 26 deletions lightning/src/chain/channelmonitor.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -2297,23 +2297,16 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitor<Signer> {

/// Determines if the disconnected block contained any transactions of interest and updates
/// appropriately.
#[rustfmt::skip]
pub fn block_disconnected<B: Deref, F: Deref, L: Deref>(
&self,
header: &Header,
height: u32,
broadcaster: B,
fee_estimator: F,
logger: &L,
pub fn blocks_disconnected<B: Deref, F: Deref, L: Deref>(
&self, fork_point: BestBlock, broadcaster: B, fee_estimator: F, logger: &L,
) where
B::Target: BroadcasterInterface,
F::Target: FeeEstimator,
L::Target: Logger,
{
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(fork_point, broadcaster, fee_estimator, &logger)
}

/// Processes transactions confirmed in a block with the given header and height, returning new
Expand DownExpand Up@@ -2347,10 +2340,10 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitor<Signer> {

/// 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<B: Deref, F: Deref, L: Deref>(
&self,
Expand DownExpand Up@@ -5113,8 +5106,8 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
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() }
Expand DownExpand Up@@ -5441,12 +5434,12 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
!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 before 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 before it.");
matured_htlcs.push(source.clone());
}

Expand DownExpand Up@@ -5594,23 +5587,26 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
}

#[rustfmt::skip]
fn block_disconnected<B: Deref, F: Deref, L: Deref>(
&mut self, header: &Header, height: u32, broadcaster: B, fee_estimator: F, logger: &WithChannelMonitor<L>
fn blocks_disconnected<B: Deref, F: Deref, L: Deref>(
&mut self, fork_point: BestBlock, broadcaster: B, fee_estimator: F, logger: &WithChannelMonitor<L>
) where B::Target: BroadcasterInterface,
F::Target: FeeEstimator,
L::Target: Logger,
{
log_trace!(logger, "Block {} at height {} disconnected", header.block_hash(), height);
let new_height = fork_point.height;
log_trace!(logger, "Block(s) disconnected to height {}", new_height);
assert!(self.best_block.height > fork_point.height,
"Blocks disconnected must indicate disconnection from the current best height, i.e. the new chain tip must be lower than the previous best 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);

// TODO: Replace with `take_if` once our MSRV is >= 1.80.
let mut should_broadcast_commitment = false;
if let Some((_, conf_height)) = self.alternative_funding_confirmed.as_ref() {
if *conf_height == height {
if *conf_height > new_height {
self.alternative_funding_confirmed.take();
if self.holder_tx_signed || self.funding_spend_seen {
// Cancel any previous claims that are no longer valid as they stemmed from a
Expand All@@ -5626,8 +5622,8 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {

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
self.onchain_tx_handler.blocks_disconnected(
new_height, &broadcaster, conf_target, &self.destination_script, &bounded_fee_estimator, logger
);

// Only attempt to broadcast the new commitment after the `block_disconnected` call above so that
Expand All@@ -5636,7 +5632,7 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
self.queue_latest_holder_commitment_txn_for_broadcast(&broadcaster, &bounded_fee_estimator, logger);
}

self.best_block = BestBlock::new(header.prev_blockhash, height - 1);
self.best_block = fork_point;
}

#[rustfmt::skip]
Expand DownExpand Up@@ -6110,8 +6106,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, fork_point: BestBlock) {
self.0.blocks_disconnected(fork_point, &*self.1, &*self.2, &self.3);
}
}

Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Drop the need for fork headers when calling Listen's disconnect by TheBlueMatt · Pull Request #3876 · lightningdevkit/rust-lightning · GitHub
Skip to content
Merged
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
5 changes: 3 additions & 2 deletions fuzz/src/full_stack.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -343,8 +343,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);
Expand Down
30 changes: 11 additions & 19 deletions lightning-block-sync/src/init.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@ use bitcoin::hash_types::BlockHash;
use bitcoin::network::Network;

use lightning::chain;
use lightning::chain::BestBlock;

use std::ops::Deref;

Expand DownExpand Up@@ -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, fork_point: BestBlock) {
self.0.blocks_disconnected(fork_point)
}
}

Expand All@@ -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, _fork_point: BestBlock) {
unreachable!()
}
}
Expand DownExpand Up@@ -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(1))
.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(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_blocks_disconnected(*fork_chain_3.at_height(3))
.expect_block_connected(*main_chain.at_height(4));

let listeners = vec![
Expand All@@ -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(1))
.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(1))
.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(1))
.expect_block_connected(*main_chain.at_height(2))
.expect_block_connected(*main_chain.at_height(3))
.expect_block_connected(*main_chain.at_height(4));
Expand All@@ -380,7 +372,7 @@ mod tests {
let old_tip = fork_chain.tip();

let listener = MockChainListener::new()
.expect_block_disconnected(*old_tip)
.expect_blocks_disconnected(*fork_chain.at_height(1))
.expect_block_connected(*new_tip);

let listeners = vec![(old_tip.block_hash, &listener as &dyn chain::Listen)];
Expand Down
20 changes: 11 additions & 9 deletions lightning-block-sync/src/lib.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
Expand DownExpand Up@@ -398,12 +398,15 @@ where
}

/// Notifies the chain listeners of disconnected blocks.
fn disconnect_blocks(&mut self, mut disconnected_blocks: Vec<ValidatedBlockHeader>) {
for header in disconnected_blocks.drain(..) {
fn disconnect_blocks(&mut self, disconnected_blocks: Vec<ValidatedBlockHeader>) {
for header in disconnected_blocks.iter() {
if let Some(cached_header) = self.header_cache.block_disconnected(&header.block_hash) {

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.

Should we change the Cache API accordingly?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

We could, but it seems somewhat disjoint from this PR, given there's further work to do in lightning-block-sync anyway, so I kinda wanted to keep it as small as can be.

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.

Mhh, might be good to document somewhere what steps you deem left before you'd consider the transition to this new approach complete. I guess all of them should land before the next release then, to not end up with a half-migrated codebase in the release?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

I mean the API/stuff in lightning in in a perfectly fine state after this PR, and lightning-block-sync just deserves a bit of efficiency in dropping a vec and improving the cache afterwards. This PR obviously doesn't accomplish the goals that #3600 set out to, but is just a step towards it (the "complicated" step that requires making sure all the lightning-internal stuff isn't broken by changing the API).

AFAIU, to get to the point that bitcoind (and all) syncs support swapping to a new chain during a reorg and don't fetch a pile of redundant blocks we need to:

  • make BestBlock contain a list of 6(ish?) block hashes, not one
  • use that list in lightning-block-sync (and eventually lightning-transaction-sync) to do the sync without leaning on the cache
  • expand the runtime caching to cache partial chains across clients in the init sync logic
  • (eventually) ensure lightning-block-sync doesn't have any spare vecs or whatever lying around.

@tnulltnullJul 3, 2025

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.

I still think it would make sense to open an issue for this larger refactoring you're planning, as there are connected aspects to discuss that don't really belong in this PR.

make BestBlock contain a list of 6(ish?) block hashes, not one

For example, how do we imagine this to work exactly, especially for the Confirm/lightning-transaction-sync case?

For Esplora, we currently retrieve the tip hash, retrieve the header and the block status (including the hight), before we call best_block_updated, which is already 3 costly RTTs. If we now would require to track the 6 most-recent hashes, we'd need to make at least 5 more subsequent calls (bringing this it at least 8 RTTs per sync round) to retrieve the hashes at [(height-5)..(height-1)]. But, given that these are individual calls, there is no way to tell if any reorg happened during some of these calls, so they are inherently race-y.

For Electrum, the results would be very similar for the polling client in its current form. Note we eventually want to switch the client to a streaming version making use of Electum's subscription model though, and requiring 'last 6 blocks' would probably require us to resort to costly polling again.

If we'd otherwise extend the client to start tracking more of the chain state (i.e., actually tracking a local chain 6-suffix) across rounds this would complicate the client logic quite a bit and make it even more susceptible to race conditions.

TLDR: I still maintain all of this would be/is resulting in a major refactor across our chain syncing crates and logic, and it would be great to discuss what we imagine to be involved and the corresponding trade-offs before we just jump into it heads first.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

We can definitely make it optional (and only used for full-node-sync) if we think its a major issue. Of course the issue becomes that you cannot (even today) safely transition from an esplora/electrum-synced node to a full-chain-sync one, but addressing that really requires a substantial rework of how we think about chain sync, so maybe its not worth worrying about that.

Of course, storing the last 6 blocks is pretty trivial for a full-node-sync client, and the point is really just to expose the thing that the sync driver gave us, so its not like we'd use it in Listen/Confirm implementations.

My understanding is that this would address the restart-with-different-node case that #3600 wanted to address, even if its optional and just used by the full-node-syncing logic?

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.

but addressing that really requires a substantial rework of how we think about chain sync, so maybe its not worth worrying about that.

Hmm, given that we even recently had users inquire whether they could switch chain sources at runtime, I do expect that we will need to tackle this eventually, as there will be users wanting to switch chain sources, and it's very unintuitive to either prohibit that, have it one-way, or even document the risks that might come from it.

Of course, storing the last 6 blocks is pretty trivial for a full-node-sync client, and the point is really just to expose the thing that the sync driver gave us, so its not like we'd use it in Listen/Confirm implementations.

So we would only do it when it's convenient, which means we could switch bitcoind nodes, but still not between chain sources?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Hmm, given that we even recently had users inquire whether they could switch chain sources at runtime, I do expect that we will need to tackle this eventually, as there will be users wanting to switch chain sources, and it's very unintuitive to either prohibit that, have it one-way, or even document the risks that might come from it.

Sure, I agree, hence why I suggested moving to storing several recent blocks rather than only one. However, as you note it might not be super practical, so we're kinda stuck.

I think, basically, if we did that plus required transactions_confirmed calls come before best_block_updated calls (which may we're doing anyway, cc #3867 (comment)) then moving from one chain source to another is totally fine, even at runtime (as long as you don't miss transactions).

So we would only do it when it's convenient, which means we could switch bitcoind nodes, but still not between chain sources?

I mean I'm not sure what else we can do? If its too impractical to do it on some chain sources we can't really make it required, and if its not available switching chain sources isn't going to be 100% reliable.

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() {

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.

If we're now happy to use the fork point, why are we keeping a Vec of all disconnected blocks around at all? Shouldn't we now be able to convert this to the new behavior entirely? Seems ChainDifference::common_ancestor would already provide what we need, no?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Its needed by the cache, which I didn't want to change in this PR - #3876 (comment)

let fork_point = BestBlock::new(block.header.prev_blockhash, block.height - 1);
self.chain_listener.blocks_disconnected(fork_point);
}
}

Expand DownExpand Up@@ -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(*fork_chain.at_height(1))
.expect_block_connected(*new_tip);
let mut notifier =
ChainNotifier { header_cache: &mut main_chain.header_cache(0..=2), chain_listener };
Expand All@@ -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(1))
.expect_block_connected(*new_tip);
let mut notifier =
ChainNotifier { header_cache: &mut main_chain.header_cache(0..=3), chain_listener };
Expand All@@ -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(*fork_chain.at_height(1))
.expect_block_connected(*fork_chain.at_height(2))
.expect_block_connected(*new_tip);
let mut notifier =
Expand Down
20 changes: 12 additions & 8 deletions lightning-block-sync/src/test_utils.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
Expand DownExpand Up@@ -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, _fork_point: BestBlock) {}
}

pub struct MockChainListener {
Expand DownExpand Up@@ -231,8 +232,8 @@ impl MockChainListener {
self
}

pub fn expect_block_disconnected(self, block: BlockHeaderData) -> Self {
self.expected_blocks_disconnected.borrow_mut().push_back(block);
pub fn expect_blocks_disconnected(self, fork_point: BlockHeaderData) -> Self {
self.expected_blocks_disconnected.borrow_mut().push_back(fork_point);
self
}
}
Expand DownExpand Up@@ -264,14 +265,17 @@ impl chain::Listen for MockChainListener {
}
}

fn block_disconnected(&self, header: &Header, height: u32) {
fn blocks_disconnected(&self, fork_point: BestBlock) {
match self.expected_blocks_disconnected.borrow_mut().pop_front() {
None => {
panic!("Unexpected block disconnected: {:?}", header.block_hash());
panic!(
"Unexpected block(s) disconnected {} at height {}",
fork_point.block_hash, fork_point.height,
);
},
Some(expected_block) => {
assert_eq!(header.block_hash(), expected_block.header.block_hash());
assert_eq!(height, expected_block.height);
Some(expected) => {
assert_eq!(fork_point.block_hash, expected.header.block_hash());
assert_eq!(fork_point.height, expected.height);
},
}
}
Expand Down
11 changes: 4 additions & 7 deletions lightning-liquidity/src/manager.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -767,14 +767,11 @@ 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, fork_point: 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");
Comment thread
TheBlueMatt marked this conversation as resolved.
*best_block = BestBlock::new(header.prev_blockhash, new_height)
assert!(best_block.height > fork_point.height,
"Blocks disconnected must indicate disconnection from the current best height, i.e. the new chain tip must be lower than the previous best height");
*best_block = fork_point;
}

// TODO: Call block_disconnected on all sub-modules that require it, e.g., LSPS1MessageHandler.
Expand Down
15 changes: 7 additions & 8 deletions lightning/src/chain/chainmonitor.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,7 +35,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;
#[cfg(peer_storage)]
Expand DownExpand Up@@ -1008,18 +1008,17 @@ where
self.event_notifier.notify();
}

fn block_disconnected(&self, header: &Header, height: u32) {
fn blocks_disconnected(&self, fork_point: 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 {}",
fork_point.height,
fork_point.block_hash,
);
for monitor_state in monitor_states.values() {
monitor_state.monitor.block_disconnected(
header,
height,
monitor_state.monitor.blocks_disconnected(
fork_point,
&*self.broadcaster,
&*self.fee_estimator,
&self.logger,
Expand Down
48 changes: 22 additions & 26 deletions lightning/src/chain/channelmonitor.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -2297,23 +2297,16 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitor<Signer> {

/// Determines if the disconnected block contained any transactions of interest and updates
/// appropriately.
#[rustfmt::skip]
pub fn block_disconnected<B: Deref, F: Deref, L: Deref>(
&self,
header: &Header,
height: u32,
broadcaster: B,
fee_estimator: F,
logger: &L,
pub fn blocks_disconnected<B: Deref, F: Deref, L: Deref>(
&self, fork_point: BestBlock, broadcaster: B, fee_estimator: F, logger: &L,
) where
B::Target: BroadcasterInterface,
F::Target: FeeEstimator,
L::Target: Logger,
{
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(fork_point, broadcaster, fee_estimator, &logger)
}

/// Processes transactions confirmed in a block with the given header and height, returning new
Expand DownExpand Up@@ -2347,10 +2340,10 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitor<Signer> {

/// 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<B: Deref, F: Deref, L: Deref>(
&self,
Expand DownExpand Up@@ -5113,8 +5106,8 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
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() }
Expand DownExpand Up@@ -5441,12 +5434,12 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
!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 before 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 before it.");
matured_htlcs.push(source.clone());
}

Expand DownExpand Up@@ -5594,23 +5587,26 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
}

#[rustfmt::skip]
fn block_disconnected<B: Deref, F: Deref, L: Deref>(
&mut self, header: &Header, height: u32, broadcaster: B, fee_estimator: F, logger: &WithChannelMonitor<L>
fn blocks_disconnected<B: Deref, F: Deref, L: Deref>(
&mut self, fork_point: BestBlock, broadcaster: B, fee_estimator: F, logger: &WithChannelMonitor<L>
) where B::Target: BroadcasterInterface,
F::Target: FeeEstimator,
L::Target: Logger,
{
log_trace!(logger, "Block {} at height {} disconnected", header.block_hash(), height);
let new_height = fork_point.height;
log_trace!(logger, "Block(s) disconnected to height {}", new_height);
assert!(self.best_block.height > fork_point.height,
"Blocks disconnected must indicate disconnection from the current best height, i.e. the new chain tip must be lower than the previous best 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);

// TODO: Replace with `take_if` once our MSRV is >= 1.80.
let mut should_broadcast_commitment = false;
if let Some((_, conf_height)) = self.alternative_funding_confirmed.as_ref() {
if *conf_height == height {
if *conf_height > new_height {
self.alternative_funding_confirmed.take();
if self.holder_tx_signed || self.funding_spend_seen {
// Cancel any previous claims that are no longer valid as they stemmed from a
Expand All@@ -5626,8 +5622,8 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {

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
self.onchain_tx_handler.blocks_disconnected(
new_height, &broadcaster, conf_target, &self.destination_script, &bounded_fee_estimator, logger
);

// Only attempt to broadcast the new commitment after the `block_disconnected` call above so that
Expand All@@ -5636,7 +5632,7 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
self.queue_latest_holder_commitment_txn_for_broadcast(&broadcaster, &bounded_fee_estimator, logger);
}

self.best_block = BestBlock::new(header.prev_blockhash, height - 1);
self.best_block = fork_point;
}

#[rustfmt::skip]
Expand DownExpand Up@@ -6110,8 +6106,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, fork_point: BestBlock) {
self.0.blocks_disconnected(fork_point, &*self.1, &*self.2, &self.3);
}
}

Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Drop the need for fork headers when calling Listen's disconnect by TheBlueMatt · Pull Request #3876 · lightningdevkit/rust-lightning · GitHub
Skip to content
Merged
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
5 changes: 3 additions & 2 deletions fuzz/src/full_stack.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -343,8 +343,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);
Expand Down
30 changes: 11 additions & 19 deletions lightning-block-sync/src/init.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@ use bitcoin::hash_types::BlockHash;
use bitcoin::network::Network;

use lightning::chain;
use lightning::chain::BestBlock;

use std::ops::Deref;

Expand DownExpand Up@@ -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, fork_point: BestBlock) {
self.0.blocks_disconnected(fork_point)
}
}

Expand All@@ -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, _fork_point: BestBlock) {
unreachable!()
}
}
Expand DownExpand Up@@ -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(1))
.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(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_blocks_disconnected(*fork_chain_3.at_height(3))
.expect_block_connected(*main_chain.at_height(4));

let listeners = vec![
Expand All@@ -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(1))
.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(1))
.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(1))
.expect_block_connected(*main_chain.at_height(2))
.expect_block_connected(*main_chain.at_height(3))
.expect_block_connected(*main_chain.at_height(4));
Expand All@@ -380,7 +372,7 @@ mod tests {
let old_tip = fork_chain.tip();

let listener = MockChainListener::new()
.expect_block_disconnected(*old_tip)
.expect_blocks_disconnected(*fork_chain.at_height(1))
.expect_block_connected(*new_tip);

let listeners = vec![(old_tip.block_hash, &listener as &dyn chain::Listen)];
Expand Down
20 changes: 11 additions & 9 deletions lightning-block-sync/src/lib.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
Expand DownExpand Up@@ -398,12 +398,15 @@ where
}

/// Notifies the chain listeners of disconnected blocks.
fn disconnect_blocks(&mut self, mut disconnected_blocks: Vec<ValidatedBlockHeader>) {
for header in disconnected_blocks.drain(..) {
fn disconnect_blocks(&mut self, disconnected_blocks: Vec<ValidatedBlockHeader>) {
for header in disconnected_blocks.iter() {
if let Some(cached_header) = self.header_cache.block_disconnected(&header.block_hash) {

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.

Should we change the Cache API accordingly?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

We could, but it seems somewhat disjoint from this PR, given there's further work to do in lightning-block-sync anyway, so I kinda wanted to keep it as small as can be.

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.

Mhh, might be good to document somewhere what steps you deem left before you'd consider the transition to this new approach complete. I guess all of them should land before the next release then, to not end up with a half-migrated codebase in the release?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

I mean the API/stuff in lightning in in a perfectly fine state after this PR, and lightning-block-sync just deserves a bit of efficiency in dropping a vec and improving the cache afterwards. This PR obviously doesn't accomplish the goals that #3600 set out to, but is just a step towards it (the "complicated" step that requires making sure all the lightning-internal stuff isn't broken by changing the API).

AFAIU, to get to the point that bitcoind (and all) syncs support swapping to a new chain during a reorg and don't fetch a pile of redundant blocks we need to:

  • make BestBlock contain a list of 6(ish?) block hashes, not one
  • use that list in lightning-block-sync (and eventually lightning-transaction-sync) to do the sync without leaning on the cache
  • expand the runtime caching to cache partial chains across clients in the init sync logic
  • (eventually) ensure lightning-block-sync doesn't have any spare vecs or whatever lying around.

@tnulltnullJul 3, 2025

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.

I still think it would make sense to open an issue for this larger refactoring you're planning, as there are connected aspects to discuss that don't really belong in this PR.

make BestBlock contain a list of 6(ish?) block hashes, not one

For example, how do we imagine this to work exactly, especially for the Confirm/lightning-transaction-sync case?

For Esplora, we currently retrieve the tip hash, retrieve the header and the block status (including the hight), before we call best_block_updated, which is already 3 costly RTTs. If we now would require to track the 6 most-recent hashes, we'd need to make at least 5 more subsequent calls (bringing this it at least 8 RTTs per sync round) to retrieve the hashes at [(height-5)..(height-1)]. But, given that these are individual calls, there is no way to tell if any reorg happened during some of these calls, so they are inherently race-y.

For Electrum, the results would be very similar for the polling client in its current form. Note we eventually want to switch the client to a streaming version making use of Electum's subscription model though, and requiring 'last 6 blocks' would probably require us to resort to costly polling again.

If we'd otherwise extend the client to start tracking more of the chain state (i.e., actually tracking a local chain 6-suffix) across rounds this would complicate the client logic quite a bit and make it even more susceptible to race conditions.

TLDR: I still maintain all of this would be/is resulting in a major refactor across our chain syncing crates and logic, and it would be great to discuss what we imagine to be involved and the corresponding trade-offs before we just jump into it heads first.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

We can definitely make it optional (and only used for full-node-sync) if we think its a major issue. Of course the issue becomes that you cannot (even today) safely transition from an esplora/electrum-synced node to a full-chain-sync one, but addressing that really requires a substantial rework of how we think about chain sync, so maybe its not worth worrying about that.

Of course, storing the last 6 blocks is pretty trivial for a full-node-sync client, and the point is really just to expose the thing that the sync driver gave us, so its not like we'd use it in Listen/Confirm implementations.

My understanding is that this would address the restart-with-different-node case that #3600 wanted to address, even if its optional and just used by the full-node-syncing logic?

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.

but addressing that really requires a substantial rework of how we think about chain sync, so maybe its not worth worrying about that.

Hmm, given that we even recently had users inquire whether they could switch chain sources at runtime, I do expect that we will need to tackle this eventually, as there will be users wanting to switch chain sources, and it's very unintuitive to either prohibit that, have it one-way, or even document the risks that might come from it.

Of course, storing the last 6 blocks is pretty trivial for a full-node-sync client, and the point is really just to expose the thing that the sync driver gave us, so its not like we'd use it in Listen/Confirm implementations.

So we would only do it when it's convenient, which means we could switch bitcoind nodes, but still not between chain sources?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Hmm, given that we even recently had users inquire whether they could switch chain sources at runtime, I do expect that we will need to tackle this eventually, as there will be users wanting to switch chain sources, and it's very unintuitive to either prohibit that, have it one-way, or even document the risks that might come from it.

Sure, I agree, hence why I suggested moving to storing several recent blocks rather than only one. However, as you note it might not be super practical, so we're kinda stuck.

I think, basically, if we did that plus required transactions_confirmed calls come before best_block_updated calls (which may we're doing anyway, cc #3867 (comment)) then moving from one chain source to another is totally fine, even at runtime (as long as you don't miss transactions).

So we would only do it when it's convenient, which means we could switch bitcoind nodes, but still not between chain sources?

I mean I'm not sure what else we can do? If its too impractical to do it on some chain sources we can't really make it required, and if its not available switching chain sources isn't going to be 100% reliable.

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() {

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.

If we're now happy to use the fork point, why are we keeping a Vec of all disconnected blocks around at all? Shouldn't we now be able to convert this to the new behavior entirely? Seems ChainDifference::common_ancestor would already provide what we need, no?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Its needed by the cache, which I didn't want to change in this PR - #3876 (comment)

let fork_point = BestBlock::new(block.header.prev_blockhash, block.height - 1);
self.chain_listener.blocks_disconnected(fork_point);
}
}

Expand DownExpand Up@@ -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(*fork_chain.at_height(1))
.expect_block_connected(*new_tip);
let mut notifier =
ChainNotifier { header_cache: &mut main_chain.header_cache(0..=2), chain_listener };
Expand All@@ -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(1))
.expect_block_connected(*new_tip);
let mut notifier =
ChainNotifier { header_cache: &mut main_chain.header_cache(0..=3), chain_listener };
Expand All@@ -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(*fork_chain.at_height(1))
.expect_block_connected(*fork_chain.at_height(2))
.expect_block_connected(*new_tip);
let mut notifier =
Expand Down
20 changes: 12 additions & 8 deletions lightning-block-sync/src/test_utils.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
Expand DownExpand Up@@ -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, _fork_point: BestBlock) {}
}

pub struct MockChainListener {
Expand DownExpand Up@@ -231,8 +232,8 @@ impl MockChainListener {
self
}

pub fn expect_block_disconnected(self, block: BlockHeaderData) -> Self {
self.expected_blocks_disconnected.borrow_mut().push_back(block);
pub fn expect_blocks_disconnected(self, fork_point: BlockHeaderData) -> Self {
self.expected_blocks_disconnected.borrow_mut().push_back(fork_point);
self
}
}
Expand DownExpand Up@@ -264,14 +265,17 @@ impl chain::Listen for MockChainListener {
}
}

fn block_disconnected(&self, header: &Header, height: u32) {
fn blocks_disconnected(&self, fork_point: BestBlock) {
match self.expected_blocks_disconnected.borrow_mut().pop_front() {
None => {
panic!("Unexpected block disconnected: {:?}", header.block_hash());
panic!(
"Unexpected block(s) disconnected {} at height {}",
fork_point.block_hash, fork_point.height,
);
},
Some(expected_block) => {
assert_eq!(header.block_hash(), expected_block.header.block_hash());
assert_eq!(height, expected_block.height);
Some(expected) => {
assert_eq!(fork_point.block_hash, expected.header.block_hash());
assert_eq!(fork_point.height, expected.height);
},
}
}
Expand Down
11 changes: 4 additions & 7 deletions lightning-liquidity/src/manager.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -767,14 +767,11 @@ 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, fork_point: 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");
Comment thread
TheBlueMatt marked this conversation as resolved.
*best_block = BestBlock::new(header.prev_blockhash, new_height)
assert!(best_block.height > fork_point.height,
"Blocks disconnected must indicate disconnection from the current best height, i.e. the new chain tip must be lower than the previous best height");
*best_block = fork_point;
}

// TODO: Call block_disconnected on all sub-modules that require it, e.g., LSPS1MessageHandler.
Expand Down
15 changes: 7 additions & 8 deletions lightning/src/chain/chainmonitor.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,7 +35,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;
#[cfg(peer_storage)]
Expand DownExpand Up@@ -1008,18 +1008,17 @@ where
self.event_notifier.notify();
}

fn block_disconnected(&self, header: &Header, height: u32) {
fn blocks_disconnected(&self, fork_point: 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 {}",
fork_point.height,
fork_point.block_hash,
);
for monitor_state in monitor_states.values() {
monitor_state.monitor.block_disconnected(
header,
height,
monitor_state.monitor.blocks_disconnected(
fork_point,
&*self.broadcaster,
&*self.fee_estimator,
&self.logger,
Expand Down
48 changes: 22 additions & 26 deletions lightning/src/chain/channelmonitor.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -2297,23 +2297,16 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitor<Signer> {

/// Determines if the disconnected block contained any transactions of interest and updates
/// appropriately.
#[rustfmt::skip]
pub fn block_disconnected<B: Deref, F: Deref, L: Deref>(
&self,
header: &Header,
height: u32,
broadcaster: B,
fee_estimator: F,
logger: &L,
pub fn blocks_disconnected<B: Deref, F: Deref, L: Deref>(
&self, fork_point: BestBlock, broadcaster: B, fee_estimator: F, logger: &L,
) where
B::Target: BroadcasterInterface,
F::Target: FeeEstimator,
L::Target: Logger,
{
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(fork_point, broadcaster, fee_estimator, &logger)
}

/// Processes transactions confirmed in a block with the given header and height, returning new
Expand DownExpand Up@@ -2347,10 +2340,10 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitor<Signer> {

/// 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<B: Deref, F: Deref, L: Deref>(
&self,
Expand DownExpand Up@@ -5113,8 +5106,8 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
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() }
Expand DownExpand Up@@ -5441,12 +5434,12 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
!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 before 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 before it.");
matured_htlcs.push(source.clone());
}

Expand DownExpand Up@@ -5594,23 +5587,26 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
}

#[rustfmt::skip]
fn block_disconnected<B: Deref, F: Deref, L: Deref>(
&mut self, header: &Header, height: u32, broadcaster: B, fee_estimator: F, logger: &WithChannelMonitor<L>
fn blocks_disconnected<B: Deref, F: Deref, L: Deref>(
&mut self, fork_point: BestBlock, broadcaster: B, fee_estimator: F, logger: &WithChannelMonitor<L>
) where B::Target: BroadcasterInterface,
F::Target: FeeEstimator,
L::Target: Logger,
{
log_trace!(logger, "Block {} at height {} disconnected", header.block_hash(), height);
let new_height = fork_point.height;
log_trace!(logger, "Block(s) disconnected to height {}", new_height);
assert!(self.best_block.height > fork_point.height,
"Blocks disconnected must indicate disconnection from the current best height, i.e. the new chain tip must be lower than the previous best 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);

// TODO: Replace with `take_if` once our MSRV is >= 1.80.
let mut should_broadcast_commitment = false;
if let Some((_, conf_height)) = self.alternative_funding_confirmed.as_ref() {
if *conf_height == height {
if *conf_height > new_height {
self.alternative_funding_confirmed.take();
if self.holder_tx_signed || self.funding_spend_seen {
// Cancel any previous claims that are no longer valid as they stemmed from a
Expand All@@ -5626,8 +5622,8 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {

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
self.onchain_tx_handler.blocks_disconnected(
new_height, &broadcaster, conf_target, &self.destination_script, &bounded_fee_estimator, logger
);

// Only attempt to broadcast the new commitment after the `block_disconnected` call above so that
Expand All@@ -5636,7 +5632,7 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
self.queue_latest_holder_commitment_txn_for_broadcast(&broadcaster, &bounded_fee_estimator, logger);
}

self.best_block = BestBlock::new(header.prev_blockhash, height - 1);
self.best_block = fork_point;
}

#[rustfmt::skip]
Expand DownExpand Up@@ -6110,8 +6106,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, fork_point: BestBlock) {
self.0.blocks_disconnected(fork_point, &*self.1, &*self.2, &self.3);
}
}

Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); Drop the need for fork headers when calling Listen's disconnect by TheBlueMatt · Pull Request #3876 · lightningdevkit/rust-lightning · GitHub
Skip to content
Merged
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
5 changes: 3 additions & 2 deletions fuzz/src/full_stack.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -343,8 +343,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);
Expand Down
30 changes: 11 additions & 19 deletions lightning-block-sync/src/init.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@ use bitcoin::hash_types::BlockHash;
use bitcoin::network::Network;

use lightning::chain;
use lightning::chain::BestBlock;

use std::ops::Deref;

Expand DownExpand Up@@ -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, fork_point: BestBlock) {
self.0.blocks_disconnected(fork_point)
}
}

Expand All@@ -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, _fork_point: BestBlock) {
unreachable!()
}
}
Expand DownExpand Up@@ -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(1))
.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(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_blocks_disconnected(*fork_chain_3.at_height(3))
.expect_block_connected(*main_chain.at_height(4));

let listeners = vec![
Expand All@@ -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(1))
.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(1))
.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(1))
.expect_block_connected(*main_chain.at_height(2))
.expect_block_connected(*main_chain.at_height(3))
.expect_block_connected(*main_chain.at_height(4));
Expand All@@ -380,7 +372,7 @@ mod tests {
let old_tip = fork_chain.tip();

let listener = MockChainListener::new()
.expect_block_disconnected(*old_tip)
.expect_blocks_disconnected(*fork_chain.at_height(1))
.expect_block_connected(*new_tip);

let listeners = vec![(old_tip.block_hash, &listener as &dyn chain::Listen)];
Expand Down
20 changes: 11 additions & 9 deletions lightning-block-sync/src/lib.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
Expand DownExpand Up@@ -398,12 +398,15 @@ where
}

/// Notifies the chain listeners of disconnected blocks.
fn disconnect_blocks(&mut self, mut disconnected_blocks: Vec<ValidatedBlockHeader>) {
for header in disconnected_blocks.drain(..) {
fn disconnect_blocks(&mut self, disconnected_blocks: Vec<ValidatedBlockHeader>) {
for header in disconnected_blocks.iter() {
if let Some(cached_header) = self.header_cache.block_disconnected(&header.block_hash) {

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.

Should we change the Cache API accordingly?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

We could, but it seems somewhat disjoint from this PR, given there's further work to do in lightning-block-sync anyway, so I kinda wanted to keep it as small as can be.

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.

Mhh, might be good to document somewhere what steps you deem left before you'd consider the transition to this new approach complete. I guess all of them should land before the next release then, to not end up with a half-migrated codebase in the release?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

I mean the API/stuff in lightning in in a perfectly fine state after this PR, and lightning-block-sync just deserves a bit of efficiency in dropping a vec and improving the cache afterwards. This PR obviously doesn't accomplish the goals that #3600 set out to, but is just a step towards it (the "complicated" step that requires making sure all the lightning-internal stuff isn't broken by changing the API).

AFAIU, to get to the point that bitcoind (and all) syncs support swapping to a new chain during a reorg and don't fetch a pile of redundant blocks we need to:

  • make BestBlock contain a list of 6(ish?) block hashes, not one
  • use that list in lightning-block-sync (and eventually lightning-transaction-sync) to do the sync without leaning on the cache
  • expand the runtime caching to cache partial chains across clients in the init sync logic
  • (eventually) ensure lightning-block-sync doesn't have any spare vecs or whatever lying around.

@tnulltnullJul 3, 2025

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.

I still think it would make sense to open an issue for this larger refactoring you're planning, as there are connected aspects to discuss that don't really belong in this PR.

make BestBlock contain a list of 6(ish?) block hashes, not one

For example, how do we imagine this to work exactly, especially for the Confirm/lightning-transaction-sync case?

For Esplora, we currently retrieve the tip hash, retrieve the header and the block status (including the hight), before we call best_block_updated, which is already 3 costly RTTs. If we now would require to track the 6 most-recent hashes, we'd need to make at least 5 more subsequent calls (bringing this it at least 8 RTTs per sync round) to retrieve the hashes at [(height-5)..(height-1)]. But, given that these are individual calls, there is no way to tell if any reorg happened during some of these calls, so they are inherently race-y.

For Electrum, the results would be very similar for the polling client in its current form. Note we eventually want to switch the client to a streaming version making use of Electum's subscription model though, and requiring 'last 6 blocks' would probably require us to resort to costly polling again.

If we'd otherwise extend the client to start tracking more of the chain state (i.e., actually tracking a local chain 6-suffix) across rounds this would complicate the client logic quite a bit and make it even more susceptible to race conditions.

TLDR: I still maintain all of this would be/is resulting in a major refactor across our chain syncing crates and logic, and it would be great to discuss what we imagine to be involved and the corresponding trade-offs before we just jump into it heads first.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

We can definitely make it optional (and only used for full-node-sync) if we think its a major issue. Of course the issue becomes that you cannot (even today) safely transition from an esplora/electrum-synced node to a full-chain-sync one, but addressing that really requires a substantial rework of how we think about chain sync, so maybe its not worth worrying about that.

Of course, storing the last 6 blocks is pretty trivial for a full-node-sync client, and the point is really just to expose the thing that the sync driver gave us, so its not like we'd use it in Listen/Confirm implementations.

My understanding is that this would address the restart-with-different-node case that #3600 wanted to address, even if its optional and just used by the full-node-syncing logic?

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.

but addressing that really requires a substantial rework of how we think about chain sync, so maybe its not worth worrying about that.

Hmm, given that we even recently had users inquire whether they could switch chain sources at runtime, I do expect that we will need to tackle this eventually, as there will be users wanting to switch chain sources, and it's very unintuitive to either prohibit that, have it one-way, or even document the risks that might come from it.

Of course, storing the last 6 blocks is pretty trivial for a full-node-sync client, and the point is really just to expose the thing that the sync driver gave us, so its not like we'd use it in Listen/Confirm implementations.

So we would only do it when it's convenient, which means we could switch bitcoind nodes, but still not between chain sources?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Hmm, given that we even recently had users inquire whether they could switch chain sources at runtime, I do expect that we will need to tackle this eventually, as there will be users wanting to switch chain sources, and it's very unintuitive to either prohibit that, have it one-way, or even document the risks that might come from it.

Sure, I agree, hence why I suggested moving to storing several recent blocks rather than only one. However, as you note it might not be super practical, so we're kinda stuck.

I think, basically, if we did that plus required transactions_confirmed calls come before best_block_updated calls (which may we're doing anyway, cc #3867 (comment)) then moving from one chain source to another is totally fine, even at runtime (as long as you don't miss transactions).

So we would only do it when it's convenient, which means we could switch bitcoind nodes, but still not between chain sources?

I mean I'm not sure what else we can do? If its too impractical to do it on some chain sources we can't really make it required, and if its not available switching chain sources isn't going to be 100% reliable.

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() {

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.

If we're now happy to use the fork point, why are we keeping a Vec of all disconnected blocks around at all? Shouldn't we now be able to convert this to the new behavior entirely? Seems ChainDifference::common_ancestor would already provide what we need, no?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Its needed by the cache, which I didn't want to change in this PR - #3876 (comment)

let fork_point = BestBlock::new(block.header.prev_blockhash, block.height - 1);
self.chain_listener.blocks_disconnected(fork_point);
}
}

Expand DownExpand Up@@ -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(*fork_chain.at_height(1))
.expect_block_connected(*new_tip);
let mut notifier =
ChainNotifier { header_cache: &mut main_chain.header_cache(0..=2), chain_listener };
Expand All@@ -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(1))
.expect_block_connected(*new_tip);
let mut notifier =
ChainNotifier { header_cache: &mut main_chain.header_cache(0..=3), chain_listener };
Expand All@@ -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(*fork_chain.at_height(1))
.expect_block_connected(*fork_chain.at_height(2))
.expect_block_connected(*new_tip);
let mut notifier =
Expand Down
20 changes: 12 additions & 8 deletions lightning-block-sync/src/test_utils.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
Expand DownExpand Up@@ -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, _fork_point: BestBlock) {}
}

pub struct MockChainListener {
Expand DownExpand Up@@ -231,8 +232,8 @@ impl MockChainListener {
self
}

pub fn expect_block_disconnected(self, block: BlockHeaderData) -> Self {
self.expected_blocks_disconnected.borrow_mut().push_back(block);
pub fn expect_blocks_disconnected(self, fork_point: BlockHeaderData) -> Self {
self.expected_blocks_disconnected.borrow_mut().push_back(fork_point);
self
}
}
Expand DownExpand Up@@ -264,14 +265,17 @@ impl chain::Listen for MockChainListener {
}
}

fn block_disconnected(&self, header: &Header, height: u32) {
fn blocks_disconnected(&self, fork_point: BestBlock) {
match self.expected_blocks_disconnected.borrow_mut().pop_front() {
None => {
panic!("Unexpected block disconnected: {:?}", header.block_hash());
panic!(
"Unexpected block(s) disconnected {} at height {}",
fork_point.block_hash, fork_point.height,
);
},
Some(expected_block) => {
assert_eq!(header.block_hash(), expected_block.header.block_hash());
assert_eq!(height, expected_block.height);
Some(expected) => {
assert_eq!(fork_point.block_hash, expected.header.block_hash());
assert_eq!(fork_point.height, expected.height);
},
}
}
Expand Down
11 changes: 4 additions & 7 deletions lightning-liquidity/src/manager.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -767,14 +767,11 @@ 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, fork_point: 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");
Comment thread
TheBlueMatt marked this conversation as resolved.
*best_block = BestBlock::new(header.prev_blockhash, new_height)
assert!(best_block.height > fork_point.height,
"Blocks disconnected must indicate disconnection from the current best height, i.e. the new chain tip must be lower than the previous best height");
*best_block = fork_point;
}

// TODO: Call block_disconnected on all sub-modules that require it, e.g., LSPS1MessageHandler.
Expand Down
15 changes: 7 additions & 8 deletions lightning/src/chain/chainmonitor.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,7 +35,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;
#[cfg(peer_storage)]
Expand DownExpand Up@@ -1008,18 +1008,17 @@ where
self.event_notifier.notify();
}

fn block_disconnected(&self, header: &Header, height: u32) {
fn blocks_disconnected(&self, fork_point: 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 {}",
fork_point.height,
fork_point.block_hash,
);
for monitor_state in monitor_states.values() {
monitor_state.monitor.block_disconnected(
header,
height,
monitor_state.monitor.blocks_disconnected(
fork_point,
&*self.broadcaster,
&*self.fee_estimator,
&self.logger,
Expand Down
48 changes: 22 additions & 26 deletions lightning/src/chain/channelmonitor.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -2297,23 +2297,16 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitor<Signer> {

/// Determines if the disconnected block contained any transactions of interest and updates
/// appropriately.
#[rustfmt::skip]
pub fn block_disconnected<B: Deref, F: Deref, L: Deref>(
&self,
header: &Header,
height: u32,
broadcaster: B,
fee_estimator: F,
logger: &L,
pub fn blocks_disconnected<B: Deref, F: Deref, L: Deref>(
&self, fork_point: BestBlock, broadcaster: B, fee_estimator: F, logger: &L,
) where
B::Target: BroadcasterInterface,
F::Target: FeeEstimator,
L::Target: Logger,
{
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(fork_point, broadcaster, fee_estimator, &logger)
}

/// Processes transactions confirmed in a block with the given header and height, returning new
Expand DownExpand Up@@ -2347,10 +2340,10 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitor<Signer> {

/// 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<B: Deref, F: Deref, L: Deref>(
&self,
Expand DownExpand Up@@ -5113,8 +5106,8 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
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() }
Expand DownExpand Up@@ -5441,12 +5434,12 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
!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 before 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 before it.");
matured_htlcs.push(source.clone());
}

Expand DownExpand Up@@ -5594,23 +5587,26 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
}

#[rustfmt::skip]
fn block_disconnected<B: Deref, F: Deref, L: Deref>(
&mut self, header: &Header, height: u32, broadcaster: B, fee_estimator: F, logger: &WithChannelMonitor<L>
fn blocks_disconnected<B: Deref, F: Deref, L: Deref>(
&mut self, fork_point: BestBlock, broadcaster: B, fee_estimator: F, logger: &WithChannelMonitor<L>
) where B::Target: BroadcasterInterface,
F::Target: FeeEstimator,
L::Target: Logger,
{
log_trace!(logger, "Block {} at height {} disconnected", header.block_hash(), height);
let new_height = fork_point.height;
log_trace!(logger, "Block(s) disconnected to height {}", new_height);
assert!(self.best_block.height > fork_point.height,
"Blocks disconnected must indicate disconnection from the current best height, i.e. the new chain tip must be lower than the previous best 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);

// TODO: Replace with `take_if` once our MSRV is >= 1.80.
let mut should_broadcast_commitment = false;
if let Some((_, conf_height)) = self.alternative_funding_confirmed.as_ref() {
if *conf_height == height {
if *conf_height > new_height {
self.alternative_funding_confirmed.take();
if self.holder_tx_signed || self.funding_spend_seen {
// Cancel any previous claims that are no longer valid as they stemmed from a
Expand All@@ -5626,8 +5622,8 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {

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
self.onchain_tx_handler.blocks_disconnected(
new_height, &broadcaster, conf_target, &self.destination_script, &bounded_fee_estimator, logger
);

// Only attempt to broadcast the new commitment after the `block_disconnected` call above so that
Expand All@@ -5636,7 +5632,7 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
self.queue_latest_holder_commitment_txn_for_broadcast(&broadcaster, &bounded_fee_estimator, logger);
}

self.best_block = BestBlock::new(header.prev_blockhash, height - 1);
self.best_block = fork_point;
}

#[rustfmt::skip]
Expand DownExpand Up@@ -6110,8 +6106,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, fork_point: BestBlock) {
self.0.blocks_disconnected(fork_point, &*self.1, &*self.2, &self.3);
}
}

Expand Down
Loading
Loading