Skip to content

Fix double-forward, prefer legacy forward maps - #4289

Merged
TheBlueMatt merged 6 commits into
lightningdevkit:mainfrom
valentinewallace:2025-12-reconstruct-fwds-followup
Jan 13, 2026
Merged

Fix double-forward, prefer legacy forward maps#4289
TheBlueMatt merged 6 commits into
lightningdevkit:mainfrom
valentinewallace:2025-12-reconstruct-fwds-followup

Conversation

@valentinewallace

@valentinewallacevalentinewallace commented Dec 16, 2025

Copy link
Copy Markdown
Contributor

Addresses a chunk of the feedback from #4227 (review) (tracked in #4280). Splitting it out for ease of review. Partially addresses #4286.

  • Fix a bug that would cause double-forwarding of inbound HTLCs when using reconstructed forward maps
  • Prefer legacy forward maps in production while randomly using reconstructed maps in tests for coverage (see commit message)
  • a few other nits from the aforementioned review

@ldk-reviews-bot

ldk-reviews-bot commented Dec 16, 2025

Copy link
Copy Markdown

👋 Thanks for assigning @joostjager 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.

@valentinewallace

Copy link
Copy Markdown
ContributorAuthor

Going to take another look at this tomorrow before un-drafting it

@codecov

codecovBot commented Dec 17, 2025

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 74.47917% with 49 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.59%. Comparing base (c9f022b) to head (5a4912c).
⚠️ Report is 16 commits behind head on main.

