Skip to content

Add storage for forwarded payments - #772

Merged
tnull merged 3 commits into
lightningdevkit:mainfrom
benthecarman:save-fwd-payment
Sep 14, 2026
Merged

tnull merged 3 commits into
lightningdevkit:mainfrom
benthecarman:save-fwd-payment

Conversation

@benthecarman

Copy link
Copy Markdown
Contributor

Routing nodes and LSPs want to track forwarded payments so they can run accounting on fees earned and track profitability across time. We now store these to make it easier to track and allows for future accounting utils in the future.

This shouldn't effect edge user nodes as they should never be forwarding payments.

Implementation is mostly just copied how we currently handle normal payments and adapted for forwarded payments.

@benthecarman
benthecarman requested a review from tnull January 27, 2026 17:33
@ldk-reviews-bot

ldk-reviews-bot commented Jan 27, 2026

Copy link
Copy Markdown

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

@tnull tnull left a comment

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.

Thanks! I do wonder if we should really enable storing all forwards by default, or rather make this optional.

Also more generally I wonder if users really expect us to store all forwarded payments forever, or if we should only keep the last X entries in the store? Also, with general-purpose HTLC interception coming up, maybe storing forwards might even be something we entirely want to leave entirely to the user after all?

What do you think?

Comment thread src/io/utils.rs Outdated
Comment thread src/io/utils.rs Outdated
Comment thread src/payment/store.rs Outdated
@benthecarman

Copy link
Copy Markdown
Contributor Author

Addressed comments.

I think this makes sense to include in ldk-node and not just leaving it up to the user. If we are focusing on LSPs this will be an essential feature, especially if we want to add accounting tools down the line.

I think it can make sense to disable this and/or add a function to prune the storage for it. Maybe just an option that tracks totals per channel rather than individual htlcs

Comment thread src/lib.rs Outdated
@benthecarman benthecarman self-assigned this Jan 29, 2026
@benthecarman
benthecarman force-pushed the save-fwd-payment branch 3 times, most recently from 970a395 to c3f7714 Compare February 4, 2026 21:48

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

IMO storing granular forwarding information long-term is dangerous. Instead, can we store information that allows for easier compaction? eg total forwarding on a per-X basis between channel pairs?

@tnull

tnull commented Feb 5, 2026

Copy link
Copy Markdown
Collaborator

IMO storing granular forwarding information long-term is dangerous. Instead, can we store information that allows for easier compaction? eg total forwarding on a per-X basis between channel pairs?

Agree it's not great to store everything forever. However, it's also hard to guess which values users are interested in. For example, one metric they def. want to see is fee revenue, and I suspect they might even want individual values for each payment forwarded rather than aggregated numbers.

@benthecarman

Copy link
Copy Markdown
Contributor Author

I started working on a version where you set an interval, say 12 hours, and we store individual events and after the time has lapsed we combine them into a single entry. Does that sound good?

@TheBlueMatt

Copy link
Copy Markdown
Contributor

I suspect they might even want individual values for each payment forwarded rather than aggregated numbers.

Yea, I kinda wonder what kind of aggregate stats they might want. Pairwise totals and counts over discrete time horizons suffices for things like "average forwarded per payment over this channel pair", but median is of course trickier. Maybe that's okay?

@TheBlueMatt

Copy link
Copy Markdown
Contributor

I started working on a version where you set an interval, say 12 hours, and we store individual events and after the time has lapsed we combine them into a single entry. Does that sound good?

I think its totally fair to store the last N hours of individual forwards, yea! After that question is format.

@benthecarman
benthecarman force-pushed the save-fwd-payment branch 2 times, most recently from 19c5b89 to cbb235e Compare February 6, 2026 01:26
@benthecarman

Copy link
Copy Markdown
Contributor Author

Made it so the Detailed mode now only stores for a configured time period and will aggregate them into channel pair stats

@tnull

tnull commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator

It seems this should be unblocked by now, but needs a considerable rebase?

@benthecarman

Copy link
Copy Markdown
Contributor Author

Rebased and updated for pagination.

However probably worth holding off on until https://git.rust-bitcoin.org/lightningdevkit/rust-lightning/issues/4766 is fixed

@tnull

tnull commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

Rebased and updated for pagination.

However probably worth holding off on until https://git.rust-bitcoin.org/lightningdevkit/rust-lightning/issues/4766 is fixed

Ugh, okay, that means that the fix needs to happen for 0.3 still then.

@tnull

tnull commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Please rebase now that #1024 landed.

@tnull tnull left a comment

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.

Thanks!

