Skip to content

Drop the need for fork headers when calling Listen's disconnect #3876

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

TheBlueMatt
Copy link
Collaborator

The Listen::block_disconnected method is nice in that listeners
learn about each block disconnected in series. Further, it included
the header of the block that is being disconnected to allow the
listeners to do some checking that the interface is being used
correctly (namely, asserting that the header's block hash matches
their current understanding of the best chain).

However, this interface has some substantial drawbacks. Namely, the
requirement that fork headers be passed in means that restarting
with a new node that has no idea about a previous fork leaves us
unable to replay the chain at all. Further, while when various
listeners were initially written learning about each block
disconnected in series seemed useful, but now we no longer rely on
that anyway because the Confirm interface does not allow for it.

Thus, here, we replace Listen::block_disconnected with a new
Listen::blocks_disconnected, taking only information about the
fork point/new best chain tip (in the form of its block hash and
height) rather than information about previous fork blocks and only
requiring a single call to complete multiple block disconnections
during a reorg.

This requires removing some assertions on block disconnection
ordering, but because we now provide lightning-block-sync and
expect users to use it when using the Listen interface, these
assertions are much less critical.

@ldk-reviews-bot
Copy link

ldk-reviews-bot commented Jun 18, 2025

👋 Thanks for assigning @jkczyz as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

@ldk-reviews-bot
Copy link

👋 The first review has been submitted!

Do you think this PR is ready for a second reviewer? If so, click here to assign a second reviewer.

@ldk-reviews-bot
Copy link

✅ Added second reviewer: @joostjager

@tnull tnull requested review from tnull and removed request for joostjager June 19, 2025 16:11
Copy link

codecov bot commented Jun 19, 2025

Codecov Report

❌ Patch coverage is 72.41379% with 32 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.95%. Comparing base (de2005a) to head (c78b08b).
⚠️ Report is 10 commits behind head on main.

Files with missing lines Patch % Lines
lightning/src/chain/channelmonitor.rs 70.00% 6 Missing ⚠️
lightning/src/chain/mod.rs 0.00% 5 Missing ⚠️
lightning/src/ln/functional_test_utils.rs 64.28% 5 Missing ⚠️
lightning/src/util/sweep.rs 0.00% 5 Missing ⚠️
lightning-liquidity/src/manager.rs 0.00% 4 Missing ⚠️
lightning-block-sync/src/test_utils.rs 66.66% 3 Missing ⚠️
lightning/src/ln/channelmanager.rs 85.71% 2 Missing ⚠️
lightning-block-sync/src/init.rs 90.00% 1 Missing ⚠️
lightning/src/ln/functional_tests.rs 66.66% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3876      +/-   ##
==========================================
+ Coverage   88.93%   88.95%   +0.01%     
==========================================
  Files         174      174              
  Lines      124539   124571      +32     
  Branches   124539   124571      +32     
==========================================
+ Hits       110758   110807      +49     
+ Misses      11287    11270      -17     
  Partials     2494     2494              
Flag Coverage Δ
fuzzing 22.19% <14.11%> (+0.01%) ⬆️
tests 88.77% <72.41%> (+0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@TheBlueMatt TheBlueMatt force-pushed the 2025-06-disconnect-less-info branch from b06cbd4 to a54159c Compare June 19, 2025 18:47
jkczyz
jkczyz previously approved these changes Jun 20, 2025
@ldk-reviews-bot
Copy link

🔔 1st Reminder

Hey @tnull! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

Copy link
Contributor

@tnull tnull left a comment

Choose a reason for hiding this comment

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

Changes LGTM, but while we're here it would be good to add some details regarding the implicit assumptions on the Listen docs, and adding (back) some asserts/debug_asserts would be appreciated.

@TheBlueMatt TheBlueMatt requested review from jkczyz and tnull June 27, 2025 21:01
@ldk-reviews-bot
Copy link

🔔 1st Reminder

Hey @tnull @jkczyz! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

1 similar comment
@ldk-reviews-bot
Copy link

🔔 1st Reminder

Hey @tnull @jkczyz! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

///
/// Each block must be connected in chain order with one call to either
/// [`Listen::block_connected`] or [`Listen::filtered_block_connected`]. If a call to the
/// [`Filter`] interface was made during block processing and further transaction(s) from the same
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, is this a new requirement? I imagine it's very hard to reliably implement this: keep track of all registered Txids/outputs, then match them on a block, apply the block, then check again if something new was registered, filter the block again, then potentially call once more. Also, why do we require it only here, but not on Confirm?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This has always been the case, just poorly documented. It is, indeed, not the case on Confirm (which doesn't require "blocks" to be connected in-order at all, though it does require all transactions to be topologically sorted).

}
self.chain_listener.block_disconnected(&header.header, header.height);
}
if let Some(block) = disconnected_blocks.last() {
Copy link
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
Collaborator Author

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)

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
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
Collaborator Author

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
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
Collaborator Author

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.

