Files
archy/.planning/phases/01-federation-mesh-hardening/01-19-PLAN.md

150 lines
7.4 KiB
Markdown

---
phase: 01-federation-mesh-hardening
plan: 19
type: execute
wave: 1
depends_on: []
files_modified:
- core/archipelago/src/api/rpc/lnd/wallet.rs
autonomous: false
requirements: [FED-08]
must_haves:
truths:
- "An invoice created through the wallet UI's Receive flow embeds a route hint for every private/unannounced channel the node holds"
- "`lncli decodepayreq <invoice>` on an invoice created via the wallet UI shows a non-empty `route_hints` array containing the private channel's chan_id"
- "A real external wallet can pay an invoice created by the wallet UI on a node whose only channel is private — the HTLC arrives and the invoice reaches SETTLED"
- "Nodes with public channels are unaffected — payments still route directly over the public channel"
- "Every other invoice-creation call site in the codebase is audited for the same omission, and each is either fixed or documented as deliberately not needing route hints"
prohibitions:
- "MUST NOT change the amount, memo, expiry, or any other invoice field's existing behavior"
- "MUST NOT log, echo, or commit any macaroon, invoice preimage, or node credential"
artifacts:
- path: "core/archipelago/src/api/rpc/lnd/wallet.rs"
provides: "Invoice creation that sets LND's `private` flag so route hints are embedded"
---
<objective>
Fix Lightning receive on nodes whose channels are private/unannounced.
`handle_lnd_createinvoice` posts to LND's REST `/v1/invoices` with only `value`
and `memo`. LND defaults `private` to `false`, so the returned invoice carries
`route_hints: []`. Private channels are not propagated through public gossip, so
a sender has no way to find a route — the invoice is unpayable by anyone.
Diagnosed on `archy-x250-mad2` (2026-07-31), whose single channel to "Olympus by
ZEUS" is `private: true` with ~40.8k sats of usable inbound. Three wallet-UI
invoices (10,000 / 5,000 / 500 sats) all had empty route hints and never received
an HTLC. The one invoice that DID settle carries a memo ("Paid to Archipelago
(Order ID: ...)") that appears nowhere in this Rust source — it came from a
separate system (likely BTCPay) that builds invoices correctly. That is why
"some payments have worked" while the wallet's own Receive flow never has.
**This is not node-specific.** The bug is unconditional; it only *manifests*
where a node lacks a public channel. Any user relying on a private channel has a
broken Receive flow today.
</objective>
<execution_context>
@$HOME/.claude/gsd-core/workflows/execute-plan.md
@$HOME/.claude/gsd-core/templates/summary.md
</execution_context>
<tasks>
<task type="auto">
<name>Task 1: Embed route hints in wallet-created invoices</name>
<reversibility rating="reversible">One field in one JSON body; revert is a single-line change.</reversibility>
<files>core/archipelago/src/api/rpc/lnd/wallet.rs</files>
<read_first>
- `core/archipelago/src/api/rpc/lnd/wallet.rs` around lines 554-557 — `handle_lnd_createinvoice`'s `invoice_body` construction
- `core/archipelago/src/api/rpc/lnd/channels.rs:271,368` — the only current uses of a `private` field (channel OPEN, not invoice creation); confirms the omission is specific to invoices
</read_first>
<action>
Add `"private": true` to `invoice_body` in `handle_lnd_createinvoice` so LND
embeds hop hints for unannounced channels:
```rust
let invoice_body = serde_json::json!({
"value": amount_sats.to_string(),
"memo": memo,
"private": true,
});
```
Setting it unconditionally is correct and safe: a route hint is harmless when
the node also has public channels — LND still routes directly over a public
channel when it can, and the hint merely offers an alternate path. Add a
short comment stating why it is unconditional, so a future reader doesn't
"optimize" it back to conditional and silently reintroduce the bug.
Then audit every OTHER invoice-creation call site for the same omission —
grep the tree for `/v1/invoices`, `addinvoice`, hold-invoice, LNURL and any
keysend-adjacent flow. Fix each that should carry route hints; for any that
deliberately should not, record the reason in the SUMMARY. Report the full
list either way.
</action>
<verify>
<automated>cd core && cargo build --release 2>&1 | tail -5 && cargo test -p archipelago 2>&1 | tail -10</automated>
</verify>
<acceptance_criteria>
- `grep -A6 'let invoice_body' core/archipelago/src/api/rpc/lnd/wallet.rs` shows `"private": true`
- `cargo build --release` succeeds
- Existing tests pass
- The SUMMARY lists every invoice-creation call site found, with fixed/not-needed and the reason
</acceptance_criteria>
<done>Wallet-created invoices ask LND for route hints, and every other invoice path has been audited.</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<name>Task 2: Confirm the fix, then hand off to post-OTA verification</name>
<what-built>
Invoice creation now sets LND's `private` flag at both call sites, so
invoices embed a hop hint for the node's unannounced channel and become
routable from outside.
</what-built>
<constraint priority="highest">
**`archy-x250-mad2` is a USER'S device holding real funds. Never deploy to
it, SSH to it, or run any command against it.** This fix reaches it only via
the normal OTA release, after the release is confirmed. Any verification
involving that node is performed by its owner after the release lands —
never by us, and never as a direct deploy.
</constraint>
<how-to-verify>
Verifiable now, without touching any user device:
1. Tests assert the request body sent to LND's `/v1/invoices` carries
`"private": true` for BOTH `handle_lnd_createinvoice` (wallet Receive)
and `create_invoice` (paid-content/peer-files seller flow).
2. On a node under our control with only PUBLIC channels, creating and paying
an invoice still behaves exactly as before — no regression. Note the
limitation honestly: a public-channel node shows empty route hints even
when the fix is correct, so this checks non-regression only.
Post-OTA-release, performed by the device owner:
3. Create an invoice through the **wallet UI** (not raw `lncli`) — e.g. 100 sats.
4. `lncli decodepayreq <invoice>` → `route_hints` populated with the Olympus
channel's `chan_id` (was `[]` before this fix).
5. Pay it from a real external wallet; `lncli listinvoices` shows a non-empty
`htlcs` array and `state: SETTLED`.
</how-to-verify>
<resume-signal>Type "approved", or describe what you saw — which step, what happened instead.</resume-signal>
</task>
</tasks>
<verification>
- `cargo build --release` and the existing test suite pass
- On archy-x250-mad2, a wallet-UI invoice decodes with populated `route_hints` and settles when paid externally
- A public-channel node is unaffected
</verification>
<success_criteria>
- Receiving Lightning payments works through the wallet UI on a node whose only channel is private
- No regression for nodes with public channels
- Any other invoice-creation path sharing this omission is fixed or explicitly cleared
</success_criteria>
<output>
Create `.planning/phases/01-federation-mesh-hardening/01-19-SUMMARY.md`. It MUST record: the full list of invoice-creation call sites audited with each one's disposition, and the decoded `route_hints` before/after evidence from the node.
</output>