Repository navigation
Conversation
|
👋 Thanks for assigning @jkczyz as a reviewer! |
fc05da7 to
badf902
Compare
f83b7ed to
f872ce4
Compare
| Ok(InvreqResponseInstructions::SendStaticInvoice { .. }) | Err(()) => return None, | ||
| }; | ||
|
|
||
| let allow_mpp = invoice_request.amount().is_some(); |
There was a problem hiding this comment.
Shouldn't this always be true for the first (non-LSP) attempt here?
There was a problem hiding this comment.
Ah, yes, it seems for the first build_invoice call we'll want always true. Will add a fixup.
| Some(pending_request) => pending_request, | ||
| None => { | ||
| return Some(( | ||
| OffersMessage::InvoiceError(InvoiceError::from_string( |
There was a problem hiding this comment.
Should we not consider duplicatively returning an existing LSPS2 cached negotiation here? In cases where the LSP over-allocated liquidity and we have enough for a second payment, or where we're getting DoS'd and most of the invreqs will not result in a payment, returning something might actually get us the payment.
FWIW if we do this it might be worth considering a marginally different router design, which might be cleaner - rather than deciding on which lease to use and then adding it to the metadata and using that metadata to tell the router what to inject, give the router a reference to the LSPS2 lease store and have it simply ask for the latest (potentially-unused) lease for a payment, then inject the metadata it wants.
There was a problem hiding this comment.
Hmm, in my mind both of these fall in the category 'further optimizations', i.e., I'd lean towards taking a new look at them in a follow-up rather than trying to land all of it in this initial implementation.
In particular I had considered doing something along the line of what you describe in the second paragraph to avoid transferring all of the parameters via the payment metadata, but for simplicity's sake (though nothing is really simple here) for now opted to keep it in the metadata for now as that makes it at least easier to reason about compared to introducing yet-another side channel that could race if we're not super careful.
There was a problem hiding this comment.
That's fine with me, I only raised it because it would likely imply a refactor of the router, so if we want to do that refactor ideally we should do it now rather than rewriting the code later. The feature itself would be nice but could be a followup.
f872ce4 to
621fd3d
Compare
|
|
||
| let (negotiated_lease, min_total_fee_msat, cheapest_lsp) = | ||
| self.negotiate_fixed_lease(amount_msat, connection_manager).await?; | ||
| let lease = self.consume_lease(&negotiated_lease.id).await?; |
There was a problem hiding this comment.
Won't this re-fetch through valid() and reject the lease we just bought if its validity is under 24h? Likewise in acquire_variable_lease.
| lease, | ||
| move |lease| async move { lease_store.remove(&lease.id).await }, | ||
| |lease| { | ||
| self.lease_state.lock().expect("lock").remove(&lease.id); |
There was a problem hiding this comment.
Can't two concurrent requests both select the same lease? Both callers would get the same single-use intercept SCID.
There was a problem hiding this comment.
Hmm, good catch. It seems this is a regression as originally we had a slightly different design there that would have made this impossible. Will fix.
| let _pending_request = pending_request; | ||
| let response = | ||
| lsps2_client.prepare_invoice_response(jit_request, connection_manager).await; | ||
| let (message, instructions) = match response { | ||
| Ok(JitInvoiceResponse { payment_metadata: jit_metadata, allow_mpp }) => { | ||
| debug_assert_eq!(allow_mpp, jit_request.allow_mpp()); | ||
| let mut merged_metadata = payment_metadata.unwrap_or_default(); | ||
| merged_metadata.extend(jit_metadata); | ||
| match build_invoice( | ||
| flow.as_ref(), | ||
| channel_manager.as_ref(), | ||
| keys_manager.as_ref(), | ||
| router.as_ref(), | ||
| secp_ctx.as_ref(), | ||
| &invoice_request, | ||
| Some(merged_metadata), | ||
| allow_mpp, | ||
| payment_info.as_ref(), | ||
| ) { | ||
| Ok((invoice, context)) => ( | ||
| OffersMessage::Invoice(invoice), | ||
| responder.respond_with_reply_path(context), | ||
| ), | ||
| Err(error) => ( | ||
| OffersMessage::InvoiceError(error.into_invoice_error()), | ||
| responder.respond(), | ||
| ), | ||
| } | ||
| }, |
There was a problem hiding this comment.
A payer may send many invoice requests (for robustness) but only pay one. Won't this cause us to acquire too many leases / is a DoS vector?
There was a problem hiding this comment.
Well, discussed this quite a bit with Matt before. Yes, it's not ideal, but we'll have to make sure to not reuse leases/intercept SCIDs as otherwise subsequent payments will not trigger channel opens. This is unfortunately a limitation of the bLIP-52/LSPS2 spec that we can't really work around. One follow-up we could consider is to add rate-limiting and more user control, e.g., via an event-based flow in LDK Node (also see the TODO mentioned in OP).
| } | ||
| } | ||
|
|
||
| fn build_invoice( |
There was a problem hiding this comment.
Ideally, LDK provides some abstraction so we don't need to repeat much of this logic.
There was a problem hiding this comment.
Yeah, possibly, but at least for v0.3 that ship has sailed I'm afraid.
|
I had Claude review this and it came up with quite a few issues. I already left comments for ones that I was able to verify. But there some others here: https://gist.github.com/jkczyz/3324c2914601a2728605a49294f25bc9. Failing tests in this branch: https://github.com/jkczyz/ldk-node/tree/claude/pr-999-edge-cases-725f70 |
b6c726d to
7d4f5be
Compare
Still have to go through the list fully. Some seem valid, some bogus. |
7d4f5be to
7b456df
Compare
Addressed most of the points now. @jkczyz please let me know what you think. |
7b456df to
03260f1
Compare
|
Rebased to address minor conflicts. |
27c34ef to
f56fa41
Compare
Co-Authored-By: HAL 9000
Co-Authored-By: HAL 9000
Limit concurrently pending JIT invoice requests so an onion-message storm cannot create an unbounded number of lease negotiations. Hold a permit while requests wait for an offer lock or an LSP response. Co-Authored-By: HAL 9000
Keep built-in service parameters usable beyond the client cache safety margin so freshly negotiated leases are not rejected due to timing. Co-Authored-By: HAL 9000
Read fee limits from BOLT12 payment context metadata and reject unsupported withholding. Record accepted fees on inbound offer payments. Co-Authored-By: HAL 9000
Make same-amount and variable callers wait for one in-flight LSPS2 request, then recheck the shared cache before negotiating. Co-Authored-By: HAL 9000
Keep one usable lease ready after fixed or variable receive flows consume cached parameters. Foreground callers share the refill lock and reuse its result when it completes. Co-Authored-By: HAL 9000
Co-Authored-By: HAL 9000
Co-Authored-By: HAL 9000
After startup discovery, refill persisted fixed-amount and variable lease cache targets. Reuse valid leases and renegotiate only missing or stale entries so receiving can resume promptly after restart. Co-Authored-By: HAL 9000
Try the next eligible provider when a selected LSP rejects or times out, and retry bounded whole rounds for transient failures. Preserve immediate errors for fee limits and unavailable liquidity sources. Co-Authored-By: HAL 9000
Use resolved BOLT12 amounts to reject variable leases outside the payment range or total fee policy. Keep BOLT11 selection amountless until payment, while recording the exact BOLT12 fee limit. Co-Authored-By: HAL 9000
Co-Authored-By: HAL 9000
Co-Authored-By: HAL 9000
Treat the one-day margin as a cache policy while allowing freshly negotiated leases to cover only the invoice being built. Keep on-demand leases caller-owned and persist only leases intended for reuse. Co-Authored-By: HAL 9000
Co-Authored-By: HAL 9000
The margin test derived its boundary from the margin constant and only acquired cached leases, so it kept passing without any margin and with negotiation ignoring it. Use fixed offsets around the margin and also cover freshly negotiated fixed- and variable-amount leases. Co-Authored-By: HAL 9000
Cover fixed and variable BOLT12 offers through a real LSPS2 service. Verify node-ID offer addressing, fee withholding, fresh JIT channels, and the variable-amount single-path behavior. Co-Authored-By: HAL 9000
Extend the order-independent fee-selection scenario through a real BOLT12 payment. Also prove that BOLT11 consumption and BOLT12 response handling share the same replenished lease pool. Co-Authored-By: HAL 9000
Co-Authored-By: HAL 9000
Rebuild the receiver from its persisted store and pay the same long-lived offer again. Verify the pending offer survives and the exact cached lease is consumed before its replacement is negotiated. Co-Authored-By: HAL 9000
Pin the direct lease serialization format and ensure fixed-amount and variable-amount caches remain isolated. These focused checks complement the end-to-end BOLT11 and BOLT12 coverage. Co-Authored-By: HAL 9000
Only count inbound capacity from graph-known counterparties with usable forwarding parameters. Otherwise a direct-recipient fallback can hide the JIT path even though the payer cannot use that capacity. Co-Authored-By: HAL 9000
Inbound capacity only counted towards a direct recipient path if the counterparty's forwarding parameters also fit a blinded hop. Payers reach a direct path over the public graph though, where a large CLTV expiry delta is no obstacle, so such capacity was ignored and invoice creation failed without a lease despite sufficient public inbound. Only require the counterparty to advertise the channel. Co-Authored-By: HAL 9000
Co-Authored-By: HAL 9000
`UnifiedPayment::receive` applied `expiry_sec` only to the BOLT 11 invoice, while the BOLT 12 offer in the same URI never expired. Besides contradicting the documented meaning of the parameter, this left every unified receive needing a JIT channel with a permanent LSPS2 lease cache target, for which leases were re-negotiated indefinitely. Apply the expiry to the offer as well, so that all Lightning payment methods of a URI lapse together and lease cache targets registered for unified offers are pruned once the URI can no longer be paid. Co-Authored-By: HAL 9000
We now skip refills for one-off BOLT11 amounts. Other than that it might make sense to address anything else as a follow-up, given this seems to be a never-ending PR if we try to address all edge cases from the start. |
68ee684 to
4213056
Compare
jkczyz
left a comment
There was a problem hiding this comment.
LGTM. Just some minor comments, but feel free to squash.
🤖 Two commit messages worth adjusting when squashing, both made stale by the refill removal: "Refill consumed payment leases" still says a lease is kept ready after fixed or variable receive flows consume cached parameters (now only registered BOLT12 targets are refilled), and "Test multi-LSP BOLT12 selection" still says the test proves BOLT11 consumption and BOLT12 response handling share the same replenished lease pool.
| /// | ||
| /// This limit applies to all configured LSPS2 liquidity sources in addition to the fee negotiated | ||
| /// for a payment. Once the payment amount is known, fees exceeding it are never accepted, whether | ||
| /// or not this limit is set. If unset, payments of a yet unknown amount use the negotiated |
There was a problem hiding this comment.
🤖 "payments of a yet unknown amount" is now too narrow: after the last fixup, a variable-amount BOLT12 payment also uses the negotiated proportional fee as its limit even though its amount is known, because the payer may deliver more than requested. Maybe "payments over variable-amount leases"?
| return Ok(()); | ||
| }; | ||
|
|
||
| if counterparty_skimmed_fee_msat > max_total_opening_fee_msat { |
There was a problem hiding this comment.
🤖 Neither refusal branch here has a test: the allowance computation is unit-tested, but nothing drives a claim into fail_claimable_payment — not a skim exceeding the allowance, and not a skimmed payment without the LSPS2 metadata. An LSP withholding more than negotiated is the case this limit exists to catch, so it'd be good to see the refusal actually fire. Fine as a follow-up if staging a misbehaving LSP is too awkward here.
Based on https://git.rust-bitcoin.org/lightningdevkit/rust-lightning/pulls/4819.
This adds BOLT12 receive support over LSPS2 JIT channels using a node-local
OffersMessageHandlerandOffersMessageFlow.Offers retain ordinary blinded message paths and never contain an intercept SCID, allowing them to outlive individualPaymentLeases.Single-use
PaymentLeasesare persisted throughDataStore, while a bounded LRU persists fixed-amount and variableLeaseCacheTargets. Leases are restored and prefilled at startup, replenished after consumption, and periodically pruned when stale or close to expiry. Cache misses negotiate on demand with serialized requests, retries, and multi-LSP failover. BOLT11 uses the same cache.Every BOLT12 offer is eligible for JIT fallback when LSPS2 is configured. We first try ordinary payment paths and omit the JIT path entirely when existing inbound liquidity is sufficient. Otherwise, we consume or negotiate a lease and include a JIT path constrained to the full payment amount. Fixed invoices advertise MPP, variable invoices disable it, and
PaymentMetadataretains the fee policy needed to validate the LSP skim.Potential follow-ups: