-
Notifications
You must be signed in to change notification settings - Fork 438
Full Trampoline Support #4414
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
Draft
carlaKC
wants to merge
68
commits into
lightningdevkit:main
Choose a base branch
from
carlaKC:2299-end-to-end
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Full Trampoline Support #4414
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
We previously assumed background events would eventually be processed prior to another `ChannelManager` write, so we would immediately remove all in-flight monitor updates that completed since the last `ChannelManager` serialization. This isn't always the case, so we now keep them all around until we're ready to handle them, i.e., when `process_background_events` is called. This was discovered while fuzzing `chanmon_consistency_target` on the main branch with some changes that allow it to connect blocks. It was triggered by reloading the `ChannelManager` after a monitor update completion for an outgoing HTLC, calling `ChannelManager::best_block_updated`, and reloading the `ChannelManager` once again. A test is included that provides a minimal reproduction of this case.
At various points we've been stuck in our TLV read/write variants but just want to break out and write some damn code to initialize a field and some more code to decide what to write for a TLV. We added the write-side part of this with the `legacy` TLV read/write variant, but its useful to also be able to specify a function which is called on the read side. Here we add a `custom` TLV read/write variant which calls a method both on read and write to either decide what to write or to map a read value (if any) to the final field.
When `OutboundPayments` calls the provided `Router` to fetch a `Route` it passes a `RouteParameters` with a specific max-fee. Here we validate that the `Route` returned sticks to the limits provided, and also that it meets the MPP rules of not having any single MPP part which can be removed while still meeting the desired payment amount.
In some uses of LDK we need the ability to send HTLCs for only a portion of some larger MPP payment. This allows payers to make single payments which spend funds from multiple wallets, which may be important for ecash wallets holding funds in multiple mints or graduated wallets which hold funds across a trusted wallet and a self-custodial wallet. In order to allow for this, we need to separate the concept of the payment amount from the onion MPP amount. Here we start this process by adding a `total_mpp_amount_msat` field to `RecipientOnionFields` (which is the appropriate place for a field describing something in the recipient onion). We currently always assert that it is equal to the existing fields, but will relax this in the coming commit(s). We also start including a payment preimage on probe attempts, which appears to have been the intent of the code, but which did not work correctly. The bulk of the test updates were done by Claude.
In some uses of LDK we need the ability to send HTLCs for only a portion of some larger MPP payment. This allows payers to make single payments which spend funds from multiple wallets, which may be important for ecash wallets holding funds in multiple mints or graduated wallets which hold funds across a trusted wallet and a self-custodial wallet. In the previous commit we added a new field to `RecipientOnionFields` to describe the total value of an MPP payment. Here we start using this field when building onions, dropping existing arguments to onion-building methods.
In some uses of LDK we need the ability to send HTLCs for only a portion of some larger MPP payment. This allows payers to make single payments which spend funds from multiple wallets, which may be important for ecash wallets holding funds in multiple mints or graduated wallets which hold funds across a trusted wallet and a self-custodial wallet. In the previous commits we moved the total-MPP-value we set in onions from being manually passed through onion-building to passing it via `RecipientOnionFields`. This introduced a subtle bug, though - payments which are retried will get a fresh `RecipientOnionFields` built from the data in `PendingOutboundPayment::Retryable`, losing any custom total-MPP-value settings and causing retries to fail. Here we fix this by storing the total-MPP-value directly in `PendingOutboundPayment::Retryable`.
In some uses of LDK we need the ability to send HTLCs for only a portion of some larger MPP payment. This allows payers to make single payments which spend funds from multiple wallets, which may be important for ecash wallets holding funds in multiple mints or graduated wallets which hold funds across a trusted wallet and a self-custodial wallet. In the previous few commits we added support for making these kinds of payments when using the payment methods which explicitly accepted a `RecipientOnionFields`. Here we also add support for such payments made via the `pay_for_bolt11_invoice` method, utilizing the new `OptionalBolt11PaymentParams` to hide the parameter from most calls. Test mostly by Claude
When we receive an HTLC as a part of a claim, we validate that the CLTV on the HTLC is >= the CLTV that the sender requested we receive, but then we use the CLTV value that the sender requested we receive as the deadline to claim the HTLC anyway. This isn't generally all that interesting (they're always the same unless the previous-hop node gave us "free CLTV"), but for trampoline payments where we're both a trampoline hop and the blinded intro point and the recipient, it means we end up allowing ourselves less claim time than we actually have. Instead, here, we just use the actual HTLC CLTV deadline.
The docs for `RouteHop::cltv_expiry_delta` claim that it includes any trampoline hops, but the way we actually implemented onion building it did not. Because the docs described a simpler and more backwards-compatible API, we update the onion-building logic to match rather than updating the docs.
Now that we've cleaned up trampoline CLTV building and added `Path::total_cltv_expiry_delta`, we can use both to do some basic validation of CLTV values on blinded tails in `Route::debug_assert_route_meets_params`
Now that we are consistently using the `RouteHop::cltv_expiry_delta` as the last hop's starting CLTV rather than summing trampoline hops, `starting_htlc_offset` is a bit confusing - its actually always the current block height. Thus, here we rename it.
In the commits that follow, we want to be able to free the other channel without emitting an event so that we can emit a single event for trampoline payments with multiple incoming HTLCs. We still want to go through the full claim flow for each incoming HTLC (and persist the EmitEventAndFreeOtherChannel event to be picked up on restart), but do not want multiple events for the same trampoline forward. Changing from upgradable_required to upgradable_option is forwards compatible - old versions of the software will always have written this field, newer versions don't require it to be there but will be able to read it as-is. This change is not backwards compatible, because older versions of the software will expect the field to be present but newer versions may not write it. An alternative would be to add a new event type, but that would need to have an even TLV (because the event must be understood and processed on restart to claim the incoming HTLC), so that option isn't backwards compatible either.
In preparation for trampoline failures, allow multiple previous channel ids. We'll only emit a single HTLCHandlingFailed for all of our failed back HTLCs, so we want to be able to express all of them in one event.
This commit adds a SendHTLCId for trampoline forwards, identified by their session_priv. As with an OutboundRoute, we can expect our HTLC to be uniquely identified by a randomly generated session_priv. TrampolineForward could also be identified by the set of all previous outbound scid/htlc id pairs that represent its incoming HTLC(s). We choose the 32 byte session_priv to fix the size of this identifier rather than 16 byte scid/id pairs that will grow with the number of incoming htlcs.
We only have payment details for HTLCSource::TrampolineForward available once we've dispatched the payment. If we get to the stage where we need a HTLCId for the outbound payment, we expect dispatch details to be present. Co-authored-by: Arik Sosman <git@arik.io> Co-authored-by: Maurice Poirrier <mpch@hey.com>
To create the right handling type based on source, add a helper. This is mainly useful for PreviousHopData/TrampolineForward. This helper maps an OutboundRoute to a HTLCHandlingFailureType::Forward. This value isn't actually used once we reach `forward_htlc_backwards_internal`, because we don't emit `HTLCHandlingFailed` events for our own payments. This issue is pre-existing, and could be addressed with an API change to the failure function, which is left out of scope of this work.
When we receive trampoline payments, we first want to validate the values in our outer onion to ensure that we've been given the amount/ expiry that the sender was intending us to receive to make sure that forwarding nodes haven't sent us less than they should.
Use even persistence value because we can't downgrade with a trampoline payment in flight, we'll fail to claim the appropriate incoming HTLCs. We track previous_hop_data in `TrampolineForwardInfo` so that we have it on hand in our `OutboundPayment::Retryable`to build `HTLCSource` for our retries.
When we are forwading as a trampoline within a blinded path, we need to be able to set a blinding point in the outer onion so that the next blinded trampoline can use it to decrypt its inner onion. This is only used for relaying nodes in the blinded path, because the introduction node's inner onion is encrypted using its node_id (unblinded) pubkey so it can retrieve the path key from inside its trampoline onion. Relaying nodes node_id is unknown to the original sender, so their inner onion is encrypted with their blinded identity. Relaying trampoline nodes therefore have to include the path key in the outer payload so that the inner onion can be decrypted, which in turn contains their blinded data for forwarding. This isn't used for the case where we're the sending node, because all we have to do is include the blinding point for the introduction node. For relaying nodes, we just put their encrypted data inside of their trampoline payload, relying on nodes in the blinded path to pass the blinding point along.
When we're a forwarding trampoline and we receive a final error from our route, we want to propagate that failure back to the original sender. Surface the information so that it's available to us.
- [ ] Check whether we can get away with checking path.hops[0] directly (outbound_payment should always be present?)
When we settle a trampoline forward, we'd need to look up all of our incoming/outgoing htlc amounts to calculate the fee we've earned. Instead, we just look it up in our outbound payments. Note that here we report the minimum fee that we charged for the forward. It's possible that the rest of the trampoline route was under its allowed budget, and we earned more than our required fee.
We're going to need to keep track of our trampoline HLTCs in the same way that we keep track of incoming MPP payment to allow them to accumulate on our incoming channel before forwarding them onwards to the outgoing channel. To do this we'll need to store the payload values we need to remember for forwarding in OnionPayload. - [ ] Readable for ClaimableHTLC is incomplete
When we are a trampoline router, we need to accumulate incoming HTLCs (if MPP is used) before forwarding the trampoline-routed outgoing HTLC(s). This commits adds a new map in channel manager, and mimics the handling done for claimable_payments. This map is not placed in claimable_payments because we'll need to be able to lock pending_outbound_payments in the commits that follow while holding a lock on our set of trampoline payments (which is not possible with claimable_payments). - [ ] Need to add persistence of trampoline payments
We're going to use the same logic for trampoline and for incoming MPP payments, so we pull this out into a separate function.
We'll only use this for non-trampoline incoming accumulated htlcs, because we want different source/failure for trampoline.
- [ ] Need to return real errors here - [ ] Need to be able to handle committed_to_claimable if we fail
- [ ] Not sure what this does, look into it
For trampoline payments, we don't want to enforce a minimum cltv delta between our incoming and outer onion outgoing CLTV because we'll calculate our delta from the inner trampoline onion's value. However, we still want to check that we get at least the CLTV that the sending node intended for us and we still want to validate our incoming value. Refactor to allow setting a zero delta, for use for trampoline payments.
Remove error that was added to prevent forwarding of trampoline payments during development, and instead process incoming trampoline htlcs. We can't perform proper validation because we don't know the outgoing channel id until we forward the HTLC, so we just perform a basic CLTV check.
The blinding point that we pass in is supposed to be the "update add" blinding point equivalent, which in blinded trampoline relay is the one that we get in the outer onion.
Don't always blindly replace with a manually built test onion when we run trampoline tests (only for unblinded / failure cases where we need to mess with the onion). The we update our replacement onion logic to correctly match our internal behavior which adds one block to the current height when dispatching payments.
- [ ] Right now, we assume that the presence of a trampoline means
that we're in a blinded route. This fails when we test an
unblinded case (which we do to get coverage for forwarding).
We likely need to decouple trampoline and blinded tail to allow
this to work properly.
|
👋 Hi! I see this is a draft PR. |
This was referenced Feb 12, 2026
Closed
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
This PR contains all the changes to support trampoline forwarding in LDK.
It depends on #4304 and #4402 (and thus also #4373).
This obviously needs to be broken up into part, and could certainly use some cleaning up, but opening it up early to provide some context for the decisions make in #4304.