swaps: accept MPP out-swap events - #737
Conversation
Advertise optional MPP support on receive invoices and extend the mailbox event proto with per-part onion data. The legacy single-onion field remains valid when the parts list is empty. Validate each received part against the invoice payment address and MPP total, then accept the set only when the forwarded amounts sum to the invoice amount. This lets compliant senders split a Lightning payment while preserving the single vHTLC claim path.
Cover the remaining rejection paths of the per-part onion validation: a part without an MPP record, a zero-amount part, a part forwarding more than the invoice amount, and a legacy single-onion event that forwards less than the invoice amount. Also rewrap two validation error messages to the formatter baseline.
There was a problem hiding this comment.
Code Review
This pull request introduces support for multi-part payments (MPP) in out-swaps. It updates the protobuf definitions and client code to handle multiple HTLC shards (parts), enabling the validation of individual onion payloads and ensuring their cumulative amount matches the expected invoice total. Additionally, the invoice generator is updated to advertise optional MPP support, and comprehensive unit tests are added to verify these validation paths. Feedback on the changes suggests optimizing the onion payload validation loop in validateOnionPayload by adding an early-exit check if the running sum of shard amounts exceeds the expected invoice amount, which avoids performing unnecessary and CPU-intensive cryptographic decoding on subsequent parts.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| idx, payload.totalAmount, expectedMsat) | ||
| } | ||
|
|
||
| sumMsat += payload.amountToForward |
There was a problem hiding this comment.
To prevent unnecessary cryptographic onion decoding operations (which are CPU-intensive) for subsequent parts when the total amount has already exceeded the expected invoice amount, we should perform an early-exit check right after updating sumMsat.
sumMsat += payload.amountToForward
if sumMsat > expectedMsat {
return fmt.Errorf("part %d: onion amounts sum %d msat exceeds invoice amount %d msat",
idx, sumMsat, expectedMsat)
}|
@claude pls review. Context: this is the darepo-client/proto half of swapdk-server issue #28 (MPP receive). It adds optional MPP invoice support and validates per-part out-swap HTLC event onions while preserving the legacy single-onion event path. The server PR is lightninglabs/swapdk-server#123 and points its client submodule at this branch. |
|
Claude finished @bhandras's task in 1m 30s —— View job Review: MPP out-swap support
Overall this is a clean, well-structured change. The core security property is solid: every shard's onion is independently decoded with the invoice auth key, and each is checked against the payment address, the MPP total, and a cross-check between the server-declared A few things worth considering: 1. No bound on the number of parts (potential resource exhaustion)
2. Early-exit on running sum (the Gemini suggestion)Each part is individually rejected if sumMsat += payload.amountToForward
if sumMsat > expectedMsat {
return fmt.Errorf("part %d: onion amounts sum %d msat exceeds "+
"invoice amount %d msat", idx, sumMsat, expectedMsat)
}Low priority, but cheap. Fix this → 3.
|
Summary
This PR adds the darepo-client side of MPP out-swap receive.
Before this change, the client treated an out-swap HTLC event as one Lightning payment part with one final-hop onion blob. That is enough for a single-part payment, but it does not describe a Lightning MPP payment. In an MPP receive, LND delivers several HTLC shards. Each shard has its own amount and onion payload, but all shards belong to one out-swap receive and one Ark vHTLC.
The client now understands that shape. It can accept a mailbox event that carries several HTLC parts, validate each part, and only then durably accept and acknowledge the out-swap event.
New model
For one receive invoice:
The legacy path stays intact. If
partsis empty, the client treatsonion_blobas the single payment part, and that one part must forward the full invoice amount on its own.For MPP events, each part carries the millisatoshi amount for that shard and the raw final-hop onion blob for that shard. The receive session decodes every onion with the receive auth key, checks that every shard belongs to the expected invoice, and checks that the aggregate forwarded amount matches the out-swap amount before accepting the event.
What changes
OutSwapHtlcPartto the client model and adds a repeatedpartsfield toOutSwapHtlcEventin the wire proto.onion_blobas the compatibility field for legacy single-part events. Whenpartsis populated, the proto still carries the whole out-swap amount inamount_sat.Why this is safe
The client still fails closed at the receive-event boundary. It does not acknowledge the mailbox event until the out-swap HTLC event has been validated and durably accepted.
For an MPP event, every shard must decode with the receive auth key and bind to the expected payment hash. Every shard must also carry the expected payment address and MPP total. The plaintext part amount from the server must match the amount committed in that shard's onion, so the server cannot lie about the split.
The aggregate check is still required. A set of individually valid shards is accepted only if the forwarded amounts add up to the invoice amount. A missing part, underpayment, overpayment, bad payment address, bad MPP total, zero-amount shard, or mismatched part amount rejects the event.
Single-part events remain compatible because an event without
partsstill uses the oldonion_blobfield. That legacy shard must forward the full invoice amount.The funding and aggregation side lives in swapdk-server PR #123. That server PR collects the held Lightning shards, funds one Ark vHTLC after the invoice total is covered, and publishes the per-part onions that this client PR validates.
Validation
go test ./sdk/swaps -run 'TestReceiveSessionValidateOnionPayload|TestInvoiceGeneratorIncludesRouteFeeHints|TestMailboxOutSwapEventReceiverPullsAndAcks' -count=1go test ./sdk/swaps ./swaprpc -count=1make commitmsg-lint commit=d6c56801make commitmsg-lint commit=57124441git diff --checkDependency / coordination
This is the darepo-client side of MPP out-swap receive and pairs with lightninglabs/swapdk-server#123.
Expected order: