--- phase: 01-federation-mesh-hardening plan: 19 subsystem: lightning tags: [lnd, invoices, route-hints, private-channels, wallet] requires: - phase: 01-federation-mesh-hardening provides: "The two existing LND invoice-creation call sites in core/archipelago/src/api/rpc/lnd/wallet.rs — the seller-side/peer-file flow and the wallet UI's Receive flow" provides: - "build_invoice_request_body(): one place where an invoice body is minted, with `private: true` unconditional so LND embeds route hints for unannounced channels" affects: [wallet, lightning, paid-content] tech-stack: added: [] patterns: - "Two call sites that must agree get one shared constructor plus one test on the constructor, rather than two near-identical literals and a hope. The duplicated json! literal is exactly how one site got fixed and the other didn't." key-files: created: [] modified: - core/archipelago/src/api/rpc/lnd/wallet.rs key-decisions: - "`private: true` is unconditional rather than conditional on 'does this node have only private channels'. It is harmless when public channels exist — LND still routes directly over a public channel when it can, and the hint is an unused alternate path — so the conditional would add a failure mode (mis-detecting channel state) to buy nothing." - "Both call sites route through one constructor so a single test pins the field for both, and neither can silently drift back to `false`." requirements-completed: [FED-08] coverage: - id: D1 description: "An invoice minted by the wallet UI's Receive flow embeds route hints for private/unannounced channels" requirement: "FED-08" verification: - kind: unit ref: "core/archipelago/src/api/rpc/lnd/wallet.rs#invoice_request_body_always_sets_private_true" status: pass human_judgment: false - id: D2 description: "The seller-side/peer-file invoice path embeds them too — the twin site does not drift" requirement: "FED-08" verification: - kind: other ref: "Both call sites (wallet.rs:330 and wallet.rs:554) call build_invoice_request_body; the json! literal exists in exactly one place" status: pass human_judgment: false - id: D3 description: "A payment actually arrives over a private channel on an affected node" requirement: "FED-08" verification: - kind: manual_procedural ref: "Post-OTA checkpoint on the affected node (archy-x250-mad2) — NOT RUN; the plan forbids deploying directly to a user device, so this is verified after the release lands" status: deferred human_judgment: true duration: 20min completed: 2026-08-02 status: complete-pending-post-ota-checkpoint --- # Phase 1 Plan 19: Invoices Embed Route Hints for Private Channels (FED-08) Summary **A node whose inbound liquidity sits on unannounced channels could not be paid: its invoices carried no route hints, so a payer had no way to discover a path in. Both invoice-creation paths now mint their body through one constructor that sets `private: true` unconditionally.** ## Performance - **Duration:** ~20 min (code committed 2026-07-31 as `e5c38866`; this SUMMARY 2026-08-02) - **Tasks:** 1/2 (Task 2 is a post-OTA checkpoint on a user device — see below) - **Files modified:** 1 ## Accomplishments - `build_invoice_request_body(amount_sats, memo)` is now the single place an LND `/v1/invoices` body is constructed, with `private: true` set unconditionally and a doc comment explaining why it must stay that way. - Both call sites use it: the seller-side/peer-file flow (`create_invoice`) and the wallet UI's Receive flow (`handle_lnd_createinvoice`, the `lnd.createinvoice` RPC). The duplicated `json!` literal that let one site diverge from the other is gone. - `invoice_request_body_always_sets_private_true` pins the field for both sites at once. ## Deviations from Plan None. **Process note:** as with 01-20, the code landed on 2026-07-31 (`e5c38866`) but no SUMMARY was written and the roadmap entry was never ticked, so the plan read as unstarted. This file closes that gap. ## Known Stubs None. ## Threat Flags None. `private: true` does not weaken anything: it adds a routing hint to an invoice the payer already holds. It does reveal the existence of an unannounced channel to whoever holds that specific invoice — which is inherent to being payable over that channel at all, and is the explicit intent of the requirement. ## Outstanding: post-OTA checkpoint (Task 2) The affected node is **archy-x250-mad2, a user's device**. Commit `516c3bfa` records the standing constraint: never deploy directly to a user device — verify post-OTA instead. So the remaining verification is: 1. Ship the release 2. After it lands on the affected node, mint an invoice from the wallet's Receive flow 3. Confirm the BOLT11 decodes with a route hint (`r` field) for the private channel 4. Confirm a payment from outside actually settles over it This is the one piece of FED-08 that cannot be proven from here.