Add storage for forwarded payments #772
Conversation
|
👋 Thanks for assigning @tnull as a reviewer! |
1feb7ae to
ba15c40
Compare
tnull
left a comment
There was a problem hiding this comment.
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?
ba15c40 to
20877bd
Compare
|
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 |
| } | ||
|
|
||
| /// Retrieves all forwarded payments. | ||
| pub fn list_forwarded_payments(&self) -> Vec<ForwardedPaymentDetails> { |
There was a problem hiding this comment.
This makes we wonder if we should wait for lightningdevkit/rust-lightning#4347 (and some follow-up PRs for implementations here ) to land so this can directly use pagination from the start?
There was a problem hiding this comment.
I figured we weren't doing a release until the next ldk release so that'd be a given but can hold off on it if that's not the case
There was a problem hiding this comment.
Yeah, given that the PaginatedKVStore is already pretty far along, let's wait on in here. Thankfully we can then directly bump the LDK dependency.
970a395 to
c3f7714
Compare
TheBlueMatt
left a comment
There was a problem hiding this comment.
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. |
|
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? |
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? |
I think its totally fair to store the last N hours of individual forwards, yea! After that question is format. |
19c5b89 to
cbb235e
Compare
|
Made it so the |
cbb235e to
ad559cc
Compare
|
It seems this should be unblocked by now, but needs a considerable rebase? |
ad559cc to
d8a4671
Compare
|
Rebased and updated for pagination. However probably worth holding off on until https://git.rust-bitcoin.org/lightningdevkit/rust-lightning/issues/4766 is fixed |
d8a4671 to
a831210
Compare
Ugh, okay, that means that the fix needs to happen for 0.3 still then. |
a831210 to
f1c31b4
Compare
|
Rebased and now uses the updated |
f1c31b4 to
81249d0
Compare
81249d0 to
6319183
Compare
tnull
left a comment
There was a problem hiding this comment.
Needs a rebase, some comments.
| } | ||
|
|
||
| impl StorableObjectId for PaymentId { | ||
| fn encode_to_hex_str(&self) -> String { |
There was a problem hiding this comment.
I'm a bit dubious why we need this refactor? Can you elaborate?
There was a problem hiding this comment.
ChannelPairStatsId is 80 bytes, so its hex would be 160 characters, which is larger than the KVStore key limit of 120. For ChannelPairStatsId I used base64 instead, which is 107 characters so it can fit. Couldn't really trim ChannelPairStatsId either
| }, | ||
| }); | ||
|
|
||
| uniffi::custom_type!(ForwardedPaymentId, String, { |
There was a problem hiding this comment.
Would be good to avoid the custom types if we can. Note that they are error prone as if conversion fails in a path where Error can't be emitted (i.e., we can't bubble up a Result<X, Error>), it will have the node panic. While most callsites should be safe, we therefore still aim to reduce the number of custom type implementations over time.
| Detailed { | ||
| /// The length of each aggregation bucket, in minutes. Must be non-zero. | ||
| /// | ||
| /// The first detailed configuration persists this value. Later detailed configurations for |
There was a problem hiding this comment.
This seems like an error prone API. If users can't change this config after the fact we maybe shouldn't allow them to configure it in the first place or provide an API design that communicates that outside of docs (who users tend to read after the fact).
There was a problem hiding this comment.
Switched to a fixed retention time
| }]); | ||
| } | ||
|
|
||
| // LDK reports the incoming and outgoing HTLCs, but not which incoming HTLC funded each |
There was a problem hiding this comment.
It seems like here we're trying to re-establish an API contract that LDK doesn't provide, and it seems error prone. Can we avoid this? If we think this matching is important we might need to change upstream first?
There was a problem hiding this comment.
There isn't a mapping of the sats for when we receive them to when we send them, they are all in a single batch. I did FIFO because it's a simple way to split the amounts while keeping the totals correct. I don't know if this is something we really can upstream in ldk because there really is no "pairs" its just one batch of htlcs coming in and going out.
| let channel_pair_stats_store = Arc::clone(&self.channel_pair_forwarding_stats_store); | ||
| let logger = Arc::clone(&self.logger); | ||
| self.runtime.spawn_cancellable_background_task(async move { | ||
| run_forwarded_payment_aggregation( |
There was a problem hiding this comment.
I'm not sure aggregating stats needs its own background task? I mean they are relatively cheap, but there's still some overhead. Do we think doing it on-demand would be very costly?
Or maybe, if we want to do it in the background, doing some prefactoring to have some of these tasks merged into a single 'maintenance task' could make sense (as we have a few by now).
There was a problem hiding this comment.
I'd rather not do it on-demand, that always feels flaky and confusing to me, especially when handling potential storage errors.
I think a maintenance task could make sense as a follow up
| } | ||
| } | ||
|
|
||
| for inbound_stats in inbound_stats_by_channel.into_values() { |
There was a problem hiding this comment.
Codex:
- [P1] Forwarding accounting is not idempotent across event replay.
The handler performs additive channel-stat updates and creates detailed records with fresh random IDs before all later persistence—including the user event queue write—has succeeded. If a later write fails, ReplayEvent causes the same forward to be processed again, doubling counts,
amounts, and fees. LDK also explicitly permits duplicate PaymentForwarded events when the fee is unknown. See /home/tnull/worktrees/ldk-node/pr-772-latest-20260721/src/event.rs:1769, /home/tnull/worktrees/ldk-node/pr-772-latest-20260721/src/payment/store.rs:1419, and LDK’s event contract
(https://github.com/lightningdevkit/rust-lightning/blob/f680fd37d8bea69383e1b18730423f347e6a8341/lightning/src/events/mod.rs#L1489-L1514). A stable idempotency marker or atomic persistence strategy is needed.
True, though unclear how to tackle without an idempotency marker / event ID.
There was a problem hiding this comment.
yeah i dont really know a way around this unless we add the payment hash to the forwarded event
Update LDK and payment instructions to compatible revisions and adapt forwarded and unified payment handling to their API changes. Keep both native and UniFFI test targets compiling. AI-assisted-by: OpenAI Codex
Allow storable objects to choose compact non-hexadecimal key encodings. Forwarding statistics use this to keep composite identifiers within the backing KVStore's key-length limit. AI-assisted-by: OpenAI Codex
Store individual forwarding events and aggregate them into channel and channel-pair statistics for fee and profitability tracking. Use fixed one-hour buckets for detailed records and expose their identifiers as opaque strings. Infer channel-pair allocations for multi-HTLC forwards by matching incoming and outgoing amounts in FIFO order. AI-assisted-by: OpenAI Codex
6319183 to
4a2b945
Compare
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.