Some comments, I think the two main things remaining is that we still don't have a way to handle replayed events and that we should probably reuse DataStore for this (fwiw, there are a few fixed bugs in DiskStore that we can just avoid when we drop the fork/DRY up the code).

Comment thread src/io/mod.rs Outdated
pub(crate) const PENDING_PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE: &str = "pending_payments";
pub(crate) const PENDING_PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE: &str = "";

/// The forwarded payment information will be persisted under this prefix.

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.

Shouldn't all forwarded-payment-store related objects be stored under the same primary namespace, and be discerned by secondary?

Comment thread src/payment/forwarding_store.rs Outdated
pub(crate) const FORWARDED_PAYMENT_AGGREGATION_BUCKET_SIZE_SECS: u64 = 60 * 60;

/// A disk-backed store for forwarding data that is too large to keep in memory.
pub(crate) struct DiskStore<SO: StorableObject> {

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.

I don't understand why we need this? The idea of DataStore being generic is exactly to avoid adding specialized stores for every instance?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed, goal was to not cache all these and just write to store

Comment thread src/data_store.rs Outdated
Comment thread src/event.rs Outdated
}
}

for inbound_stats in inbound_stats_by_channel.into_values() {

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.

Hmm, @TheBlueMatt recently indicated the HTLC ID was added to PaymentForwarded, but it still doesn't seem to be the case. So AFAIU we have no idempotency token available here. Begs the question if we're fine shipping this with a known bug or how we'd want to deal with this.

Comment thread src/payment/forwarding.rs Outdated
@benthecarman

Copy link
Copy Markdown
Contributor Author

Addressed most review comments, however, can hold off until https://git.rust-bitcoin.org/lightningdevkit/rust-lightning/pulls/4912 is merged

@tnull tnull left a comment

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.

Addressed most review comments, however, can hold off until https://git.rust-bitcoin.org/lightningdevkit/rust-lightning/pulls/4912 is merged

Cool. That should land shortly, though we'll also need a backport PR to land. That said, it seems all fixups now happened in the second commit. Can you split all changes to DataStore out to 1-2 prefactor commits, also to avoid switching to UpdatableObject and back?

Comment thread src/builder.rs Outdated
Comment thread src/event.rs Outdated
Comment thread src/data_store.rs Outdated
/// Returns whether this store contains no objects.
pub(crate) async fn is_empty(&self) -> Result<bool, Error> {
let mut page_token = None;
loop {

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.

Why do we need this loop here? That seems just like a risk that we eventually could add a bug that loops forever? Whether the store is empty should be a simple O(1) decision based on the first response, no?

Comment thread src/data_store.rs Outdated
}

/// Returns all stored objects matching `f` by reading each page from the backing store.
pub(crate) async fn list_filter_from_store<F: FnMut(&&SO) -> bool>(

@tnull tnull Aug 21, 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.

No, one of the main design goals (i.e., why we introduced the typestate pattern and dropped list_payments_with_filter) of the recent refactor was to disallow any internal callers from making prohibitively expensive listing calls. For a long running node this will be insanely expensive as it always interatively walks the entire uncached store. We can't do this, IMO hence have to drop this and all dependent callers.

@tnull

tnull commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

Needs a rebase now.

@benthecarman
benthecarman force-pushed the save-fwd-payment branch 3 times, most recently from 449c831 to 2397afd Compare August 21, 2026 21:01
@benthecarman
benthecarman requested a review from tnull August 31, 2026 05:01
StorableObject required an update representation from every type a
store holds. Stores that only read, write, and delete whole objects
still had to supply update methods that no caller used.

Move merge behavior to UpdatableObject. Keep DataStore available to all
storable objects, and restrict only merge operations to updatable
objects.

AI-assisted-by: OpenAI Codex and Anthropic Fable
Add a cache policy that reads objects from persistent storage without
keeping them in memory. Add a key-only empty check for namespaces that
can grow without a bounded working set.

AI-assisted-by: OpenAI Codex and Anthropic Fable
Store unambiguous single-HTLC forwarding events. Aggregate them into
per-channel and channel-pair statistics.

Use fixed one-hour buckets for detailed records. Keep details out of the
payment LRU cache. Use one persistence namespace for forwarding data.

Expose analytics through Rust and UniFFI. Keep forwarding persistence
and event-recording logic behind one internal store.

AI-assisted-by: OpenAI Codex and Anthropic Fable
@tnull
tnull merged commit 1ce9cad into lightningdevkit:main Sep 14, 2026
35 of 37 checks passed
@benthecarman
benthecarman deleted the save-fwd-payment branch September 14, 2026 11:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants