Files
amethyst/quartz/plans/2026-07-23-bolt12-zap-interop-vectors.md
Claude 7535d791f3 feat(bolt12): verify compressed payer proofs via merkle reconstruction
Real BOLT12 wallets emit selective-disclosure payer proofs: `invreq_metadata`
is always withheld and other invoice fields may be elided for privacy, with
`proof_omitted_tlvs` / `proof_missing_hashes` / `proof_leaf_hashes` carrying
enough to rebuild the invoice signature's merkle root. The verifier previously
reported these as unsupported (cryptoVerified = false), so a zap paid through a
real wallet never counted locally.

Implement the lightning/bolts#1346 reader:

- Bolt12Merkle.reconstructRoot rebuilds the invoice root from the disclosed
  LnLeaf hashes + supplied nonce leaves (proof_leaf_hashes) + omitted-field
  markers + missing subtree hashes (consumed post-order DFS, smallest-to-largest).
  Add emitMissingHashes as the writer dual, unify both on one tree builder.
- Fix two latent interop bugs the vectors exposed: the nonce leaf hashes the
  record's type bytes (not the full encoded TLV), and the payer proof signs
  under fieldname `proof_signature` (not `signature`).
- Bolt12PayerProof gains marker/leaf/missing accessors and the invoice-field
  range predicate; the verifier reconstructs on every proof (type 0 is always
  the implied first omitted leaf) and drops the Unsupported result.
- Add Bolt12ProofBuilder to mint spec-compliant proofs (tests + future interop),
  and rewire Bolt12ProofFixture onto it.

Validated byte-for-byte against the draft's own conformance suite
(bolt12/payer-proof-test.json): all 5 valid vectors verify, all 23 invalid are
rejected, and the writer reproduces every vector's compression fields exactly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SpgpWLKzgD7vS9Fs4CXTR3
2026-07-25 02:35:16 +00:00

45 lines
2.3 KiB
Markdown

# BOLT12 zap proof verification — interop test vectors (follow-up)
Status: **done** (both work items shipped), with one standing upstream caveat.
`Bolt12ProofVerifier` now reconstructs and fully verifies **compressed** BOLT12
payer proofs — the selective-disclosure case real wallets emit — so they count as
`cryptoVerified = true` (subject to the usual offer-binding rule). Verified
byte-for-byte against the draft's own conformance vectors.
## What shipped
- **Vector-driven interop test.** `bolt12/payer-proof-test.json` from
[lightning/bolts#1346] is vendored into `quartz/src/commonTest/resources/bolt12/`
and driven by `Bolt12PayerProofVectorTest`: all 5 `valid_vectors` verify, all 23
`invalid_vectors` are rejected, and the writer (`Bolt12ProofBuilder`) reproduces
each valid vector's `proof_omitted_tlvs` / `proof_missing_hashes` /
`proof_leaf_hashes` exactly. The vectors disproved two of our earlier guesses,
now fixed:
- the **nonce leaf** hashes the record's *type* bytes, not the full encoded TLV
(`Bolt12Merkle.nonceLeafHash`);
- the **proof signature** field name is `proof_signature`, not `signature`
(`Bolt12ProofVerifier.PROOF_SIG_FIELD`).
- **Compressed-proof merkle reconstruction.** `Bolt12Merkle.reconstructRoot`
rebuilds the invoice root from the disclosed `LnLeaf` hashes + `proof_leaf_hashes`
(nonce leaves) + `proof_omitted_tlvs` markers + `proof_missing_hashes` (consumed
post-order DFS smallest-to-largest). `invreq_metadata` (type 0) is always the
implied first omitted leaf. The `isCompressed()` short-circuit is gone; every
proof now goes through reconstruction.
## Standing caveat (upstream)
#1346 is still an unmerged draft. The TLV type numbers, signature digest tag
strings, leaf/branch tags, and the invoice/proof field ranges track the current
PR head (`vincenzopalazzo/bolts@1be97b2`) and MUST be re-checked if the spec
changes before it merges. The vector test is the tripwire: refresh the resource
from the merged BOLT and it will flag any drift.
## Not covered here
Offer↔recipient-identity binding is still out of scope — a verified proof only
proves payment to the *embedded* offer, not that the offer belongs to the
p-tagged recipient (see `Bolt12ZapValidator.isInvoiceBoundToOffer`).
[lightning/bolts#1346]: https://github.com/lightning/bolts/pull/1346