Files with missing linesPatch %Lines
lightning/src/ln/channelmanager.rs74.73%43 Missing and 5 partials ⚠️
lightning/src/ln/channel.rs50.00%0 Missing and 1 partial ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #4289 +/- ##
=======================================
Coverage 86.58% 86.59% =======================================
Files 158 158 Lines 102287 102368 +81 Branches 102287 102368 +81 =======================================
+ Hits 88568 88644 +76 - Misses 11304 11311 +7 + Partials 2415 2413 -2 
FlagCoverage Δ
fuzzing35.95% <15.42%> (-0.90%)⬇️
tests85.87% <74.47%> (+<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.

@valentinewallace
valentinewallaceforce-pushed the 2025-12-reconstruct-fwds-followup branch from 89f5d07 to c6bb096CompareDecember 17, 2025 20:25
Comment threadlightning/src/ln/channelmanager.rs Outdated
@valentinewallace
valentinewallaceforce-pushed the 2025-12-reconstruct-fwds-followup branch from c6bb096 to 425747eCompareDecember 17, 2025 21:13
Comment threadlightning/src/ln/channelmanager.rs
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/reload_tests.rs
Comment threadlightning/src/ln/channelmanager.rs Outdated
(17, in_flight_monitor_updates, option),
(19, peer_storage_dir, optional_vec),
(21, WithoutLength(&self.flow.writeable_async_receive_offer_cache()), required),
(23, reconstruct_manager_from_monitors, required),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Oops sorry for the delay. Writing a bool here to determine whether to look at data we're always writing seems quite weird? I'm not sure what the right answer is, but ideally we run tests with both the new and old code. In the past (with block connection) we've taken a somewhat hacky approach of just flipping a coin and using a random value to decide. In general its worked and we haven't seen many cases of flaky tests making their way upstream, but its a bit more annoying for devs. Still, absent a better option (I'm not a huge fan of running the entire test suite twice every time, even running it an extra time in CI kinda sucks...) that seems reasonable.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Agreed, I went with the random option you mention.

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.

We are now still running the entire test suite twice, but not on the same CI run. I think better options are:

  • Deliberately pick a few tests that sufficient cover the logic, and only run those twice.
  • Do a more extensive test matrix nightly.

@valentinewallacevalentinewallaceJan 8, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I feel more comfortable with the entire test suite running with the new vs old code rather than selected tests, but adding a whole extra CI run to an already slow CI does suck IMO. So I like this current tradeoff.

How about another environment variable? lol

@joostjagerjoostjagerJan 8, 2026

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 think that there will be much more of this for the chan mgr refactor. My draft PRs also assume a safe_channels feature flag and an associated CI job. My preference would be to use the same mechanism here, and accept that while the project is underway, we'll have an additional CI job (a single job for all PRs in the project) that only runs on a single platform. It's not that significant.

@valentinewallace
valentinewallaceforce-pushed the 2025-12-reconstruct-fwds-followup branch 3 times, most recently from 7b858c1 to 71062f2CompareJanuary 7, 2026 20:42
@valentinewallace

Copy link
Copy Markdown
ContributorAuthor

Addressed feedback, main diff is here. Also pushed some whitespace fixes after.

@valentinewallace
valentinewallace marked this pull request as ready for review January 7, 2026 20:44
@valentinewallace
valentinewallace requested review from TheBlueMatt and joostjager and removed request for wpaulinoJanuary 7, 2026 20:49
// to ensure the legacy codepaths also have test coverage.
#[cfg(not(test))]
let reconstruct_manager_from_monitors = false;
#[cfg(test)]

@joostjagerjoostjagerJan 8, 2026

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.

What happened to the idea of using the safe_channels flag here, so that we can gate this and all other changes in the chan mgr refactor project, and make it worth doing a separate CI run for it?

Using conditional compilation for the legacy code (not safe_channels) might improve readability. I noticed that I did had to pay some attention to the control flow interventions with for example continue statements when reconstructing. Also makes it easier to delete that code eventually.

Comment on lines +17953 to +17956
#[cfg(not(test))]
let reconstruct_manager_from_monitors = false;
#[cfg(test)]
let reconstruct_manager_from_monitors = {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

@TheBlueMatt pointed out that the way this is currently structured, a future version of LDK that does not write the pending_intercepted_htlcs/forward_htlcs maps will not be able to downgrade to this version of the code, because it only runs the reconstruction logic in tests.

So instead of running reconstruction logic in tests only, we should consider running it if the manager's written version is >= X, where X is a future version where we can assume that the new data is always present and the old data stopped being written.

I'm not sure what version that would be, and I also think we can hold off on this a little until we look into reconstructing more maps. Otherwise we might have some additional complexity (i.e. one var for reconstruct_fwd_maps_from_monitors if version > X, one for reconstruct_claimable_map_from_monitors if version > Y, etc). But worth thinking about and incorporating into upcoming PRs.

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.

How can we add a conditional like that if we don't know yet which version the new data is always present?

@valentinewallacevalentinewallace self-assigned this Jan 8, 2026
@valentinewallacevalentinewallace moved this to Goal: Merge in Weekly GoalsJan 8, 2026
@valentinewallacevalentinewallace added the weekly goal Someone wants to land this this week label Jan 8, 2026
@valentinewallace
valentinewallaceforce-pushed the 2025-12-reconstruct-fwds-followup branch from 71062f2 to f1ea7bbCompareJanuary 8, 2026 19:01
@valentinewallace

Copy link
Copy Markdown
ContributorAuthor

Discussed offline, going to add an environmental variable to set which manager reconstruction paths to use. Rebased on main to get the changes from #4296

@valentinewallace
valentinewallaceforce-pushed the 2025-12-reconstruct-fwds-followup branch from f1ea7bb to f245139CompareJanuary 8, 2026 19:31
@valentinewallace

Copy link
Copy Markdown
ContributorAuthor

Added the environment variable: diff

@joostjagerjoostjager left a comment

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.

As mentioned before, I reluctantly accept the test input randomization and the reload boolean.

The only thing I'd like to be sure of that I really understand is the upgrade/downgrade plan that was landed on. Ideally the new data would be written and used in the next release. Postponing that another release is a big decision and it seems there is not that much gained from it except for removal of some code that already exists in main.

Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs
// persist that state, relying on it being up-to-date on restart. Newer versions are moving
// towards reducing this reliance on regular persistence of the `ChannelManager`, and instead
// reconstruct HTLC/payment state based on `Channel{Monitor}` data if
// `reconstruct_manager_from_monitors` is set below. Currently it is only set in tests, randomly

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.

Bringing everything together, would the upgrade/downgrade situation look like this?

VersionReadWriteUpgrade to version (max)Downgrade to version (min)
0.2legacylegacy0.5-
0.3legacylegacy+new0.60.2
0.4legacy+newlegacy+new0.60.2
0.5legacy+newnew0.60.4
0.6newnew-0.4

Also wondering - in relation to the discussion in yesterday's sync meet - how the reconstruct_manager_from_monitors makes for a faster path?

@TheBlueMattTheBlueMattJan 11, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

would the upgrade/downgrade situation look like this?

My understanding is that our goal is that 0.3 would support reading new as well, so that 0.5 can downgrade to 0.3 rather than 0.4.

Also wondering - in relation to the discussion in yesterday's sync meet - how the reconstruct_manager_from_monitors makes for a faster path?

So that we get the above. I don't see a reason to want to only allow 0.5 to downgrade to 0.4 rather than 0.3. The code currently doesn't do that but presumably in a followup we could do that?

@joostjagerjoostjagerJan 12, 2026

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.

@valentinewallace and I discussed this more. It seems that having a dedicated flag for signaling that the old maps are not written is better than guessing at a future version number where this is the case.

This currently assumes we'll skip one version before merging the final changes (new read and new write only), but the flexibility remains to wait more versions and enlarge the downgrade window.

VersionReadWriteUpgrade to version (max)Downgrade to version (min)Set flag
0.2 (current)legacylegacy0.4any
0.3legacy OR new with flaglegacy+newanyany
0.4legacy OR new with flaglegacy+newanyany
0.5newnewany0.3X

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It seems that having a dedicated flag for signaling that the old maps are not written is better than guessing at a future version number where this is the case.

You mean instead of using SERIALIZATION_VERSION/MIN_SERIALIZATION_VERSION constants you want to use a TLV? I guess that's fine, but it seems much simpler to use the version numbers so that we can also drop some of the legacy crap that is written as non-TLVs that we'll never be writing anymore. Don't really see a reason to avoid that.

The upgrade path looks right to me, though.

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 is not a great idea to assume things about a specific future version number.

@joostjagerjoostjagerJan 14, 2026

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.

Would that look like this?

VersionReadWriteUpgrade to version (max)Downgrade to version (min)SER_VERMIN_SER_VER
0.2 (current)legacylegacy0.4any11
0.3legacy OR new with flaglegacy+newanyany21
0.4legacy OR new with flaglegacy+newanyany21
0.5newnewany0.332

We are wondering what happens if we need to bump in between for a different reason.

There is also the issue that the new format is really a per-htlc format change, not overall chan mgr version.

Comment on lines +17953 to +17956
#[cfg(not(test))]
let reconstruct_manager_from_monitors = false;
#[cfg(test)]
let reconstruct_manager_from_monitors = {

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.

How can we add a conditional like that if we don't know yet which version the new data is always present?

Comment threadlightning/src/ln/channelmanager.rs
@joostjager

Copy link
Copy Markdown
Contributor

You may want to update the PR title and description with details

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @TheBlueMatt! 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
Copy Markdown

🔔 2nd Reminder

Hey @TheBlueMatt! 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.

@valentinewallacevalentinewallace changed the title Follow-ups to #4227 (Part 1)Fix double-forward, prefer legacy forward mapsJan 12, 2026
@valentinewallace
valentinewallaceforce-pushed the 2025-12-reconstruct-fwds-followup branch from f245139 to 17b562cCompareJanuary 12, 2026 21:02
Necessary for the next commit and makes it easier to read.
We recently began reconstructing ChannelManager::decode_update_add_htlcs on
startup, using data present in the Channels. However, we failed to prune HTLCs
from this rebuilt map if a given HTLC was already forwarded to the outbound
edge (we pruned correctly if the outbound edge was a closed channel, but not
otherwise). Here we fix this bug that would have caused us to double-forward
inbound HTLC forwards.
No need to iterate through all entries in the map, we can instead pull out the
specific entry that we want.
We are working on removing the requirement of regularly persisting the
ChannelManager, and as a result began reconstructing the manager's forwards
maps from Channel data on startup in a recent PR, see
cb398f6 and parent commits.
At the time, we implemented ChannelManager::read to prefer to use the newly
reconstructed maps, partly to ensure we have test coverage of the new maps'
usage. This resulted in a lot of code that would deduplicate HTLCs that were
present in the old maps to avoid redundant HTLC handling/duplicate forwards,
adding extra complexity.
Instead, always use the old maps in prod, but randomly use the newly
reconstructed maps in testing, to exercise the new codepaths (see
reconstruct_manager_from_monitors in ChannelManager::read).
@valentinewallace
valentinewallaceforce-pushed the 2025-12-reconstruct-fwds-followup branch from 17b562c to 5a4912cCompareJanuary 12, 2026 21:15

@joostjagerjoostjager left a comment

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.

Assuming the upgrade story will be refined in the follow up, all my comments have been addressed.

@TheBlueMatt
TheBlueMatt merged commit c5d7b13 into lightningdevkit:mainJan 13, 2026
19 of 20 checks passed
@github-project-automationgithub-project-automationBot moved this from Goal: Merge to Done in Weekly GoalsJan 13, 2026
@TheBlueMattTheBlueMatt mentioned this pull request Jan 14, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

weekly goalSomeone wants to land this this week

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants

@valentinewallace@ldk-reviews-bot@joostjager@TheBlueMatt