feat: add configurable route hints to BOLT11 receive APIs#2
Conversation
2a5154f to
62fd214
Compare
62fd214 to
ffce1d0
Compare
kaloudis
left a comment
There was a problem hiding this comment.
Reviewed the branch at e70a3c6. The core mechanics check out: claimability parameters (payment secret registration, MIN_FINAL_CLTV_EXPIRY_DELTA, currency, MPP feature, signature recovery) match what ChannelManager expects per the pinned rust-lightning, cargo check passes with and without --all-features, and uniffi bindgen generates valid Python/Kotlin/Swift bindings including the None variant.
Five inline comments below. Minor notes not worth separate threads:
- The PR description is stale: it still says unusable channels are skipped, but e70a3c6 made them fail fast.
- Custom-hint validation runs after
create_inbound_payment*. No state leaks (inbound payments are stateless), but validating the hints first is the cleaner fail-fast order and skips pointless key derivation on the error path. sha256::Hash::from_slice(&payment_hash.0)on a fixed[u8; 32]can never fail —sha256::Hash::from_byte_array(payment_hash.0)removes the dead error branch.- The
*_for_hashreceive variants got no route-hints counterpart even thoughreceive_innernow supports the combination — worth deciding whether that asymmetry is intentional before this API ships.
|
|
||
| /// Controls which route hints are included when creating a BOLT11 invoice. | ||
| #[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||
| pub enum RouteHintsMode { |
There was a problem hiding this comment.
Invalid mode/ids combinations are representable and silently accepted. custom_route_hint_user_channel_ids is only read in the Custom branch: Automatic or None plus Some(ids) silently ignores the ids (no error, and no doc stating they only apply to Custom), while Custom plus a null list falls through unwrap_or_default() into Error::InvalidChannelId, pointing the caller at their channel ids instead of the missing argument.
A data-carrying enum makes both mismatches unrepresentable and deletes the second parameter from every signature:
pub enum RouteHints {
None,
Automatic,
Custom(Vec<UserChannelId>),
}UniFFI handles this via [Enum] interface — this UDL already uses that shape with sequence fields (e.g. Event::PaymentReceived(... sequence<CustomTlvRecord> custom_records)).
| ) | ||
| } | ||
|
|
||
| fn create_bolt11_invoice_with_route_hints( |
There was a problem hiding this comment.
This adds a second in-repo invoice-construction path that will drift from LDK's. Automatic invoices go through ChannelManager::create_bolt11_invoice; None/Custom now go through this hand-rolled builder (itself near-verbatim from lsps2_create_jit_invoice in liquidity.rs). Any upstream change to LDK's invoice construction — new feature bits, CLTV buffer changes, payment metadata — lands only in the Automatic arm, and the modes diverge silently.
Since this repo already pins ZeusLN/rust-lightning (lsps7-for-ldk-node-close-fix, currently 2 commits off upstream), the cleaner fix is a small additive field on Bolt11InvoiceParameters in the fork, e.g. route_hints_override: Option<Vec<RouteHint>> (with Some(vec![]) meaning no hints). All three modes then stay on LDK's single audited construction-and-signing path, and this whole function disappears. That would also resolve the signing and error-granularity comments below.
|
|
||
| let invoice = invoice_builder | ||
| .build_signed(|hash| { | ||
| Secp256k1::new() |
There was a problem hiding this comment.
This regresses invoice signing from the NodeSigner abstraction to raw key extraction. On the base branch every invoice is signed inside create_bolt11_invoice via node_signer.sign_invoice(&raw_invoice, Recipient::Node); this path instead pulls the raw node secret and signs with a fresh Secp256k1 context. WalletKeysManager already implements sign_invoice (delegating to the inner KeysManager and its cached secp context), so the LDK-idiomatic form works here as-is:
let raw_invoice = invoice_builder.build_raw().map_err(...)?;
let signature = self.keys_manager.sign_invoice(&raw_invoice, Recipient::Node);
let invoice = raw_invoice
.sign(|_| signature)
.map(|signed| Bolt11Invoice::from_signed(signed))...(The raw-key pattern here matches liquidity.rs's JIT path inherited from upstream, so it's an established in-repo idiom — but there's no reason to add a second such site when the signer abstraction is already wired up.)
| } | ||
| } | ||
|
|
||
| fn build_custom_route_hints_from_channels( |
There was a problem hiding this comment.
All six failure causes in this function return Error::InvalidChannelId, with no logging and an undocumented cap. Empty list, more than MAX_CUSTOM_ROUTE_HINTS ids (even when every id is valid), unknown id, not-ready channel, missing scid, and missing forwarding info all render as "The given channel ID is invalid." Elsewhere in this file coarse errors are consistently paired with a log_error! naming the actual cause; this helper logs nothing, so neither callers nor operators can tell a caller bug from a transient state.
Two related items:
- The 3-id cap is currently stated only in the PR description — it should be on the
receive_*_with_route_hintsdoc comments and in the UDL. - Duplicate ids aren't deduped:
[x, x, x]yields three identical hints, while[x, x, y, z](three distinct channels) is rejected on length. Harmless for payers, but inconsistent — worth deduping before the length check.
| .or(channel.short_channel_id) | ||
| .ok_or(Error::InvalidChannelId)?; | ||
|
|
||
| let forwarding_info = channel |
There was a problem hiding this comment.
A transient state is reported as a permanently-invalid channel id. A channel can be is_channel_ready while counterparty.forwarding_info is still None until the peer's first channel_update is processed — LDK's own hint selection treats this as skip-with-trace ("Ignoring channel for invoice route hints", invoice_utils.rs), and it has a test for exactly this state. Failing fast is the right call for Custom mode (silently dropping a requested hint would be worse), but the caller gets InvalidChannelId for a valid id in a retryable state, indistinguishable from a typo. The same applies to the missing-scid and not-ready checks above.
Since Error is an exhaustive enum (adding a variant is a breaking change for exhaustive matchers and the FFI surface), the minimum fix is a distinct log_error! per cause so the states are at least distinguishable in logs — see the error-granularity comment on this function.
Summary
RouteHintsMode(None/Automatic/Custom) for BOLT11 invoice creationreceive_with_route_hintsandreceive_variable_amount_with_route_hints(Rust + UniFFI)receive*APIs stay onAutomaticCustomtakes up to 3UserChannelIds and builds hints from ready channels that have an inbound SCID and forwarding info. Unusable channels are skipped;