Skip to content

spec: revert accumulator to on-chain half-aggregated nullifiers (implements research#16) - #97

Merged
TaprootFreak merged 10 commits into
developfrom
spec/revert-accumulator-paper-model
Jul 12, 2026
Merged

spec: revert accumulator to on-chain half-aggregated nullifiers (implements research#16)#97
TaprootFreak merged 10 commits into
developfrom
spec/revert-accumulator-paper-model

Conversation

@TaprootFreak

Copy link
Copy Markdown
Contributor

Summary

Reverts the normative on-chain layer of docs/specification.md from the batched BatchInscription/BatchBundle/AggregateBatchProof model back to the peer-reviewed Shielded CSV paper model, implementing the accepted decision record (research#16, accepted in f392fa0) and the finding dispositions of the paper-conformance remediation (#96, which authorizes exactly this versioned specification rewrite).

The on-chain object is now a per-transition account-state nullifier (Pkᵢ, Rᵢ) — the AggregateStateNullifierV3 — published on Bitcoin, NISSHAC half-aggregated across a publish (~64 B/tx asymptotic). Every node rebuilds the global nullifier accumulator by first-occurrence keyed by Pkᵢ from Bitcoin alone (no off-chain consensus object). Reorg safety is the paper's conditional NAV (execute/no-op under arbitrary-depth rollback/replay). The per-account recursive proof C and all surviving hardening are preserved.

Relationship to #96

This PR is the execution of #96's accepted plan, not a competing revert. #96 is the accepted architecture direction + finding dispositions (F-01…F-08) + the §9 specification edit map + Gates A–C; it explicitly states "the normative source remains specification.md until one versioned specification PR incorporates this plan." This is that PR. It follows #96's §9 edit-map area-for-area and adopts its vocabulary (AggregateStateNullifierV3, TransitionEssenceV3, NISSHAC).

risks.md / assurance.md are owned by #96 and are deliberately not touched here (kept identical to develop) to avoid conflicts. Rebase this PR to land after #96. Still needed on those pages (for #96 or a follow-up): remove/replace the Publisher concentration (serial accumulator), conflicting-SpendRecord griefing, and BatchBundle retention risk sections (their cause is removed by this revert); fix the one risks.md sentence that still says "Bitcoin holds only the opaque BatchInscription"; and update assurance.md's actor-behaviour-risk clause.

Per-section change map (specification.md)

Area Change
Front-matter data-flow diagrams, contents, requirements-traceability rewritten to the per-transition nullifier
§1.1 domain tags: dropped anchors-MMR/batch contexts; added NavCommit, NavRand, StateUpdate; HalfAgg now on-chain
§1.4 on-chain nullifier (Pkᵢ, Rᵢ) / AggregateStateNullifierV3; SpendRecord redefined (96 B authorization); ProofData 5th field nav_commitment; ProofData = v1 TransitionEssenceV3; removed BatchInscription/BatchBundle/next_pubkey_commit rows
§1.5 CoinProof carries creating_nullifier + nav_opening (was anchor_hint/anchors_opening)
§1.6 accumulator keyed by Pkᵢ, first-occurrence, Bitcoin-only; anchors-MMR row removed
§1.7.6 accumulator SMT re-keyed nf → Pkᵢ, leaf Rᵢ
§1.7.9 dropped C_batch/AggregateBatchProof/H_agg; on-chain binding is H(ProofData), not proof bytes
§1.7.10 anchors-MMR deleted; replaced by a normative NISSHAC (half-aggregation-with-commitments) subsection
§2.1 fixed-message m_state signature + in-circuit sign-to-contract opening; conditional-NAV carry-forward (clause 1); whole-lineage receive-anchoring (clause 10); genesis publishes Pk₀
§2.2 C_batch/AggregateBatchProof removed; on-chain anchoring + conditional NAV described
§2.4/§2.5/§2.6 soundness rows; C_batch dimensioning removed (MAX_NAV_DEPTH added); NISSHAC-cited crypto costs
§2.3.1/§2.3.2/§2.3.3 mint/send/receive flows on the paper model; every state-advancing transition anchors
§3.1–§3.10 full on-chain-layer rewrite: nullifier object, transition signing, half-aggregation, first-occurrence scanning, Pkᵢ-keyed accumulator, fees, deterministic any-depth reorg + conditional NAV, states (no pending-DA, no mint-verified)
§4 recovery + DA: accumulator rebuilt from Bitcoin; BatchBundle consensus-DA removed; CoinProof custody-DA kept
§5 anchoring trail, balance attestation, address view re-based on the on-chain nullifier + conditional NAV
§6 threat model: contention-free self-publish; deterministic reorg
§7 wire formats, state-nullifier REST/gossip APIs (anchors endpoints removed), publisher interface (half-aggregate+inscribe), kernel RPC
Glossary / test vectors reworked to the paper model; V.5/V.6 to the nullifier layout

Also updated (derived pages #96 does not own): protocol.md, comparisons.md, intro.md, requirements.md, implementation-mandate.md.

F-04 verification outcome — anchor every state-advancing transition

F-04 (a mint accepted without a Bitcoin state nullifier) is settled toward anchoring: the unanchored mint-verified path is removed. Adversarial two-lens verification confirmed the batched-model mint-verified was "out of frame" of the safety proofs, i.e. not proven safe. In the Pkᵢ-keyed accumulator this generalizes: an unanchored receive would also let a coin be double-spent across an account fork (the two branches' later spends consume different rotated keys, so only anchoring the fork-point transition's Pkᵢ by first-occurrence catches it). Therefore every state-advancing transition — send, receive, and mint, including the genesis InitialProof under Pk₀ — publishes (Pkᵢ, Rᵢ) and is arbitrated by first-occurrence. Genesis publishes Pk₀ itself (the nk_commit binding alone does not separate two genesis forks under one Pk₀). Consequence, stated honestly: issuance frequency/timing become chain-visible (amounts/assets/parties/graph stay hidden).

docs#95 acceptance-criteria checklist

Issue impact

Self-check (specification.md)

The self-check sweep over specification.md returns only intentional design-history asides: §1.4 ProofData note ("without a redundant next_pubkey_commit"), §2.1 clause 2 ("the earlier next_pubkey_commit hiding commitment, now carried structurally by ash"), and §3.10 ("the batched design's dependence on … BatchBundle … is gone"). No broken internal or cross-page anchors (build is green).

Judgment calls

  1. next_pubkey_commit / TransitionEssenceV3docs: define a coherent paper-conformance remediation path #96's TransitionEssenceV3 lists a separate "next-key hiding commitment". This PR realizes it through new_account_state_hash (which commits the rotated current_pubkey inside an off-chain hash folded into H(ProofData)), not as a separate published field — keeping spec: hide the next_pubkey rotation edge behind a per-transition commitment #86's bind+hide substance without a redundant field. Flagged for reviewer.
  2. Every receive anchors — follows from the Pkᵢ-keyed fork-soundness argument above; receiving now costs a ~64-byte on-chain nullifier (accepted trade-off).
  3. NISSHAC placement — the full scheme is a new §1.7.10 (contiguous with §1.7.9; the old §1.7.10 anchors-MMR was deleted).

Links

research#16 · accepted decision f392fa0 · paper-conformance review research#22 · the authorizing plan #96 · docs#40 (the design reverted) · docs#21 (the pre-#40 paper-faithful state) · docs#95 (scope) · zk-coins/landing-page#32 (derived-copy follow-up).

@TaprootFreak

Copy link
Copy Markdown
Contributor Author

Review status — held at draft pending one soundness decision

Two parallel review lenses were run over this branch: conformance (CONTRIBUTING/style, English, internal anchors/links, #96 §9 edit-map coverage, hygiene, no risks.md/assurance.md creep) and logic/soundness (F-04 anchoring completeness, accumulator determinism, conditional-NAV/reorg coherence, no regression of the surviving hardening). Build is green on Node 20 with onBrokenLinks/onBrokenAnchors = throw, so every internal link and anchor resolves.

Conformance — clean after one fix

  • Full-text stale-term sweep over all six files (mint-verified, C_batch, BatchInscription, BatchBundle, bundle_locator, AggregateBatchProof, 231-byte, prev_root, new_root) returns only the intended design-history/traceability asides (specification.md §3.10 contrast note; implementation-mandate.md D4/D15). No derived page describes the batched model as current.
  • docs: define a coherent paper-conformance remediation path #96 §9 edit map: every row is covered in specification.md (or the derived pages for the last row); requirements.md + protocol.md carry the asymptotic-64-byte / private-recovery edits and drop the 231-byte and hidden-count claims; risks.md correctly untouched (owned by docs: define a coherent paper-conformance remediation path #96).
  • Two glossary entries added (AggregateStateNullifierV3, NISSHAC), alphabetically placed, anchors resolve, descriptions match §3.1/§3.3/§3.5/§1.7.10; the §2.2 clause 5§2.2 network/chain separation link-text tidy points at the bold "Network/chain separation (normative)." paragraph in §2.2.
  • Fixed: implementation-mandate.md still referred to "the two circuit_digests" in three places, stale under the single-C-circuit model (spec §1.7.9 pins one circuit_digest(C) per network; /v1/info returns circuit_digests: { C }). Corrected to singular with verb agreement.

Logic — one must-fix soundness gap (this is why the PR is held at draft)

The headline invariant this revert exists to establish — every state-advancing transition in a coin's lineage is first-occurrence-anchored — is asserted in the prose (§2.1 clause 1 line 594; §2.3.3 step 2; §2.4 "No double-spend") but is not enforced by the enumerated predicate for a transition that advances state without creating a delivered coin.

The only clauses that force an account-state nullifier into a canonical nav check are clause 10(d) (each received coin's immediate creating transition) and the prefix(prev.nav, w.nav) carry-forward of clause 1. A transition's own nullifier is inserted into a descendant's nav by nothing: the prefix chain carries each ancestor's dependency set, never the ancestor's own nullifier. So a pure receive, a self-held mint, or a genesis receive — none of which produces a coin any counterparty runs clause 10(d) against — advances state with no verifier-enforced anchoring of its Pkᵢ.

Concrete account-fork double-spend (the docs#92 class this PR claims to close): account X receives coin C at state S₀ (current_pubkey = P0), then builds two receive transitions off S₀R_a (rotates P0→Pa, folds C) and R_b (P0→Pb, folds C). X anchors only R_a (wins first-occurrence on P0). X then spends C in each branch: T_a (key Pa) → coin to Bob, T_b (key Pb) → coin to Carol; Pa ≠ Pb, so both spends anchor without colliding. Bob's clause-10(d) check pins T_a (first-occurrence ✓); Carol's pins T_b (first-occurrence ✓). R_b's key P0 is checked by nobody, so C is spent twice. Both recursive proofs are internally valid. The self-held-mint and genesis-receive forks (§6.5, §3.10) reopen by the same mechanism. §2.4's argument only covers an off-chain-only spend (which creates a delivered coin), not an off-chain-only receive.

Proposed fix (mirrors the accepted clause 10(d), symmetric): extend clause 1's AccountUpdateProof branch to witness the previous transition's on-chain nullifier (Pk_prev, R_prev, R'_prev) and require (Pk_prev, R_prev) ∈ w.nav with R_prev sign-to-contract-opening H(prev_proof.ProofData) — i.e. prove the transition that produced prev_account_state was itself first-occurrence-anchored. Key-only membership is insufficient (the fork loser's key is present with the winner's leaf R), so the leaf must be checked to open the previous proof's ProofData, exactly as 10(d) already does. This propagates to downstream receivers through the existing prefix chain and closes all three forks with one check. nav's stated definition at line 594 already names "the account-state nullifier of prev_account_state" as a member — this change makes that stated intent normative and enforced.

This is a substantive normative change to the core compliance predicate with hard-fork implications, so it is left for maintainer decision rather than applied unilaterally. The rest of the logic review is clean: accumulator determinism (pure function of Bitcoin, well-defined total order, no DA gate), conditional-NAV/reorg semantics, and the surviving hardening (nk_commit #79, coin-value #82, next-key hiding via new_account_state_hash #85/#86, wide-integer conservation #88, user@domain #91, CoinProof custody/DA separation) all hold.

Recommendation: resolve the clause-1 anchoring check before this moves out of draft.

…cate (close whole-lineage double-spend)

Clause 1 only enforced anchoring of a delivered coin's immediate creating
transition (clause 10(d)); a pure receive, self-held mint, or genesis-receive
advanced state (rotating current_pubkey) with no verifier-enforced anchor of its
own (Pki, Ri). The prefix-NAV carry-forward only ever carried each ancestor's
dependency set, never an ancestor's own nullifier leaf, so an account could fork
a receive at one state, anchor only one branch, and spend the same coin twice
under distinct rotated keys with no on-chain collision.

Add, on every AccountUpdateProof, a predecessor-nullifier check: witness the
previous transition's on-chain nullifier (Pk_prev, R_prev, R'_prev), require
(Pk_prev, R_prev) a canonical member of w.nav via the same gadget clause 10(d)
uses, and require R_prev to sign-to-contract-open H(prev_proof.ProofData) so the
accumulator leaf at Pk_prev is exactly R_prev (leaf, not key-only). This is the
faithful adaptation of the paper's per-transition union-membership check
(ToSAccVVerifyUnionMembership): the single circuit cannot commit its own R before
H(ProofData), so it binds the predecessor's nullifier, already on-chain at
proving time. The anchoring invariant now tiles losslessly: 10(d) covers the
immediate/cross-account edge, this check covers each own-lineage edge, and
genesis is caught by the first successor's predecessor-check plus the Pk0
first-occurrence. Rewrite the descriptive whole-lineage-anchoring prose to point
at the enforced clause. Residual Pk_prev key-binding and proving-cost impact
flagged as open refinements.
…ssor key is a load-bearing open gap, not belt-and-suspenders

Adversarial review of the predecessor-nullifier check found that binding only
the leaf R_prev (via sign-to-contract to H(prev_proof.ProofData)) while leaving
Pk_prev a free witness does NOT close the double-spend against a malicious
prover: because the accumulator folds any validly-signed (Pk, R) pair by
first-occurrence (proofs are off-chain) and raw single nullifiers are
permissionless, an attacker can publish a naked nullifier under a fresh key it
controls whose leaf opens H(prev_proof.ProofData), satisfying both membership
and leaf checks without anchoring the fork loser's real (Pki, Ri) — so no
first-occurrence collision arises and the fresh-key-substitution fork survives.
Clause 10(d) / receive-check-4 carry the identical free-Pk_create gap.

Reclassify the Pk_prev key-binding from an optional 'open refinement' to a
normative Known soundness gap that is REQUIRED to close the fresh-key variant
(the predecessor-check closes only the same-key fork), note it is the same
systemic binding clause 10(d) needs, and make the closure statements in clause 1,
2.2, 2.3.3, 2.4 and 3.7 explicitly contingent on binding the consumed key.
Flagged for the paper authors' review; no closure is claimed that the predicate
does not enforce.
… fresh-key fork end-to-end

Completes the predecessor-anchoring fix by binding the nullifier KEY, not only
its leaf R. C now exposes each transition's consumed key (Pkᵢ = txn_pubkey, the
current_pubkey it spends and publishes as its on-chain nullifier key) as a public
output, distinct from serialize(ProofData) (so H(ProofData) and vectors V.4–V.6
are unchanged). Every on-chain-nullifier membership check is bound to it:
  - clause 1 predecessor-check: Pk_prev == prev_proof.consumed_pubkey (in-recursion);
  - clause 10(d) / receive-check-4: Pk_create == creating_proof.consumed_pubkey.
Together with the existing leaf/S2C check this is airtight: the leaf pins a
same-key fork loser (its stored leaf is the winner's R), the key pins a fresh-key
substitution (a naked nullifier minted under an attacker key no longer matches the
predecessor's exposed consumed key). This is the faithful port of the paper's
ToSAccVVerifyUnionMembership, which binds BOTH the incoming account state's
nullifier public key AND its tx commitment.

Genesis is closed because the InitialProof exposes consumed_pubkey = Pk₀ (bound to
the address via owner = H(Pk₀‖nk_commit)) and the first successor binds Pk_prev to
it; the mint-fork the same way via a mint's successor/receiver.

Remove all 'Known soundness gap' / contingent framing and rewrite clause 1, §1.4,
§2.2, §2.3.3 steps 2 and 4, §2.4, §3.7, §6.5 as unconditionally enforced. Add the
public-input-layout update (§2.5), a Requirement-2 privacy note (Pkᵢ is already the
public on-chain nullifier key; consecutive keys stay unlinkable, rotation hidden in
new_account_state_hash), and the proving-cost note (an equality on an exposed key,
not a new gadget).
…x() leaf-preservation

Follow-up hardening found by independent verification of the consumed-key closure.

A (disclosure-surface fresh-key substitution — was still open). The key binding
reached the in-circuit successor (clause 1 (iii)) and receiver (clause 10(d)) but
NOT the out-of-circuit disclosure verifiers, where the attacker is the subject: a
malicious subject could prove a valid C-proof of a fork-loser/never-anchored state
and point a link at a fresh-key naked nullifier (R S2C-opening H(pi.ProofData),
permissionless) to make it read completed / attest a false balance. Bind Pk to the
proof's own consumed_pubkey everywhere first-occurrence anchoring is asserted for a
third party: §5.6 step 3 (Pk_create == creating_proof.consumed_pubkey), §5.7 balance
attestation (Pk_anchor == pi.consumed_pubkey, statement 5), §5.8 address view and
§5.5 anchoring trail (inherit it), §1.5 creating_nullifier (tie Pk_create, not just
R_create), and update §3.7 to name disclosure verifiers alongside successor/receiver.

B (pin prefix() leaf-preservation). The same-key closure at recursion-internal hops
lifts each hop's witnessed w.nav to the canonical top only via prefix(); its
soundness needs prefix() to be a genuine LEAF-PRESERVING monotone SMT extension (an
extension must not alter an existing leaf). Pin it normatively in §3.7 (a forged
leaf Pkᵢ->R_loser at a deep hop cannot be a leaf-preserving sub-map of a canonical
accumulator whose first-occurrence leaf is R_winner), reference it from clause 1 /
clause 10(c), and clarify MAX_NAV_DEPTH=48 (prefix-chain admission count) is
orthogonal to the accumulator's 256-bit SMT key length.

C (editorial). §1.7.9: circuit_digest(C) reflects the new consumed_pubkey public
output. §2.5: pin its encoding — 8 Goldilocks field elements (secp256k1 NonNative
[U32Target; 8], as txn_pubkey), so C exposes 20 + 8 = 28 application public inputs.
…rification

Round-4 review consistency fix. The §2.6 proving-cost note still described the
clause-1 membership gadget as 'against MAX_NAV_DEPTH = 48', conflating the
admission-count bound with the membership path length — the exact confusion the
prefix()/orthogonality clarification removed elsewhere. Reword: the membership
gadget is a 256-sibling Poseidon path (the accumulator is 256-bit-keyed, §1.7.6),
bounded in admission count by MAX_NAV_DEPTH=48, not in path length (§2.5, §3.7).
Also add §5.8 to the §5.5 anchoring-trail disclosure-verifier cross-ref for
completeness.
…st-state + §3.10 cross-ref

Cosmetic parity/cross-ref completeness (soundness-neutral; both review lenses
flagged these as the only remaining nits):
- §4.5 recovery step 4: state the inherited Pk_create == creating_proof.consumed_pubkey
  binding explicitly (already covered by the re-run §2.3.3 receive checks).
- §7.5 latest-state selection: note each qualifying nullifier's key is bound to its
  transition's consumed_pubkey (re-run by the candidate's own recursive proof), so no
  fresh-key substitution qualifies.
- §3.10 completed-requiring cross-ref list: add §5.8 alongside §2.3.3/§4.5/§5.6/§5.7.
@TaprootFreak

Copy link
Copy Markdown
Contributor Author

Resolved — the clause-1 anchoring gap is closed end-to-end; moving out of draft

The soundness gap that held this PR at draft — every state-advancing transition in a coin's lineage must be first-occurrence-anchored, but the enumerated predicate did not enforce it for a transition that advances state without creating a delivered coin (pure receive / self-held mint / genesis-receive) — is now closed, verified across four adversarial review rounds (parallel logic/soundness + conformance lenses each round, plus an independent confirmation pass). Each round surfaced a deeper layer; all are now closed.

What was found, layer by layer

  1. The original gap (whole-lineage double-spend). A pure receive / self-held mint / genesis-receive rotated the account key with no verifier-enforced anchoring of its own (Pkᵢ, Rᵢ); prefix(prev.nav, w.nav) carried each ancestor's dependency set, never the ancestor's own nullifier leaf. Fork a receive at S₀, anchor one branch, spend the coin in each branch under distinct rotated keys — no on-chain collision, coin spent twice.
  2. Predecessor-nullifier check (leaf only) was insufficient. Binding only the leaf R_prev (via sign-to-contract to H(prev_proof.ProofData)) while leaving Pk_prev a free witness let a malicious prover publish a fresh-key naked nullifier whose leaf opens H(prev_proof.ProofData) and evade the collision. The same free-key gap independently affected clause 10(d)/receive-check-4 (Pk_create).
  3. The consumed-key binding — the faithful port of the paper's ToSAccVVerifyUnionMembership (the paper authors' feedback), which binds both the incoming account state's nullifier public key and its transaction commitment. C now exposes each transition's consumed key consumed_pubkey = Pkᵢ (the current_pubkey it spends and publishes as its on-chain nullifier key) as a public output, and every on-chain-nullifier membership check binds to it: clause 1 (iii) Pk_prev == prev_proof.consumed_pubkey, clause 10(d)/§2.3.3 step 4 Pk_create == creating_proof.consumed_pubkey. With the retained leaf/S2C check this is a two-way closure: the leaf pins a same-key fork loser (stored leaf is the winner's R), the key pins a fresh-key substitution.
  4. Disclosure-surface propagation. The key binding was extended to the out-of-circuit disclosure verifiers, where the attacker is the subject (proof-of-funds / confirmation links to a third party): §5.6 step 3, §5.7 balance-attestation statement 5 (Pk_anchor == pi.consumed_pubkey), §5.8, §5.5 anchoring trail, §1.5 creating_nullifier, with §3.7 updated to name disclosure verifiers. Recovery (§4.5), latest-state selection (§7.5), and the §3.10 cross-ref inherit/state the binding.
  5. prefix() pinned. The same-key closure at recursion-internal hops relies on prefix() being a genuine leaf-preserving monotone SMT extension (an extension must not alter an existing leaf), now pinned normatively in §3.7 — so a forged internal-hop leaf Pkᵢ → R_loser cannot survive up the chain to a canonical accumulator whose first-occurrence leaf is R_winner. MAX_NAV_DEPTH = 48 (admission count) is clarified orthogonal to the accumulator's 256-bit SMT key length.

Consistency / non-soundness updates

serialize(ProofData), H(ProofData), and test vectors V.4–V.6 are unchanged (the consumed key is a separate public output, not a sixth ProofData digest). §1.7.9 notes circuit_digest(C) reflects the new public output (an anti-circuit-substitution guard); §2.5 pins its encoding (8 Goldilocks field elements, [U32Target; 8], as txn_pubkey) — C exposes 20 + 8 = 28 application public inputs. Requirement 2 is preserved: Pkᵢ is already the public on-chain nullifier key, only the top proof's consumed key is public, consecutive Pkᵢ stay unlinkable (rotation hidden in new_account_state_hash).

Open item (honestly scoped, not a soundness hole)

The realizability of a fixed-size in-circuit gadget deciding the leaf-preserving-extension relation over a 256-bit SMT is an implementation/proving-cost question. The spec pins the relation (§3.7) and defers the gadget to the proving-cost / §1.7.8 review (§2.6, "not yet quantified"). The specification is sound independent of gadget efficiency.

The reverted paper-model anchoring is now complete and self-consistent. Ready for review.

@TaprootFreak
TaprootFreak marked this pull request as ready for review July 12, 2026 21:30
@TaprootFreak
TaprootFreak merged commit 6ca36ab into develop Jul 12, 2026
5 checks passed
joshuakrueger-dfx added a commit that referenced this pull request Jul 15, 2026
…align encodings

#97 already landed the on-chain half-aggregated state-nullifier model in
specification.md. Recast the remediation/analysis/assurance/risks framing
from 'accepted future direction' to 'normative via #97': F-01/F-02/F-04/F-06
are resolved in the spec (with § citations), Gate A is closed, and only the
executable-conformance (vectors) and assurance (proofs/backend/audit) gates
remain open.

- risks.md: drop the retired public-BatchBundle/root-prefix ledger-safety
  claim and state the residual data-availability risk in V3 terms (§3.6/§4.6).
- §1.1: align the domain strings and encodings with the shipped spec
  (m_state = 'zkCoins/v1/StateUpdate', H(ProofData), x-only R).
- paper-conformance-analysis.md: English-only per CONTRIBUTING — English
  title and H1 (H1 above the snapshot blockquote), executive summary translated.
joshuakrueger-dfx added a commit that referenced this pull request Jul 15, 2026
Adds the paper-deviation analysis (byte-pinned mirror of research#22) and the
paper-conformance remediation register, and wires the release-gate banners into
risks.md and assurance.md.

Framed to reflect that PR #97 already made the on-chain half-aggregated
state-nullifier model normative in specification.md: F-01/F-02/F-04/F-06 are
resolved in the spec (Gate A closed), leaving only executable-conformance
(vectors) and assurance (proofs/backend/audit) gates open. The §1
AggregateStateNullifierV3 object matches the shipped §3.5/§3.8 (no on-chain fee
field or network_id; marker/version/format/u16-count/block_anchor layout).
Documentation is English-only per CONTRIBUTING.

Stacked on #99, which owns the risks.md/assurance.md body cleanup to the V3
model; this PR adds only the remediation banners and gate rows on top.
joshuakrueger-dfx added a commit that referenced this pull request Jul 15, 2026
… (V3)

Re-architects the v2 supply cap for the V3 on-chain half-aggregated
nullifier model (#97), which removed the per-nf accumulator the original
nf_mint mechanism relied on.

V3-native enforcement: a v2 (capped) asset MUST be minted in the issuing
account's genesis transition (send_counter == 0, current_pubkey == Pk0),
so the mint consumes Pk0 and publishes (Pk0, R) on Bitcoin. Because
asset_id binds creator_pubkey = Pk0 and every Pki is admitted to the global
accumulator at most once by first-occurrence — across all accounts sharing
Pk0 — the asset can be minted at most once globally. That single mint emits
amount <= cap_total (exact wide-integer check), so total supply is provably
<= cap_total and auditable by any holder from asset_terms. No nf_mint and no
supply-specific accumulator: Pk0 first-occurrence is the uniqueness anchor.

Adds AssetIdV2/IssuanceTermsV2 derivations (§1.4/§1.1), the v2 asset_terms
payload (cap_total/terms_salt) and its §7.1 wire encoding, the version-
dispatched receiver recompute (§2.3.3 step 6), and the v2 mint circuit
branch (§2.1 clause 3 / §6.5).
TaprootFreak pushed a commit that referenced this pull request Jul 17, 2026
Adds the paper-deviation analysis (byte-pinned mirror of research#22) and the
paper-conformance remediation register, and wires the release-gate banners into
risks.md and assurance.md.

Framed to reflect that PR #97 already made the on-chain half-aggregated
state-nullifier model normative in specification.md: F-01/F-02/F-04/F-06 are
resolved in the spec (Gate A closed), leaving only executable-conformance
(vectors) and assurance (proofs/backend/audit) gates open. The §1
AggregateStateNullifierV3 object matches the shipped §3.5/§3.8 (no on-chain fee
field or network_id; marker/version/format/u16-count/block_anchor layout).
Documentation is English-only per CONTRIBUTING.

Stacked on #99, which owns the risks.md/assurance.md body cleanup to the V3
model; this PR adds only the remediation banners and gate rows on top.
TaprootFreak added a commit that referenced this pull request Jul 17, 2026
Three drifts against the post-#97 spec:
- fee section described the paper's first-to-publish-wins gossip race as the
  current model; spec §3.8 adopts spender-picks-publisher in v1 and defers the
  race. Rewrote the publisher-role bullets to v1 (pick a publisher, one fee
  coin under the shared ocr, re-pick/self-publish on censorship) and marked the
  gossip race as the deferred upgrade.
- trade-off table called the 231-byte root chain 'current'; it is retired by #97.
- a mint-verified sentence still read 'the specification must remove'; #97 removed it.
TaprootFreak added a commit that referenced this pull request Jul 17, 2026
* docs: paper-conformance remediation register and analysis

Adds the paper-deviation analysis (byte-pinned mirror of research#22) and the
paper-conformance remediation register, and wires the release-gate banners into
risks.md and assurance.md.

Framed to reflect that PR #97 already made the on-chain half-aggregated
state-nullifier model normative in specification.md: F-01/F-02/F-04/F-06 are
resolved in the spec (Gate A closed), leaving only executable-conformance
(vectors) and assurance (proofs/backend/audit) gates open. The §1
AggregateStateNullifierV3 object matches the shipped §3.5/§3.8 (no on-chain fee
field or network_id; marker/version/format/u16-count/block_anchor layout).
Documentation is English-only per CONTRIBUTING.

Stacked on #99, which owns the risks.md/assurance.md body cleanup to the V3
model; this PR adds only the remediation banners and gate rows on top.

* docs: reconcile the remediation register with the normative spec

Three drifts against the post-#97 spec:
- fee section described the paper's first-to-publish-wins gossip race as the
  current model; spec §3.8 adopts spender-picks-publisher in v1 and defers the
  race. Rewrote the publisher-role bullets to v1 (pick a publisher, one fee
  coin under the shared ocr, re-pick/self-publish on censorship) and marked the
  gossip race as the deferred upgrade.
- trade-off table called the 231-byte root chain 'current'; it is retired by #97.
- a mint-verified sentence still read 'the specification must remove'; #97 removed it.

---------

Co-authored-by: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com>
TaprootFreak added a commit that referenced this pull request Jul 23, 2026
…ion-readiness pass) (#111)

* docs: make the v1 assurance model self-contained — no external gates, instantiation final, surface frozen

Project decision (2026-07-22): v1 ships without an external audit or any
other human-gated mainnet step. The assurance weight moves to the executable
conformance harness and the spec's internal soundness argument.

- spec 1.7.8: reference instantiation is final for v1 (was: pending
  cryptographic review); any refinement is a version bump. New normative
  'v1 freeze' clause: circuit shape (C, C_balance), 1.7 encodings and the
  section-7 wire formats are frozen once the digests are pinned;
  IssuanceTerms_v2 is part of the initial circuit build.
- assurance: mainnet gate rewritten to machine-checkable criteria; external
  audit removed from workstream 2; paper proofs demoted to a quality goal.
- remediation: F-05 closed (immutable plonky2 1.1.0 stays pinned), F-07
  closed for v1, Gate C rewritten without external-review steps.
- risks: the four residuals (hosted-prover redirect, co-output ocr,
  publisher ash-chain, fee pricing) are now accepted, final v1 boundaries
  (register IDs D-17..D-20), no longer open design work.
- mandate: future-review escape hatch replaced by the version-bump rule.

* spec: close the normative defects blocking autonomous implementation

- S2C tweak preimage unified to t = H(bytes(R') || H(ProofData)) everywhere
  (clause 2 carried an undefined tag placeholder that contradicted 3.2/1.7.10).
- awaiting_signature now surfaces all five ProofData fields plus
  proof_data_hash; the wallet MUST recompute H(ProofData) itself before
  signing (fail-closed), and the /sign body key is canonically s2c_nonce.
- v2 mint witness lists cap_total/terms_salt (present iff version 2).
- clause 7 balances update is an exact per-asset wide-integer equation.
- clause 8 coin-history updates are constrained two-root transitions with
  explicit prior-leaf checks (admit requires H'_leaf(0), spend H'_leaf(1))
  over witnessed history_update_paths — in-circuit replay-guard.
- crediting may never rest on a single foreign Path-B answer: own Path A
  or agreeing multi-node fan-out, discrepancies fail closed.
- asset_terms with an unknown issuance_version is malformed (receive step 6
  and the 7.1 wire rule).
- the 6-confirmation receive floor is hard (MAY more, MUST NOT fewer).
- gadget freedom ends at the digest pin: network conformance reproduces the
  reference circuit exactly.
- fuzzy message detection is explicitly not part of v1.
- hash-derived scalars get big-endian interpretation and reduction rules.
- privacy claims scoped precisely: publisher-link sentence corrected, 2.4
  privacy row scoped to the on-chain observer, and a normative 'Precise v1
  privacy statement' added to 6.7 (D-17..D-20 boundaries).

* spec: pin the section-7 wire surface byte-exactly

- 7.5: normative TransitionRequest JSON schema (kind-dependent presence
  rules, fail-closed malformed handling; receive/fee-less publication
  clarified) and exact pull/challenge + pull bodies mapping the 5.1
  objects onto the wire.
- 7.8: complete normative kernel.v1 proto contract for all 14 procedures,
  with the API-layer/kernel authority boundary made explicit (chan_bind is
  an opaque equality token kernel-side).
- 7.6: publisher body field encodings pinned; fee-less hand-off defined.
- 7.4: Blossom authorization event fully specified (kind 24242, tags,
  rejects, idempotent PUT response).
- 7.1: nested CoinProof fixed-field layouts and a closed malformed-bundle
  enumeration.
- 7.5: machine_code is now a closed enumeration with HTTP mappings;
  unknown conditions map to internal_error.

* spec: complete the executable conformance harness

- 3.2: even-y normalisation (step 1b) and tweak/nonce redraw rule (step 3b)
  — as previously written, roughly half of all signing attempts would have
  produced a signature no BIP-340 verifier accepts; 1.7.10 Sign() aligned.
- New V.8: fully pinned synthetic signing + NISSHAC half-aggregation
  fixture (SHA-256/secp256k1 only; computed twice independently,
  byte-identical, all algebraic self-checks pass). Covers both key
  normalisation branches and a deterministic fixture nonce rule.
- New V.9: normative negative-control table (mutation, aggregate
  tampering, duplicate member, malformed encodings, wrong network,
  bounded-reorg behaviour).
- V.2-ext: the real BIP-39/BIP-32/HKDF derivation chain pinned end to end
  (reference mnemonic, all-hardened paths, nav_rand@0/1).
- V.4 + /v1/info now pin circuit_digest(C_balance) alongside C.
- V.7: node<->SDK parity matrix (byte-equal vs verify vs node-only).
- mandate: the A-to-Z suite is a machine-evaluable pass predicate with
  fixed fixtures and hard assertions, including reorg, restore
  (Requirement 6) and portability (Requirement 10) controls.

* mandate: add the normative 10-step path-to-mainnet runbook

Ordered, machine-followable sequence from first circuit build to mainnet
activation — every step with explicit inputs, outputs and a hard pass
predicate, no human-gated stop (project decision 2026-07-22). The
assurance gates now name the runbook as their executable form.

* docs: make the deviation register live again + align conformance claims

- paper-conformance-analysis: the audit stays a pinned snapshot, but the
  register table becomes the living index CONTRIBUTING points to — new
  authoritative 'Status (current develop)' column (six pre-#97 rows marked
  RETIRED, D-06 superseded by D-16, D-12 decided, D-13 partially
  superseded by token standard 2) and new rows D-17..D-20 for the
  accepted v1 boundaries.
- spec: new 'Relationship to the source papers' section naming every
  load-bearing v1 deviation; requirements-traceability rows for Req 3
  (freeze resistance) and Req 4 (own-node re-verification) fixed; intro
  box now references the register instead of claiming 'faithful'.
- protocol/comparisons: construction-identity overclaims replaced by
  register-referencing wording (CONTRIBUTING paper-conformance rule).
- remediation: Gate B wording matches the single-implementation reality;
  F-08 matrix timing concretized.
- requirements: Req 9 partial (single-anchor attestation) signed off as
  final for v1.

* spec: avoid MDX JSX evaluation of a brace set in V.2-ext/V.8 prose

* docs: acceptance-sweep remediation — mirrors, dimensioning, closed verdicts

specification.md:
- S2C tweak mirrors unified (5.7 statement 4, glossary, remaining sites);
  glossary Path B / nf / Mint / Circuit digest entries aligned with the
  normative sections (the nf entry wrongly claimed on-chain disclosure).
- V.3 worked example fixed to the post-mint state (coin_history_root@0,
  not the empty root) — it contradicted the exact clause-7 equation.
- unknown inscription version/format bytes are fail-closed (3.5).
- 5.7 statement 5 split: in-circuit key binding vs host-side anchor
  checks (inscription, first occurrence, completed).
- clause-1 dependency attribution corrected (receives via 10(d), inputs
  inductively via the prefix carry-forward).
- privacy scoping: clause 9 counterparty scope, D-19 generalised to any
  holder of two consecutive CoinProofs, D-18 output-count bucket.
- 7.6 fee-less hand-off rule made single and consistent (no publisher-side
  S2C without a fee CoinProof; receivers verify downstream).
- new MAX_HISTORY_UPDATES = 20 circuit constant with no-op slots (2.5);
  C_balance public-input layout pinned (2.5); closed network-tag set
  incl. zkCoins/v1/regtest with 1:1 short-name mapping; Path-B response
  byte layout + tip_block_hash; /sign field widths; SMT big-endian bit
  rule; Idempotency-Key semantics; ECDH x-only lift convention (1.1).
- V.8 label strings explicit; V.9 gains N-11..N-15; N-10 requires
  fail-stop (health not ready) — mirrored as a 3.9 MUST; V.7 step 5 uses
  curve-valid fixture keys; parity matrix: SDK reproduces Poseidon values
  byte-equal, only circuit digests are node-only.

other docs:
- risks: closed 'Verdicts at a glance' table (18 rows, none open) — the
  mainnet incentive gate is now machine-checkable; D-18/D-19 scope notes.
- assurance: verdict enum + accepted-v1-boundary, WS1 output = the risks
  table, testnet gate deep-review replaced by hard predicates, mainnet
  gate names Gates A–C.
- remediation: 6-conf floor wording, Gate C machine-checkable, security
  package explicitly non-gating, F-08 paper-baseline column corrected.
- analysis: snapshot sections declared non-release-governing; D-12/D-14
  evidence cells superseded; D-09 verdict disentangled from D-20.
- protocol/comparisons: privacy claims aligned with 6.7; 'full
  specification' points at the spec, not the paper.
- mandate: runbook step 3 REGEN predicate honest about verification-locked
  cells; A-to-Z gains a second (capped, v2) asset — multi-asset discipline.

* spec: crediting is Path-A-only; pin the V.10 note-encryption fixture; testnet = Signet

- Crediting decisions now rest exclusively on the verifier's own Path-A
  accumulator (project decision: one mechanism, no multi-node quorum
  variant). Path-B answers are display/delegation only and can never back
  a credit — stated in 2.3.3 step 4, 3.7 and the glossary.
- New V.10: the 1.3 note-key chain (esk/epk/IVPK/ss/K_tx/K_out/kb) pinned
  end to end under the 1.1 x-only ECDH lift — computed twice
  independently, byte-identical; parity-matrix row added. detect_tag stays
  REGEN (Poseidon), NIP-44 covered by its own vectors.
- Runbook step 7 pins the public test chain to Bitcoin Signet.
- Grammar fix in the clause-1 dependency bullet.

* docs: resolve internal inconsistencies flagged in review

- V.4: rename nav_rand@0 -> nav_rand_sample@0 (collided with the real HKDF
  value of the same name in V.2-ext); no pinned bytes changed
- 3.2: correct 'pure receive publishes nothing' — a pure receive publishes
  its own on-chain state nullifier (2.3.3, 3.10)
- Req-3 summary: the spending key is held only by the wallet, not 'no component'
- publisher body: 'no coin plaintext' -> no payment/change plaintext; the fee
  CoinProof addressed to the publisher is the necessary exception
- add zkCoins/v1/regtest to the network-tag list
- block_anchor.height: align REST body + kernel proto to the on-chain u32,
  with an explicit out-of-range rejection
- kernel.v1: add EntrustOperationalBundle / RevokeOperationalBundle procedures
  (+ proto messages) backing the /v1/bootstrap/entrust and /revoke endpoints
- comparisons/spec: replace unqualified superlatives with verifiable claims

* docs: scope conservation to no-inflation; fold change-drop into D-17

Clause 3 enforces only no-inflation (Out <= In) in-circuit. The earlier
'funds are conserved' overclaimed: returning the difference as change is part
of the node-built output_templates witness, not a separate in-circuit check.
A dishonest self-selected prover can therefore redirect OR drop (burn) the
change - the same accepted v1 boundary D-17 as send-output redirection (the
wallet trusts its own node; the trustless path is self-hosting, Req 4).

- specification.md clause 3: precise no-inflation wording + D-17 reference
- specification.md 6.6 (both trust-config statements): redirect-or-burn
- paper-conformance-analysis.md D-17: scope extended to redirect-or-burn
- risks.md: table row, heading, risk + mitigation aligned to redirect-or-burn
- no-inflation mechanism (wide-integer >=) unchanged; the across-account
  no-inflation meaning of the requirements-traceability row kept intact

* docs: close the real section-7 wire gaps (receipt-push endpoint + minor completions)

- 7.5: add GET /v1/receipts/stream - the public, pull-session-gated (chan_bind)
  SSE front of kernel.v1 SubscribeReceipts (4.9 push source); typed Receipt event
- 7.1: canonical self-delivery-record wire layout (CoinProof + M x {epk, out_ciphertext})
- 7.8: Scope message invariant (exactly one of all_assets / non-empty asset_ids)
- 7.4 + /v1/info + kernel.v1 Info: advertise max_blob_bytes; Blossom DELETE returns
  200, or 409 retention_hold when the blob is still a required replica
- 7.6: publisher reason as a closed enum, batch_eta typed as u64 seconds
- 7.8 SubscribeReceipts row now maps to GET /v1/receipts/stream

Job-SSE union and out_ciphertext were already fully specified; no redundancy added.
No test vectors / REGEN touched.

* docs: escape pipe chars in the publisher reason enum (MDX table build fix)

The unescaped | in reason?: "..." | "..." split the §7.6 table cell, which
tore the inline-code span and left { in prose -> MDX expression error. Escaped
to \| so the cell (and the { accepted, ... } span) stays intact.

* docs: round-2 acceptance fixes — flows, wire gaps, harness corrections

specification.md:
- 2.3.1/2.3.2 wallet/node step order now matches the 7.5 handshake (intent
  first, sign after the witness-determined ProofData is surfaced).
- 2.6 cost catalogue includes the in-circuit foreign-nullifier S2C
  openings (clause 1(ii) + 10(d)) — part of the pinned shape.
- change formula includes Mint(a); stray 'pure receive publishes nothing'
  corrected; clause-1 induction covers clause-8 self-outputs.
- public-input limb encoding pinned for C and C_balance; kernel.v1 gains
  EntrustBundle/RevokeBundle plus AttestBalance/IssueViewGrant with REST
  counterparts (/v1/attest/balance, /v1/grants) — Requirement 9 now has an
  executable surface; bootstrap bodies exact.
- invoice_message framing and ViewGrant payload byte-exact;
  idempotency_conflict code; format-0x00 count rule; NullifierPath REST
  field; V.4 gains detect_tag/AssetIdV2/terms-hash rows; V.5 cell and V.7
  step 5 single-sourced; N-01 canonical case; privacy mirrors (2.4 row,
  5.6 scoped note, 6.7 D-17 breadth, glossary nav_opening).

mandate/README/others:
- A-to-Z corrected: v2 asset genesis-minted by a third account (Carol)
  with explicit outputs (6.5 forbids self-credit), Bob's balance asserted
  after the receive transition, Alice arithmetic fixed (999_749_000), new
  attestation and grant controls; build report defined as a normative
  artefact; step-7 predicate excludes regtest-only controls; freeze timing
  consistent (in force since the vectors pin); README testnet = Signet;
  analysis evidence cells closed; remediation NISSHAC import non-gating;
  CONTRIBUTING structure lists all living pages; risks hosted-prover text
  reflects the five-field recompute.

* spec: unify the kernel entrust/revoke boundary after the parallel-session rebase

Both lanes independently added the missing bundle procedures; keep one
consistent authority model (7.8 rule: the API layer runs the 5.1 gate,
the kernel never re-verifies and binds chan_bind opaquely) — the
kernel-side OwnershipProof variant is removed, the fail-closed erasure
comment is preserved, table descriptions reworded accordingly.

* docs: close the gaps for autonomous implementation — append-only-log accumulator (D-05) + wallet-verifiable rotation (npk_commit, D-21) (#114)

* docs: resolve the mechanical findings of both PR-111 reviews

- Requirement 3 scoped precisely to the holder's own delegation boundary
  (the self-selected prover's D-17 redirect/burn limit) instead of an
  absolute 'no one can steal' that risks.md itself contradicts.
- D-number ranges made consistent (privacy D-17..D-19 in 6.7; D-16 in 3.9;
  D-20 in the risks verdict table); Gate C reference fixed.
- publisher 'never holds a coin/proof' gains its one exception (its own fee
  coin's CoinProof); glossary Publisher/Half-aggregation/issuance_version
  entries corrected.
- /v1/attest/balance job now fits the kind/result contract
  ('attest_balance' + attestation field); clause-4 Path-B leftover removed
  (crediting is Path-A-only); C_balance statement binds R_anchor to the
  signature nonce; witness lists the 256-sibling membership paths clause
  1(i)/10(d) require.
- A-to-Z credit timing consistent with the deferred-credit model; three
  accounts named; runbook gains the full-stack step 4b; assurance REGEN
  exception and the expected-attempts wording corrected.

* spec: replace the nullifier-accumulator SMT with an RFC-6962 append-only Merkle log

Fixes the core soundness defect: the leaf-preserving SMT-submap prefix()
was not provable in constant circuit size (linear in the foreign
insertions between two of an account's transitions), and the naive
weakening reopened a buried-fork-loser double-spend.

- 1.7.6: the global nullifier accumulator is now a Certificate-Transparency
  Merkle tree (RFC 6962 / RFC 9162 over Poseidon) over the canonical
  first-occurrence sequence; first-occurrence = append, later occurrence =
  skip; leaf binds its position; nav_root binds size; a local Pk->(pos,R)
  index serves out-of-circuit first-occurrence/non-membership.
- 3.7: prefix() becomes an RFC-6962 log-consistency proof and membership an
  inclusion proof, both written out normatively (SUBPROOF/PATH recursion,
  integer split point k = largest power of two < n). Constant-size,
  independent of n-m. Leaf-preservation is now a theorem (consistency
  composes; first-occurrence gives one winner per key), closing the
  buried-fork-loser without a per-leaf submap.
- The construction is a port of a peer-reviewed primitive (CT log
  consistency); the exact algorithm was differential-tested against an
  RFC-6962 reference by two independent implementations (0 soundness
  violations over ~4300 cases). The in-circuit arithmetization of the
  consistency recursion remains the one element flagged for human
  cryptographic review (1.7.8).

Coin-history SMT, on-chain object, and the ProofData layout are unchanged.
MAX_NAV_DEPTH, the witness fields, the finality gate, C_balance and the
test vectors follow in subsequent commits.

* spec: align the compliance predicate with the append-only-log accumulator

- 2.1 witness: nav_prefix -> nav_consistency (+ size_prev), the 256-sibling
  membership paths -> RFC-6962 audit paths (nav_inclusion /
  creating_nav_inclusion) with u64 positions pos_prev/pos_create, and
  nav is now the (size, mth) log value committed as
  nav_root = Hc("NfLog/Root", size || mth).
- clause 1: nav_commitment opens nav_root; prefix(prev.nav, w.nav) is the
  RFC-6962 log-consistency relation; the predecessor membership check (i)
  is an RFC-6962 inclusion at pos_prev < size; genesis is the trivial
  empty-log consistency.
- clause 10(c): consistency prefix(r_nav || w.nav) with size_r <= size and
  the sender's minimal-covering size rule; 10(d): inclusion at
  pos_create < size. Key/leaf bindings (1(iii)/10(d)/9) unchanged.

MAX_NAV_DEPTH, the finality gate, the sender size rule, C_balance and the
test vectors follow next.

* spec: dimensioning, finality gate and sender rule for the accumulator log

- 2.5: MAX_NAV_DEPTH is now H_MAX = 64, the Merkle-log height (log2 of the
  entry count); inclusion <= H_MAX hashes, consistency <= 2*H_MAX. The old
  '48 vs 2^48 / orthogonal to the 256-bit key' muddle is gone (a position-
  indexed log has no separate key length).
- 3.9: define size_final = the log prefix whose inclusion blocks have >= 6
  confirmations (height <= tip_height - 5, from the 5.6 confirmation count
  tip_height - height + 1 — 6 conf, not 7). A committed nav and a balance
  attestation MUST authenticate only positions < size_final, so an ordered
  log's reorg-order sensitivity stays a liveness cost, never a double-spend
  lever (a <=5-block reorg touches only positions >= size_final).
- 2.3.3 step 2: the receiver MUST additionally require w.nav.size <=
  size_final. 2.3.2 step 5: the sender sets nav to the minimal covering
  log size <= size_final (all dependencies must be final, else it waits).
- 2.6 cost note reworded to the log inclusion/consistency gadget;
  nav_commitment preimage is nav_root everywhere.

* spec: C_balance on the log, residual-review note, and V.11 vectors

- 5.7: the balance-attestation circuit C_balance takes the same gadget
  change — prefix(nav, nav_ceiling) becomes an RFC-6962 log-consistency
  proof (size <= size_ceiling), nav_root binds size||mth, and the verifier
  requires nav_ceiling.size <= size_final (the 3.9 finality gate).
- 1.7.8: a normative residual-review note names the in-circuit RFC-6962
  consistency verifier's arithmetization as the one element recommended for
  human cryptographic review before mainnet, with a differential-test
  obligation against an RFC-6962 reference at the 2^k boundaries.
- V.4: the retired SMT empty root E256 is replaced by nflog_empty =
  Hc("NfLog/Empty", 0); no NfAcc/* constants remain.
- new V.11: nullifier-accumulator-log conformance vectors (MTH, inclusion,
  consistency at the 2^k-1/2^k/2^k+1 boundaries) and a normative negative-
  control table (non-prefix, forged-leaf, position-swap, size-inflation,
  out-of-range, buried-fork-loser, non-final-position), all differential-
  tested against an RFC-6962 reference before pinning.

* docs: register the accumulator-log redesign (D-05) honestly

- paper-conformance-analysis D-05 status: DECIDED — the accumulator is now
  an RFC-6962 append-only Merkle log; prefix is a constant-size
  log-consistency proof (succinctness/soundness gap fixed), still a pure
  function of the on-chain nullifiers; registered as a CT-consistency
  (ordered-sequence) port, distinct from the paper's ToS set IsPrefix.
- risks 'Accumulator history relation': the prefix-succinctness defect is
  fixed, with two honest caveats — the ordered log is more reorg-order-
  sensitive than the order-independent SMT (a liveness cost only, bounded
  by the size<=size_final finality gate, never a double-spend lever), and
  this is a CT-consistency port, not the paper's ToS IsPrefix. The hard
  6-confirmation finality (D-16) and its rationale (the absent
  DistinctElement no-op, #105) are unchanged.

* spec: wallet-verifiable key rotation via a sixth ProofData field npk_commit

Closes the hosted-prover rotation-capture (Requirement 5): a node could
previously fold a next_pubkey of its own into new_account_state_hash
(Poseidon, which the thin wallet cannot recompute) and have the wallet
blind-sign it, capturing the account's future spend authority.

- new sixth ProofData field npk_commit = H("zkCoins/v1/NpkCommit" ||
  next_pubkey || npk_rand) (SHA-256, wallet-native); serialize(ProofData)
  160 -> 192 bytes; C application public inputs 28 -> 36 (npk_commit is a
  non-Poseidon 32-byte value, 8 u32 limbs).
- clause 2 opens npk_commit against w.next_pubkey/w.npk_rand; the custody
  S2C signature commits H(ProofData) over it, so a substituted rotation
  key is caught.
- 7.5 awaiting_signature surfaces npk_commit; the wallet MUST recompute it
  from ITS OWN next_pubkey + fresh npk_rand and refuse to sign on a
  mismatch (fail-closed) -- this is what makes the rotation verifiable.
  TransitionRequest and the kernel AwaitingSignature carry npk_rand /
  npk_commit; npk_rand is fresh per attempt, never reused (a reuse would
  re-link two transitions).
- 1.4/2.1/2.5/glossary/V.4 updated for the six-field 192-byte ProofData.
  The V.8 signing fixture is recomputed at 192 bytes in the next commit.

* spec: recompute the V.8 signing fixture at 192-byte ProofData

The sixth ProofData field npk_commit grows serialize(ProofData) to 192
bytes, changing m_SC and every dependent signature/aggregate value. The
V.8 synthetic fixture is recomputed accordingly: each ProofData gains a
sixth npk_commit field, and m_SC, t, R, e, s, z, a1, a2, s_agg follow (the
pre-tweak nonces R', keys and the five original fields are unchanged).
Values computed twice independently (two model families), byte-identical,
all BIP-340 / CommVerify / AggregateVerify self-checks pass. V.1 pins the
SHA-256-computable npk_rand@0 / npk_commit@0 for the V.4 H(ProofData@0)
formula.

* docs: protocol.md accumulator row follows the Merkle-log redesign (fix broken anchor)

* spec: propagate the append-only-log accumulator to the remaining SMT mirror sections

§1.6 tree table, §3.7 (opening, Path-B, Storage), §2.5 dimensioning, the
glossary, §7.1 nav_opening, §7.5/§7.8 Path-B REST+proto, §2.3.2/§2.3.3 nav
prose and V.4 nav_empty now describe the RFC-6962 append-only Merkle log
instead of the retired 256-bit nullifier SMT. u64 position/size absorbed as
8-byte big-endian byte strings per §1.7.2. The per-account coin-history SMT is
unchanged.

* docs: propagate the two crypto-blocker fixes to the register, mandate and protocol

Register the append-only-log accumulator (D-05, log-consistency prefix) and the
sixth wallet-verifiable ProofData field npk_commit (192-byte serialize, new
deviation D-21) across the paper-conformance analysis and remediation notes, the
implementation mandate and protocol.md. Pinned-snapshot columns kept intact;
only the living/normative statements were updated.

* spec: propagate the six-field/192-byte ProofData to the remaining mirror sections

npk_commit is the sixth ProofData field: §2.5 public-input count (20 + 8 + 8 =
36, read map 28+8), §1.4 field enumeration and serialize, §1.7.2 concatenation
note, §2.6 in-circuit SHA-256 inventory point (e), §7.8 kernel-proto npk_rand,
V.9 N-01 fuzz range (192), the Requirement-5 traceability row and the send/mint
flow prose now describe six fields / 192 bytes. Add the V.11 pinned sample-leaf
sequence so the log vectors are reproducible, and retire the E₂₅₆ token from the
V-determinism summary in favour of E'₂₅₆ + nflog_empty.

* docs: close four residual go-live gaps from the acceptance sweep

- V.9: add negative controls N-16/N-17 so Requirement 5 is testable — the
  wallet MUST refuse to sign when the surfaced npk_commit (or proof_data_hash)
  does not match its own next_pubkey/npk_rand recomputation (§7.5 fail-closed).
- V.7 parity matrix: add the V.11 append-only-log vectors so the log
  conformance is a byte-equal node↔SDK cross-check, and generate them in the
  runbook (implementation-mandate step 2 now lists V.11).
- V.11 intro: state that the RFC-6962 construction structure — not the still
  <REGEN> byte vectors — was differential-tested, avoiding an overclaim.
- paper-conformance-analysis: the living maintenance note now names the pinned
  external-audit action items (§§8–10) as superseded and not a v1 release gate.

* spec: pin the per-network scan origin and tie up three log/balance residuals

- §3.6/§1.7.9: pin a consensus-critical activation_height per network. Because
  the accumulator log is position-bound (Hc("NfLog/Leaf", p ‖ Pk ‖ R)), the
  scan-origin height is a consensus parameter — nodes starting at different
  heights assign different positions and every inclusion/prefix/nav diverges.
  The origin is now pinned (mainnet at deployment, runbook step 9), foreclosing
  a pre-deployment-inscription split.
- §5.7 statement 6: bind the disclosed nav_ceiling to a committed log root
  (Hc("NfLog/Root", size_ceiling ‖ mth_ceiling)) so the balance-attestation
  prefix check runs between two committed roots, not free witnesses.
- Glossary ProofData: six fields / 192-byte serialize; §1.7.8 residual-review
  note now also names the inclusion-PATH gadget.

* spec: finish the append-only-log/6-field propagation to the remaining mirrors

§2.2 on-chain anchoring, §1.4/§2.1/§3.6 accumulator API, the §3.7 insertion
paragraph (the log is order-SENSITIVE — positions are order-bound, unlike the
retired set-keyed SMT), §1.7.6 coin-history/empty-log notes, the V.4/V.7/V.11
harness rows, the source-paper relationship (D-05 = the CT-consistency port),
and §1.7.8 (differential-test discharges the review, not a v1 gate). Also make
the ViewGrant Bech32m payload signature-recomputable (carry the nonce, unify the
asset_ids discriminator) and pin concrete per-network activation_height values
plus /v1/info + kernel Info exposure.

* docs: correct the living gate/register/mandate bodies to the log + 6-field model

The main prose (not just parenthetical notes) of the remediation gate doc, the
implementation-mandate D12 decision and the D-05 register row now describe the
192-byte six-field ProofData and the RFC-6962 append-only-log accumulator, and
D-05 gets an explicit release gate (the V.11 differential-test). Runbook step 9
also checks the pinned activation_height.

* spec: set the conditional NAV to the shared size_final prefix (privacy + soundness)

The conditional NAV is now the SHARED >=6-confirmation-final prefix size_final,
not a per-account minimal-covering size. size_final is identical for every prover
at a given tip, so the fee-coin nav_opening reveals only a shared global ordinal,
not the account's activity — closing the receive-recency linkage the minimal-
covering size leaked. Building against a still-pending dependency is allowed only
as proving-pipelining: the nullifier MUST NOT be broadcast until every covered
position is final, otherwise a tolerated reorg could strand Pki as the first-
occurrence winner with a non-canonical nav and brick the account.

Adds the §3.7 canonical-value predicate (mth = MTH(D[0:size]) on the verifier's
scan; creditable iff size <= size_final), reframes NL-7 as deferred-not-rejected,
defines serialize(BalanceAttestation) in §7.1, binds nav_ceiling to a committed
log root and defaults the REST/kernel nav_ceiling to size_final, and aligns the
privacy narrative (spec + risks D-17/D-18/storage) with the size_final model.

* spec: finish nav=size_final across mint/receive/clause-10c + close attestation gaps

Propagate the shared-size_final conditional NAV to the mint (§2.3.1), receive
(§2.3.3) and cross-account (§2.1 clause 10c) flows and the §5.6 privacy note —
the size_final default reached only §2.3.2 (send) before. Define size_final
precisely in §3.9 (size at height <= tip-5, 0 when tip<5) and reconcile it with
proving-pipelining. Pin activation_height to a unique value (the genesis-
inscription block height) and define the network parameter set as a published
artefact. Make size_ceiling a C_balance public input (so serialize(Balance-
Attestation) matches the PI list), make /health/ready return the byte-unique
nav_root, add the nav_size proving-pipeline request field, and enumerate the
V.11 inclusion vectors.

* docs: propagate size_final + append-only-log framing to the gate/risk/mandate docs

remediation §3.2 takes the conditional NAV at size_final (not the live tip) and
names the CT-log accumulator as deviation D-05 (not paper-compatible); the
risks.md storage verdict and the mandate D12 decision follow the size_final /
append-only-log model; the A-to-Z reorg assertion names the concrete (size, mth)
/ nav_root value.

* spec: close the §7 wire-consistency and register-framing residuals

Every §7 accumulator surface now names the byte-unique nav_root (not the bare
mth); the attest-balance body carries size_ceiling for a non-default ceiling and
its job has no awaiting_signature phase; network-params.json gets a byte-exact
canonical encoding and activation_height is the observed genesis-inscription
height (no chicken-egg); §3.9 separates authenticating a position from crediting
it; the source-paper relationship lists D-05 among the load-bearing deviations;
the §6.7 Requirement-5 row names npk_commit; and the Passkey seed is a named
HKDF-SHA-256 construction.

* docs: size_final-by-default framing and D-05 attribution in the register/risks

remediation states the conditional NAV is size_final by default with proving-
pipelining as the opt-in, and ties D-05's V.11 differential-test to the runbook
conformance step; the risks verdict row credits the CT-log consistency port to
D-05 and keeps D-16 as the separate bounded-finality boundary.

* spec: final wire/harness/§3.9 consistency fixes

Every §7 accumulator surface reports the field root = nav_root = Hc(NfLog/Root,
size ‖ mth) with a consistent field name; §3.9 scopes the < size_final rule to
creditable NAVs with the pipelined-nav exception spelled out; V.11 disambiguates
the inclusion split point s from the size exponent and states NL-7 as a credit
rejection (deferred, consistent with the negative-control header); the testnet
activation_height is the observed Signet genesis-inscription height; and the
receive-path Path-B is an RFC-6962 log-inclusion proof, not an SMT path.

* docs: D-05 living column, honest deviation claims, and a machine-evaluable runbook

The D-05 register's living column describes the append-only RFC-6962 log and
frames its differential-test as a release gate (not a past-tense claim); intro.md
qualifies 'implements Shielded CSV' with the registered v1 deviations; and the
runbook step-8 gate is an explicit checklist while step 9 spells out the genesis
inscription, activation_height read, and network-params.json assembly.

* spec: remove proving-pipelining and close the codex-review findings

A soundness review showed the pipelining 'do not broadcast until final' rule is
not adversarially enforceable — the wallet cannot see the nav size behind the
hiding nav_commitment, and the node holds the signed SpendRecord and can publish
a non-final nav early, bricking the account under a tolerated reorg. v1 therefore
has NO proving-pipelining: nav is always size_final; a not-yet-final dependency
means the wallet waits. The §7.5 wallet recompute now also binds nav_commitment
to size_final (fail-closed). Also: mint takes nav = size_final in every case;
/v1/chain/accumulator reports (size, nav_root); the V.11 inclusion vectors are a
closed (p,n) list and NL-1/NL-2 are complete; and the Passkey seed HKDF is named.

* docs: drop pipelining from the register/runbook/risks and fix review findings

Remediation, the mandate D12 decision and the risks D-18 note follow the no-
pipelining nav = size_final model; remediation §1.3 describes Path-A anchoring
instead of non-existent CoinProof location fields; the runbook step-5 gate lists
the encoding/signature negative controls (moving the reorg cases N-09/N-10 to
step 6 with their own convergence/fail-stop predicates) and the V.11 + D-05
differential-test; and intro.md drops the 'true privacy' marketing overclaim.

* spec+risks: address the second codex review round

Remove the §7.5 wallet check (c): a thin wallet runs no Poseidon and no scan
loop, so recomputing nav_commitment was unimplementable — the pipelining removal
already forecloses the honest-node reorg brick, and the malicious-own-node case
is the accepted D-17 thin-client boundary, now registered honestly in risks
(a compromised own node can freeze an account, not only redirect; mitigated by
self-hosting). Also: /v1/chain/accumulator prose matches the JSON field name; the
wallet (not the node) waits for a pending dependency and a dependency_not_final
machine_code is added; npk_rand is a fresh uniform-256-bit CSPRNG draw (not an
HKDF value); the V.4 mint vector is marked the genesis case; and the stale
not-yet-final-nav wording is corrected.

* docs: reconcile the own-node-freeze note with the §6.6 guarantee

The §6.6 freeze-resistance is scoped to third-party nodes/publishers; a
compromised own node is the separate D-17 thin-client boundary (it can strand
your own key), now registered in the D-17 row and the risks verdict table and
tightened to a never-finalizing (fork-loser) or unsatisfiable ProofData. The
stale '§2.3.2 nav the wallet verifies' line is corrected to node-enforced,
not wallet-verified, after the §7.5(c) removal.

* docs: the freeze/redirect actor is the selected prover (own or vetted foreign node)

§6.6 already grants a self-selected foreign node used as prover the same lie/omit
capability as the own node (the wallet checks only npk_commit + H(ProofData), not
satisfiability), so the freeze/redirect boundary is the transition's selected
prover — own or a vetted foreign node — not the own node alone. §6.6, the risks
verdict row and freeze note, and the D-17 register now say so; the mitigation is
self-hosting or vetting the prover.

* spec: qualify the §6.6 freeze-resistance closing statement with the prover exception

The paragraph's closing 'no party ... holds the authority to withhold a holder's
own coins' now excepts the holder's own selected prover (the D-17 thin-client
boundary the same paragraph already establishes).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant