spec: fold receive into the recursion (anchors MMR), fix key custody & transport, close ~60 consistency defects - #74
Merged
Conversation
Contributor
Author
|
Quality gate: three adversarial verification rounds (soundness / privacy / engineering / whole-document lenses, run in parallel), each finding verified against the source before acceptance.
Production build verified green with |
TaprootFreak
marked this pull request as ready for review
July 7, 2026 07:35
…sport, and 24 consistency defects
Design changes (recorded as D10-D16 in the implementation mandate):
- receive is now a real C transition (clause 10): the creating proof is
verified in-circuit (PCD across accounts), the coin admitted into the
coin-history SMT and balances; MAX_RX_COINS = 4. Previously §2.3.3
mutated account state outside the circuit, so an account's proof
attested nothing about the coins it holds.
- anchors MMR (§1.7.10) + fifth ProofData field anchors_root: receive
proofs bind the creating SpendRecord to an admitted batch in-circuit,
and clause 1 carries the root forward monotonically. This closes a
collusion-inflation hole: per-hop out-of-circuit anchor checks are not
transitive, so a malicious intermediary could launder an un-anchored
spend into downstream lineages. Derived from chain + bundles - no
on-chain format change, no C_batch change. serialize(ProofData) is now
160 bytes; /v1/chain/anchors endpoints added.
- nk moves from the SPEND branch to its own hardened branch A/3' and
into the operational bundle: the node-side prover needs it in the
witness (clause 4), so 'wallet only' was unimplementable. It still
cannot spend.
- detect_tag/epk travel as cleartext tags on the outer gift-wrap event;
the promised 1-ECDH+1-hash scan was impossible with the tag inside
the NIP-44-encrypted seal.
- ovk gets a concrete mechanism (out_ciphertext under
HKDF('zkCoins/v1/OutKey', ovk || epk), stored with the self-delivered
record); zkavk's 32B incoming-only variant is now grounded.
- BatchBundles are plaintext blobs (public, self-authenticating); ZBE
applies only to CoinProof bundles - the referenced 'publisher delivery
key' never existed.
- wording aligned to the thin-client rule: Requirement 4's verifier is
'the receiver, or its node on its behalf' (§6.2/§6.3/§6.7).
Consistency fixes: stale pre-batch plain-language box; 'txid prefix' vs
witness marker; supply-auditability claim vs §6.5; sub-linear vs linear
publisher proving; 'on-chain Rj'; CoinProofs wrongly listed as bundle
content; block_anchor bound missing from the §3.6 failure list; Path-B
size (8 KiB, not 512 B); terms_hash added to the issuance witness;
mint/send records use the canonical SpendRecord layout incl. k;
creating_prev_ash and anchor_hint in produced CoinProofs; le32 -> u32-be;
four->five interface families; dead anchors (§ Access model, § Explorer,
V.7 -> §3.3); Variant-2 renamed and 0x01 defined as retired; glossary
(zkbid, MMR, ProofData, DeliveryEvent, ZBE, zkavk, nk, ovk, new entries);
test vectors extended (anr_empty, 160-byte H(ProofData)).
… primitive parity plus external audit
… 30+ consistency defects
Adversarial verification of the previous commit found two real design
flaws in the anchors-MMR mechanism it introduced; both are fixed here,
and the ~30 wording/consistency defects the same pass surfaced are
cleared.
Design fixes (recorded in D12):
- Transitivity. Clause 10 previously bound only the receiver's own
anchors_root; a colluding intermediary could fold an un-anchored coin
against a fabricated MMR leaf, then wash it by exposing its own clean
root to the next hop — enabling a double-spend two hops downstream.
Clause 10(c) now binds, in-circuit, extends(creating_proof.anchors_root,
own anchors_root) for every received coin: the creating account's
anchors view MUST be a prefix of the receiver's, so a forged leaf
propagates into the top-level root and the first honest verifier's
single scan check (§2.3.3 step 2) rejects it.
- Privacy. anchors_root as a cleartext public input was dictionary-
decodable (one value per admission-prefix length) — a receive-recency
fingerprint that let a publisher or recipient correlate an account's
proofs, breaking Requirement 2. The fifth ProofData field is now a
hiding commitment anchors_commitment = Hc('AnchorsView', anchors_root ||
anchors_rand); the opening travels only to a coin's recipient (in the
CoinProof bundle) or a disclosure verifier. The §5.7 balance
attestation proves extends against a *global* MMR ceiling, so it no
longer leaks the subject's own prefix length.
Consistency fixes (same verification pass): nk custody wording in §6.1
(no longer 'SPEND branch'); Requirement 4 reworded to 'the receiver, or
its own node on its behalf' on the requirements page and everywhere it
is quoted; §6.2/§6.3/§6.6 thin-client alignment (node verifies, wallet
trusts its own node; foreign-node-only = fail-closed discrepancy
detection); BatchBundle is a plaintext blob everywhere (§1.4, §4.2.1,
§7.1, §7.2, §7.4, glossary); ZBE's Poly1305 is stated as its one new
primitive (NIP-44 uses HMAC); mint-verified re-verifies 'the mint's
recursive proof' not only an InitialProof; receive step 7 handles the
first-transition (InitialProof) case; ACK is MUST not MAY; §4.7 relay
cover-traffic claim corrected for the cleartext scan tags; Path-B path
size (8 KiB raw / ~17 KB JSON); zkavk 32-byte variant added to §1.7.7;
member-root anchors endpoint added to §7.5/§7.8; 231-byte payload vs
'229 after the 2-byte marker'; mandate D10-D16 table delimiter; ASCII
diagram alignment; V.4 gains anchors_rand and anchors_commitment rows.
…anchors_rand, close residual leaks
Second adversarial pass on the anchors-commitment design. Fixes:
- Clause 10(d) anchored the creating spend's member_root against r_root
(the creating account's OWN view) — unsatisfiable, since a spend is
appended to the MMR only after its batch reaches completed, strictly
after r_root was fixed. Now checked against w.anchors_root (the
receiver's later, scanned-valid root), which is where the admission
actually lands. Clause 10(c)'s extends(r_root, w.anchors_root) still
carries the creating account's receive history; (d) anchors the spend.
- Tip-picking is now normative: a transition MUST set anchors_root to the
live anchors-MMR tip. The publisher does receive the fee coin's opening
(it must, to fold the fee coin), so 'publisher learns nothing' was
wrong; with tip-picking the opening reveals only the global proving-time
tip (already implied by block_anchor), never the account's receive
history. Same rule makes the §5.6 link-holder and §5.7 ceiling
disclose nothing account-specific.
- anchors_rand is now normative: fresh per transition, derived
HKDF('zkCoins/v1/AnchorsRand', op_secret || send_counter) so it is
reconstructible for the clause-1 carry-forward and unpredictable to
outsiders; MUST NOT be reused or derived from anchors_root alone (that
would make two same-root transitions publicly linkable). Hiding
rationale stated (enumerable root set → rests on rand entropy).
- §5.7 ceiling MUST be a global root chosen independently of the
subject's view (verifier MAY prescribe it); 'reveals only the number'
corrected to acknowledge the anchor disclosure.
Consistency: AnchorsView tag unified to the bare-context form (matches
the surrounding MMR tags and the test vector); clause-1 commitment
opening written out as root||rand; §2.4 'the node' -> 'a foreign node';
glossary CoinProof gains anchors_opening; §5.4 zkavk 32-byte variant;
V.4 anr_empty description; MAX_BATCH_MEMBER_DEPTH=16; bulk anchors-roots
endpoint; §4.7 blob-fetch-pattern residual; intro diagram alignment.
… back the bulk-roots RPC
Third adversarial pass confirmed the anchors mechanism converges (0 HIGH,
sound and satisfiable). Remaining consistency/completeness nits closed:
- op_secret was referenced by the anchors_rand HKDF but never defined.
Added as a dedicated hardened branch A/4' in the key hierarchy and the
operational bundle {ivk, ovk, op, nk, op_secret}, kept separate from op
so anchors randomness never shares key material with the Nostr key.
The deterministic derivation makes anchors_rand reproducible by any
node holding the bundle (node portability, Requirement 10).
- Registered the AnchorsRand HKDF context in the §1.1 domain-separation
list (it was used but unregistered).
- Backed GET /v1/chain/anchors/roots with a ListAnchorsRoots server-stream
RPC — it is the light-client bulk-sync privacy path, so leaving it
unbacked violated the §7.5 map-to-RPC invariant.
- Made explicit that tip-picking is a wallet obligation, not circuit- or
verifier-enforced: a non-conforming wallet can only leak its own
recency, never break soundness or another account's privacy.
- Glossary + §7.8 note extended for op_secret.
TaprootFreak
force-pushed
the
spec/v1-consistency-pass
branch
from
July 7, 2026 08:34
73b3704 to
7ad01ed
Compare
TaprootFreak
added a commit
that referenced
this pull request
Jul 7, 2026
The rebase combined #74's step 5 (V.5 spender signature, sign-to-contract per section 3.3) with the step-6 lock wording that anchored both V.5 and V.6 to section 3.2. Section 3.2 is the publisher's BatchInscription S2C, correct only for V.6; the spender's S2C over H(ProofData) that governs V.5 lives in section 3.3. Split the reference so each vector points to its own section.
TaprootFreak
added a commit
that referenced
this pull request
Jul 7, 2026
#75) * spec: point V.7 step 6 to the correct S2C section per signature vector The rebase combined #74's step 5 (V.5 spender signature, sign-to-contract per section 3.3) with the step-6 lock wording that anchored both V.5 and V.6 to section 3.2. Section 3.2 is the publisher's BatchInscription S2C, correct only for V.6; the spender's S2C over H(ProofData) that governs V.5 lives in section 3.3. Split the reference so each vector points to its own section. * docs: apply the same per-vector S2C split to the mandate's lock wording Section 4 carried the same (V.5,V.6)->section 3.2 misassignment for the sign-to-contract tweak check: V.5's spender S2C lives in section 3.3, only V.6's publisher S2C is section 3.2. Split the citation to match the V.7 step 6 fix; the auxiliary-randomness rationale keeps its section 3.2 reference (nonce hygiene is defined there).
TaprootFreak
added a commit
that referenced
this pull request
Jul 10, 2026
#76) * docs: fix post-merge key-custody artifacts in risks.md and the mandate An integrity review of the consolidated develop state (five merged PRs) found stale wording that the #74 key-model change (D10/D12) reached in specification.md but not in the sister pages: - risks.md called the wallet 'sole custodian of the SPEND branch (skᵢ, nk); these keys never leave it' — but nk moved to its own hardened branch A/3' and is delegated to the wallet's own node in the operational bundle (§1.2/§6.1). The sentence even cited §1.2, which refutes it. Reworded: only skᵢ never leaves; nk is delegated. - risks.md listed the operational bundle as (ivk/ovk/op); completed it to {ivk, ovk, op, nk, op_secret} at both mentions. - specification.md §6.6 still said 'the wallet re-verifies every node answer against Bitcoin' — a D16 leftover contradicting the adjacent §6.2/§6.3 node-side wording and Requirement 4. Reworded to node-side. - implementation-mandate.md D10 listed the bundle as 4 keys; extended to 5 and documented op_secret/A4' + the anchors_rand HKDF in D12 (it was introduced in #74 but never recorded in the decision log). * docs: align D12 anchors_rand expression with the normative §1.7.10 encoding (u64-be) * docs: include op_secret in the §4.5 recovery key re-derivation (anchors_rand)
This was referenced Jul 10, 2026
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Second consistency pass over the normative spec. Phase 1 (#70) removed the outdated pages; this pass hardens
docs/specification.mditself. A multi-agent review (5 dimensions, adversarially verified) surfaced defects across correctness, privacy, and consistency; three further adversarial rounds found and closed four HIGH design flaws in the mechanism this pass introduces, until the design converged (0 HIGH, soundness verified).The core design change — receive is now part of the recursion
Previously §2.3.3 credited a received coin by mutating account state outside the circuit, so an account's own recursive proof attested nothing about the coins it holds — breaking §2.2's transitivity claim. This pass makes receive a real
Ctransition (predicate clause 10): the creating proof is verified in-circuit (PCD across accounts), and the coin is folded into the account's coin-history SMT and balances.To keep that trustless, a coin may be folded only if its creating spend was actually admitted on Bitcoin. That is proven in-circuit against an anchors MMR — an append-only accumulator over admitted batches'
member_roots, derived by every node from chain + bundles (no on-chain change). Its root is carried in a fifthProofDatafield.Two adversarial rounds hardened this:
extends), and the creating spend is anchored against the receiver's scanned-valid root (10(d)). A colluding intermediary cannot launder an un-anchored spend — the forgery propagates into the top-level root and the first honest verifier's single scan check rejects it.Hc("AnchorsView", anchors_root ‖ anchors_rand), not a decodable root (which would fingerprint an account's receive-recency).anchors_randis deterministic (HKDFkeyed by a new hardened branchA/4' = op_secret);anchors_rootis normatively tip-picked, so the opening a recipient or fee-publisher sees reveals only the global proving-time tip.Other fixes (recorded as D10–D16 in the implementation mandate)
A/3'and into the operational bundle — the node-side prover needs it in the witness; the old "wallet only" label was unimplementable. It still cannot spend.detect_tag/epkbecome cleartext tags on the outer gift-wrap (the promised 1-ECDH scan was impossible with them inside the encrypted seal);ovkgets a concreteout_ciphertextmechanism;BatchBundles are plaintext blobs (ZBE only wrapsCoinProofbundles).Verification
npm run buildgreen (onBrokenLinks/onBrokenAnchors: throw⇒ a green build proves no orphaned links or anchors)docs/implementation-mandate.md(D10–D16)