feat(10-05): report BIP-32 key origin on lnd.create-psbt, and record the honest signing posture (D-07b/D-09)

With Bitcoin Core's wallet deleted, LND's PSBT round trip is the only
external-signer path Archipelago has, and D-09's key-origin protection moves
from Core descriptors (of which none remain) to the PSBT itself.

Adds `psbt_key_origin_report(&str) -> Result<PsbtKeyOriginReport>` to
lnd/wallet.rs, reporting `input_count`, `inputs_with_key_origin` and
`all_inputs_have_key_origin`. An input counts as carrying key origin when
either its `bip32_derivation` or `tap_key_origins` map is non-empty. A PSBT
with zero inputs reports false rather than vacuous truth. Parsed with the
already-present `bitcoin` and `base64` crates; no dependency added.

`lnd.create-psbt` gains an additive `key_origin` object on its response and a
`tracing::warn!` with the counts when key origin is missing, because that is
the exact condition under which a hardware signer refuses the PSBT. Computed
best-effort: a decode failure degrades to `null`, never to an error, so a
user's send cannot fail because an inspection helper could not parse
something. `handle_lnd_finalize_psbt` and `handle_lnd_create_raw_tx` (which
deliberately auto-signs with LND's hot keys) are untouched.

Three tests, with fixtures built programmatically from the `bitcoin` crate
rather than pasted as opaque base64: with-derivations, without-derivations,
and malformed-is-an-error-not-a-panic.

KEY-03-SIGNING-POSTURE.md gains an honest per-step coverage map of the
fund -> export -> sign offline -> import -> finalize -> broadcast round trip.
Of six steps, only the new inspection has automated coverage; steps 1, 4, 5
and 6 have none, and there is no air-gap transport (no animated QR, no .psbt
file exchange) — export/import is copy-paste of base64. Untested paths are
named as untested.

Records the verdict that decides whether any of this is an air gap: on a
default node an external signer CANNOT meaningfully sign a PSBT from
`lnd.create-psbt`, because LND holds the keys for every input it selects.
Evidence: the PSBT is funded from LND's own wallet; `ensure_wallet_initialized`
creates a full key-holding wallet via /v1/initwallet; the generated lnd.conf
carries no `remotesigner.*` block; and a search of apps/, scripts/,
core/archipelago/src and image-recipe/ for remotesigner/createwatchonly/
nochainbackend returns zero matches. No fleet node is provisioned watch-only.
What ships is PSBT transport, not air-gapped custody — the gap is
provisioning, not plumbing.

Adds the standing honesty statement in its own subsection: Lightning channel,
revocation and HTLC keys are NOT air-gappable at all. They must sign in real
time to answer counterparty commitments; remote signing relocates them to a
hardened host, it does not cool them.

Also adds a status banner to PSBT-SIGNING-ARCHITECTURE.md recording that its
Phase 1 was superseded by deletion rather than delivered, so §0's "single
highest-value change" and §2.1's invariant now read against a code path that
no longer exists. Banner only; §5.4's honesty table is byte-identical.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-08-02 10:08:42 -04:00
co-authored by Claude Opus 5
parent 40b77e392a
commit 262998747e
3 changed files with 406 additions and 0 deletions
@@ -1,5 +1,33 @@
# PSBT-First Signing Architecture
> ## ⚠️ Status update (2026-08-02): **§8 Phase 1 was superseded by deletion, not delivered**
>
> Phase 1 ("Descriptor watch-only read path", §8) planned to **rewrite**
> `handle_bitcoin_init_wallet_from_seed` so Bitcoin Core's wallet held only the xpub. That is not
> what happened. Under Phase 10 decision **D-07b**, the entire Bitcoin Core wallet path was
> **deleted**: `handle_bitcoin_init_wallet_from_seed` and its `bitcoin.init-wallet-from-seed`
> dispatch arm are gone. It had no caller, LND is the wallet the product drives, and the endpoint
> was authenticated *and* password-gated, so F-13 was key-at-rest duplication rather than an
> exposed endpoint.
>
> **Consequences for reading the rest of this document:**
>
> - **§0's "single highest-value change"** and **§2.1's invariant** now read against a code path
> that no longer exists. Their goal — the BIP-84 private key existing in exactly one place —
> is **achieved**, by removal rather than by conversion to watch-only.
> - **§1.1, §2.2, §3.1 and §7.3** describe a Core watch-only wallet and a wallet migration.
> **There is no such wallet and no migration was performed or is planned.**
> - **§3.1's key-origin requirement** still holds, but it now applies to the **PSBT** rather than
> to Archipelago-emitted descriptors, of which there are none left. `lnd.create-psbt` inspects
> and reports it (`psbt_key_origin_report`, `core/archipelago/src/api/rpc/lnd/wallet.rs`).
> - **§5 (LND) is unaffected and remains accurate**, including **§5.4's honesty table**, which is
> correct as written and unchanged.
>
> **For the current state, read `docs/security/KEY-03-SIGNING-POSTURE.md`** — it records the
> deletion with its evidence, an honest per-step coverage map of the LND PSBT round trip, and the
> verdict on whether an external signer can sign a default node's PSBT today (it cannot: no fleet
> node is provisioned watch-only). Phases 2-7 below are unaffected as design targets.
> **Status: specification.** No implementation. This document defines a target architecture and
> a phased rollout that a future `/gsd-plan-phase` can consume directly. It deliberately
> contains no code, adds no dependencies, and changes no wallet or signing behaviour.