Copy link
Contributor

@tnull tnull Jul 3, 2025

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
Collaborator Author

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
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
Collaborator Author

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.

@ldk-reviews-bot
Copy link

🔔 2nd Reminder

Hey @jkczyz! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

Copy link
Contributor

@tnull tnull left a comment

Choose a reason for hiding this comment

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

rustfmt is unhappy right now

@TheBlueMatt TheBlueMatt force-pushed the 2025-06-disconnect-less-info branch from ef73b0a to a6985eb Compare July 2, 2025 22:37
@ldk-reviews-bot
Copy link

🔔 3rd Reminder

Hey @jkczyz! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot
Copy link

🔔 4th Reminder

Hey @jkczyz! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot
Copy link

🔔 5th Reminder

Hey @jkczyz! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot
Copy link

🔔 6th Reminder

Hey @jkczyz! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@@ -231,7 +232,7 @@ impl MockChainListener {
self
}

pub fn expect_block_disconnected(self, block: BlockHeaderData) -> Self {
pub fn expect_blocks_disconnected(self, block: BlockHeaderData) -> Self {
self.expected_blocks_disconnected.borrow_mut().push_back(block);
Copy link
Contributor

Choose a reason for hiding this comment

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

This no longer needs to be a Vec and/or should be renamed, I think?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I guess there could be multiple reorgs and it'd need to be a vec again?

Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm... right but in either case this should take the fork point rather than the last block disconnected.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

If its just one reorg at a time yes, but if there's two reorgs we may go back to the fork point, connect, then go back to a new fork point and a new tip?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, makes sense to keep it as a Vec then. What I was saying is you can just call it twice with each fork point. Updating it is just a way to make the tests read in terms of the new interface.

So for instance, in sync_from_overlapping_chains, previously it was:

let fork_chain_1 = main_chain.fork_at_height(1);

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

And now you have it as:

let fork_chain_1 = main_chain.fork_at_height(1);

let listener_1 = MockChainListener::new()
	.expect_blocks_disconnected(*fork_chain_1.at_height(2))

But it would be closer to the interface as:

let fork_chain_1 = main_chain.fork_at_height(1);

let listener_1 = MockChainListener::new()
	.expect_blocks_disconnected(*fork_chain_1.tip())

Which would need MockChainListener::blocks_disconnected to compare the passed fork point against the expectation rather than using the expectation's previous block.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That doesn't work because fork_at_height creates a new chain with the same length. So we can't just call tip cause the tip is still at height 5 or whatever for all the chains.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, right it would need to be *fork_chain_1.at_height(1).

Comment on lines 277 to 278
assert_eq!(new_best_block.block_hash, expected_block.header.prev_blockhash);
assert_eq!(new_best_block.height, expected_block.height - 1);
Copy link
Contributor

Choose a reason for hiding this comment

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

If the new best block is at the given height, shouldn't we expect that the old block at that height was disconnected?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Here the "expected_block" is the block we expect to be disconnected, so the new height is that height minus one. I renamed expected_block to disconnected_block.

Copy link
Contributor

Choose a reason for hiding this comment

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

Given the code was changed to only call on the fork point, why not change expected_blocks_disconnected to store those instead?

Comment on lines 12557 to 12559
self.do_chain_event(Some(new_best_block.height), |channel| {
channel.best_block_updated(
new_height,
header.time,
new_best_block.height,
Copy link
Contributor

Choose a reason for hiding this comment

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

Won't this mean we process the new best block twice? Once when we give the new best block here and then again when block_connected is called for that block?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The new_best_block here is the fork point block, not the actual final block after the reorg, I will clarify that in the docs.

@TheBlueMatt TheBlueMatt requested a review from jkczyz July 22, 2025 22:23
@TheBlueMatt TheBlueMatt force-pushed the 2025-06-disconnect-less-info branch from 7615b5a to 529dbcc Compare July 22, 2025 22:23
Comment on lines 89 to 111
/// Indicates the new best tip is the provided [`BestBlock`].
fn blocks_disconnected(&self, new_best_block: BestBlock);
/// The provided [`BestBlock`] is the new best block after disconnecting blocks in the reorg
/// but before connecting new ones (i.e. the "fork point" block). For backwards compatibility,
/// you may instead walk the chain backwards, calling `blocks_disconnected` for each block
/// which is disconnected in a reorg.
fn blocks_disconnected(&self, fork_point_block: BestBlock);
Copy link
Contributor

Choose a reason for hiding this comment

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

To avoid confusion, maybe we should rename this fork_detected then? Though I guess the naming wouldn't be intuitive for the backwards compatibility case. But maybe that is not a concern given lightning-block-sync doesn't support backwards compatibility. When would someone need this? When they are using their own implementation of Listen and not using lightning-block-sync?

Copy link
Collaborator Author

@TheBlueMatt TheBlueMatt Jul 23, 2025

Choose a reason for hiding this comment

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

Hmm, but a fork is "detected" before its applied. In something like Bitcoin Core, first you find a fork by seeing the new best header that you'll move to, then you disconnect blocks back to the fork point, then you connect. We want people to call this method after the disconnect step, but before the reconnect part (and then still call connect after).

Copy link
Contributor

Choose a reason for hiding this comment

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

Right, but at least in lightning-block-sync we simply poll the BlockSource for the best block. The implementation would have already disconnected / connected any blocks. We're simply recreating that sequence by finding the fork point. But now we don't care about the disconnects, only whether any occurred.

Are you thinking of a user of Listen that is tightly integrated with the block source? I suppose in that case the current naming is fine.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

You're right that in general things will work by just seeing that a reorg happened and seeing only the result (ultimately most things are downstream of Bitcoin Core which never exposes the state as of the disconnections to the user, only after the re-connections). But my concern with renaming it to reflect that is people might think they don't need to call block_connected again afterwards (after all, they "told us about the reorg"). The nice thing about the block_disconnected terminology is that it implies that things are serially happening - blocks disconnected, then reconnected later.

Copy link
Contributor

@jkczyz jkczyz Aug 4, 2025

Choose a reason for hiding this comment

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

Yeah I see. I guess what I find weird about the new interface is that, IIUC, for the backwards compatibility use, you don't need to go back to the fork point (just one before). So in that sense the parameter naming is not accurate. Plus, anyone implementing the interface needs to support both uses.

edit: Based on reading the trait-level doc change, I think I misunderstood the backwards compatible case. See #3876 (comment)

Comment on lines 89 to 111
/// Indicates the new best tip is the provided [`BestBlock`].
fn blocks_disconnected(&self, new_best_block: BestBlock);
/// The provided [`BestBlock`] is the new best block after disconnecting blocks in the reorg
/// but before connecting new ones (i.e. the "fork point" block). For backwards compatibility,
/// you may instead walk the chain backwards, calling `blocks_disconnected` for each block
/// which is disconnected in a reorg.
fn blocks_disconnected(&self, fork_point_block: BestBlock);
Copy link
Contributor

Choose a reason for hiding this comment

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

Right, but at least in lightning-block-sync we simply poll the BlockSource for the best block. The implementation would have already disconnected / connected any blocks. We're simply recreating that sequence by finding the fork point. But now we don't care about the disconnects, only whether any occurred.

Are you thinking of a user of Listen that is tightly integrated with the block source? I suppose in that case the current naming is fine.

@@ -231,7 +232,7 @@ impl MockChainListener {
self
}

pub fn expect_block_disconnected(self, block: BlockHeaderData) -> Self {
pub fn expect_blocks_disconnected(self, block: BlockHeaderData) -> Self {
self.expected_blocks_disconnected.borrow_mut().push_back(block);
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm... right but in either case this should take the fork point rather than the last block disconnected.

Comment on lines 277 to 278
assert_eq!(new_best_block.block_hash, expected_block.header.prev_blockhash);
assert_eq!(new_best_block.height, expected_block.height - 1);
Copy link
Contributor

Choose a reason for hiding this comment

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

Given the code was changed to only call on the fork point, why not change expected_blocks_disconnected to store those instead?

@TheBlueMatt TheBlueMatt force-pushed the 2025-06-disconnect-less-info branch from 529dbcc to 5c309a1 Compare July 28, 2025 19:41
@TheBlueMatt TheBlueMatt requested a review from jkczyz July 28, 2025 19:43
@ldk-reviews-bot
Copy link

🔔 1st Reminder

Hey @jkczyz! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot
Copy link

🔔 2nd Reminder

Hey @jkczyz! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot
Copy link

🔔 3rd Reminder

Hey @jkczyz! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

Comment on lines 89 to 111
/// Indicates the new best tip is the provided [`BestBlock`].
fn blocks_disconnected(&self, new_best_block: BestBlock);
/// The provided [`BestBlock`] is the new best block after disconnecting blocks in the reorg
/// but before connecting new ones (i.e. the "fork point" block). For backwards compatibility,
/// you may instead walk the chain backwards, calling `blocks_disconnected` for each block
/// which is disconnected in a reorg.
fn blocks_disconnected(&self, fork_point_block: BestBlock);
Copy link
Contributor

@jkczyz jkczyz Aug 4, 2025

Choose a reason for hiding this comment

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

Yeah I see. I guess what I find weird about the new interface is that, IIUC, for the backwards compatibility use, you don't need to go back to the fork point (just one before). So in that sense the parameter naming is not accurate. Plus, anyone implementing the interface needs to support both uses.

edit: Based on reading the trait-level doc change, I think I misunderstood the backwards compatible case. See #3876 (comment)

@@ -231,7 +232,7 @@ impl MockChainListener {
self
}

pub fn expect_block_disconnected(self, block: BlockHeaderData) -> Self {
pub fn expect_blocks_disconnected(self, block: BlockHeaderData) -> Self {
self.expected_blocks_disconnected.borrow_mut().push_back(block);
Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, makes sense to keep it as a Vec then. What I was saying is you can just call it twice with each fork point. Updating it is just a way to make the tests read in terms of the new interface.

So for instance, in sync_from_overlapping_chains, previously it was:

let fork_chain_1 = main_chain.fork_at_height(1);

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

And now you have it as:

let fork_chain_1 = main_chain.fork_at_height(1);

let listener_1 = MockChainListener::new()
	.expect_blocks_disconnected(*fork_chain_1.at_height(2))

But it would be closer to the interface as:

let fork_chain_1 = main_chain.fork_at_height(1);

let listener_1 = MockChainListener::new()
	.expect_blocks_disconnected(*fork_chain_1.tip())

Which would need MockChainListener::blocks_disconnected to compare the passed fork point against the expectation rather than using the expectation's previous block.

Comment on lines -1162 to 1176
assert!(request.merge_package(package, height).is_ok());
assert!(request.merge_package(package, new_best_height + 1).is_ok());
// Using a HashMap guarantee us than if we have multiple outpoints getting
// resurrected only one bump claim tx is going to be broadcast
bump_candidates.insert(pending_claim.clone(), request.clone());
Copy link
Contributor

Choose a reason for hiding this comment

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

Previously, this would use the height of the first disconnected block rather than the right before the fork, which is different if the re-org is more than one block. Is this ok?

Copy link
Collaborator Author

@TheBlueMatt TheBlueMatt Aug 5, 2025

Choose a reason for hiding this comment

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

Yea, it shouldn't break anything. It means we may more aggressively merge packages than we maybe want to (due to the COUNTERPARTY_CLAIMABLE_WITHIN_BLOCKS_PINNABLE; checks) but also we'll probably select better package_locktimes by jumping back instead of stepping back one block at a time. Really we probably want to wait to do much of anything until we've fully finished processing the full reorg, but that would be a very substantial API overhaul...

More generally, the "jump back to fork point" thing already has to work because that's what we expect Confirm users to do.

Comment on lines 1176 to 1189
if let Some((new_timer, new_feerate, bump_claim)) = self.generate_claim(
current_height, &request, &FeerateStrategy::ForceBump, conf_target,
new_best_height, &request, &FeerateStrategy::ForceBump, conf_target,
destination_script, fee_estimator, logger
Copy link
Contributor

Choose a reason for hiding this comment

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

Same question here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I don't think so - it may result in a bit more aggressive RBF than maybe we want, but we'll survive.

Also, more generally, the "jump back to fork point" thing already has to work because that's what we expect Confirm users to do.

pub(super) fn block_disconnected<B: Deref, F: Deref, L: Logger>(
&mut self, height: u32, broadcaster: B, conf_target: ConfirmationTarget,
pub(super) fn blocks_disconnected<B: Deref, F: Deref, L: Logger>(
&mut self, new_best_height: u32, broadcaster: B, conf_target: ConfirmationTarget,
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm... when called from transaction_unconfirmed, this is not the new best height, but rather one before the observed confirmation height. That seems ok given the changes to the checks below. But the parameter name isn't accurate for both calling variations. Would dropping the commit and renaming the parameter to disconnected_block_height be any better? (cc: @tnull)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hmm, it is the new best height as far as we're aware, though? Not sure its really worth changing.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, yeah and I guess disconnected_block_height isn't actually accurate since we are subtracting one.

Comment on lines 86 to 90
/// disconnected in a reorg, each time passing the new-best-block (i.e. information about the block
/// prior to the one being disconnected) such that you ultimately pass information about the fork
/// point to `blocks_disconnected`.
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this really backwards compatible? They would be making the same number of calls but one of them would have a different parameter.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I mean its more backwards-compatible? Yea, its still not the same API - you have to call with the previous block hash + height instead of the disconnected block header + height, which is different, but its a much smaller change over just calling it with the fork point. I suppose we could remove the discussion of the backwards compatibility thing, but we would still handle it just fine, I'm not sure what we'd ever do that would break if you call with repeated disconnects further back.

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe best to drop the backwards compatibility discussion then. That seems to depend largely on how Listen is implemented. e.g., if using equality with height instead of inequality, then the implementation might be broken for a one-block reorg since the height of the fork point rather than the disconnected block would be given. Also, wouldn't be backwards compatible if the implementation requires the disconnected block's hash or information from the disconnected block's header.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I simplified it a bit, I don't want to fully remove it because I want to make it a part of the API that you're allowed to call them repeatedly. eg in the case of a reorg and then another reorg before the first is fully handled, you can just call it again and go back.

@TheBlueMatt TheBlueMatt force-pushed the 2025-06-disconnect-less-info branch from 5c309a1 to 8a404d6 Compare August 5, 2025 13:54
@TheBlueMatt TheBlueMatt requested a review from jkczyz August 5, 2025 14:08
@TheBlueMatt TheBlueMatt force-pushed the 2025-06-disconnect-less-info branch from 8a404d6 to d1e3ca2 Compare August 5, 2025 14:08
Copy link
Contributor

@tnull tnull left a comment

Choose a reason for hiding this comment

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

Excuse the delay here, will see to pickup review here.

For now CI is unhappy:

error[E0425]: cannot find value `height` in this scope
    --> lightning/src/chain/channelmonitor.rs:5338:23
     |
5338 |             if *conf_height == height {
     |                                ^^^^^^ not found in this scope

For more information about this error, try `rustc --explain E0425`.
error: could not compile `lightning` due to previous error

@tnull tnull self-requested a review August 7, 2025 11:28
Copy link
Contributor

@jkczyz jkczyz left a comment

Choose a reason for hiding this comment

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

Looks like a rebase is needed to fix CI.

/// disconnected in a reorg, each time passing the new-best-block (i.e. information about the block
/// prior to the one being disconnected) such that you ultimately pass information about the fork
/// point to `blocks_disconnected`.
/// "fork point" block, i.e. the highest block that is in both forks. You may call
Copy link
Contributor

Choose a reason for hiding this comment

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

s/You may/You may instead

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I dunno if its always "instead", there's the case of a back-to-back reorg where you're jumping back to the fork point but suddenly find a new one and step backwards more.

@TheBlueMatt TheBlueMatt force-pushed the 2025-06-disconnect-less-info branch from 705c35c to ddc96bd Compare August 7, 2025 14:45
@TheBlueMatt
Copy link
Collaborator Author

Rebased and fixed the silent conflict.

@TheBlueMatt TheBlueMatt force-pushed the 2025-06-disconnect-less-info branch from ddc96bd to bf41b15 Compare August 7, 2025 19:45
Copy link
Contributor

@jkczyz jkczyz left a comment

Choose a reason for hiding this comment

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

LGTM. Please squash.

The `Listen::block_disconnected` method is nice in that listeners
learn about each block disconnected in series. Further, it included
the header of the block that is being disconnected to allow the
listeners to do some checking that the interface is being used
correctly (namely, asserting that the header's block hash matches
their current understanding of the best chain).

However, this interface has some substantial drawbacks. Namely, the
requirement that fork headers be passed in means that restarting
with a new node that has no idea about a previous fork leaves us
unable to replay the chain at all. Further, while when various
listeners were initially written learning about each block
disconnected in series seemed useful, but now we no longer rely on
that anyway because the `Confirm` interface does not allow for it.

Thus, here, we replace `Listen::block_disconnected` with a new
`Listen::blocks_disconnected`, taking only information about the
fork point/new best chain tip (in the form of its block hash and
height) rather than information about previous fork blocks and only
requiring a single call to complete multiple block disconnections
during a reorg.

We also swap to using a single `BestBlock` to describe the new
chain tip, in anticipation of future extensions to `BestBlock`.

This requires removing some assertions on block disconnection
ordering, but because we now provide `lightning-block-sync` and
expect users to use it when using the `Listen` interface, these
assertions are much less critical.
`OnchainTxHandler` is an internal struct and doesn't implement
`Listen`, but its still nice to have its API mirror the `Listen`
API so that internal code all looks similar.
Now that the `Listen` interface allows blocks to be disconnected in
batches rather than one at a time, we should test this. Here we add
a new `ConnectStyle` for the functional test framework which tests
doing so.
When calling `Channel::best_block_updated` we pass it the timestamp
of the block we're connecting so that it can track the highest
timestamp it has seen.

However, in some cases, we don't actually have a timestamp to pass,
which `Channel::best_block_updated` will happily ignore as it
always takes the `max` of its existing value. Thus, we really
should pass a `None` to ensure the API is understandable, which we
do here.
`Listen` is somewhat quiet on high-level use and even requirements,
which we document further here.
@TheBlueMatt TheBlueMatt force-pushed the 2025-06-disconnect-less-info branch from bf41b15 to c78b08b Compare August 7, 2025 20:29
@TheBlueMatt
Copy link
Collaborator Author

Squashed without further changes.

Copy link
Contributor

@jkczyz jkczyz left a comment

Choose a reason for hiding this comment

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

Linting CI failures look unrelated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants