Skip to content

test(formal): the economic SMT and register, formalized — and a vacuous theorem repaired on the way in - #785

Merged
cryptskii merged 8 commits into
mainfrom
test/formalize-economic-smt-and-register
Sep 8, 2026
Merged

test(formal): the economic SMT and register, formalized — and a vacuous theorem repaired on the way in#785
cryptskii merged 8 commits into
mainfrom
test/formalize-economic-smt-and-register

Conversation

@cryptskii

Copy link
Copy Markdown
Collaborator

Discharges amendment 2c-C2 ruling G, plus the two properties added during review — quorum intersection and observation non-collapse. Seven commits, deliberately separable.

Two things exploration changed before any proof was written

A sentence in merged ruling G was false. It cited lean4/DSMNonInterference.lean as "proving leaf-key injectivity for the relationship SMT". It does not: relKey : Nat → Nat → Nat × Nat is a min/max sort, and relKey_injective proves that sorting an unordered pair is injective. That file has no hash, no SMT, no domain separation, zero axioms.

And a theorem in it was vacuous. operation_locality read

theorem operation_locality (s1 s2 : PairState) (amount : Nat) :
    let _ := pairCommit s1 amount
    s2 = s2 := by rfl

The let _ := binder is discarded, so the goal is s2 = s2. It mentioned the operation nowhere and held for any operation whatsoever — yet the file header called it "the mathematical core of Paper Lemma 3.1" and the published verification report listed it among proven theorems. It passed CI because sorry-free is not the same as non-vacuous.

The seven commits

A repair the vacuous frame condition, in isolation, before any new work
B a domain-tag registry that cannot silently lose a tag
C an economic register read a client can actually count
D lean4/DSMEconomicSmtSeparation.lean — obligations 1–7
E tla/DSM_EconRegisterObservation.tla — the concurrent half, expected failures gated
F correct ruling G and state the claim boundary
G two lints only the pinned toolchain catches

A. PairState is one pair with no notion of the others, so "committing here does not touch there" was not expressible over it. Adds PairWorld and commitAt; operation_locality now states j ≠ k ⇒ commitAt w k amount j = w j. Three theorems prove it is not free, and dropping hne leaves the frame goal unsolved — a control the old theorem could never admit.

B. 347 tags declared, 326 registered. Twenty-one escaped every domain-tag test, including two production signing domainsDSM/add-device-admission and DSM/add-device-self-attest (§16.3 device enrolment). Three checks now, proving three different things: a source-wide multiset comparison of declared against registered (the load-bearing guard), a consciously-bumped count tripwire, and membership assertions for the ten domains the proof's premise rests on.

The decisive control is the case a count provably cannot catch — declare a tag, forget the array:

the_declared_domain_tag_count_is_the_expected_one ... ok      <-- registry still 347
every_declared_domain_tag_reaches_the_registry ... FAILED
  declared domain tags that never reach all_tags(): ["DSM/mutation-probe"]

C. The economic read endpoints stamped neither the register incarnation nor an asserted absence. Since answer_counts_for needs both echo halves, every economic read on the live path was uncountable — and the two defects compose into exactly the collapse the amendment forbids:

every read -> Unavailable
  -> read_cell_quorum's  Unavailable => Ok(None)   economic_registers.rs:232
  -> winning_faucet_ticket's .ok().flatten()        peer_lineage.rs:165-169
  -> the verifier sees EMPTINESS

Fixed, tested on both branches of both registers, and mutation-controlled with the two realistic slips.

D. 64 theorems, zero axiom, zero opaque, no Mathlib. Three layers, and the separation between them is the honesty claim: a byte layer assuming nothing, a symbolic layer whose abstraction is declared rather than hidden, and an adequacy bridge that is the only place cryptography appears.

  • The delimiter result is named injectivity in (tag, message), not "prefix-freedom" — which it is not in either direction: DSM/x‖00‖a is a prefix of DSM/x‖00‖ab, and DSM/foo / DSM/foo/bar are both NUL-free with one a prefix of the other.
  • Obligation 1 gets a byte-layer bridge. In a free term algebra, same-domain injectivity holds because constructors are injective — which says nothing about the real byte grammar. Four per-key lemmas close that gap.
  • The hash assumptions are split and local. A single global injectivity hypothesis is stronger than collision resistance and silently re-absorbs the zero-sentinel assumption the inductive datatype was chosen to expose. Neither is global: a universal "distinct preimages give distinct 256-bit outputs" is false by pigeonhole, and the zero-sentinel exclusion is not implied by preimage resistance.
  • Tag distinctness is decided by the kernel over the verbatim Rust literals. Colliding two tags fails the build: "Tactic decide proved that the proposition ¬EconDomain.smtLeaf.tag = EconDomain.smtNode.tag is false".

E. Faithful config: 2,021,284 states, 135,349 distinct, depth 14. Five falsifications, each modelling a real shipped defect and machine-gated on the invariant it must violate — TlaSpec::expect_violation inverts the verdict. Previously the guarded family's expected failures were asserted in a README and checked by nobody. Neutering one now reports:

FAILED [expected-to-fail]
  ERROR: falsification config must violate EmptinessIsGrounded,
         but saw no violation at all — the invariant is decoration

Corrections to ruling G's obligations, found while discharging them

  • Obligation 4 presupposes a root domain that does not exist. empty_economic_root() = default_node(256) and every non-empty root is an econ_node output — node and root are one domain. The module proves that rather than asserting it; a model separating them would prove something false.
  • The leaf-state domain is missing from the list but is step 2 of the frozen five-step chain. Included.
  • Obligation 1's input domain is not closed for settlement-receipt and consumed-source, whose receipt_id and source_id are themselves domain hashes. Discharged relative to the declared 32-byte inputs.

The claim boundary

Formal coverage establishes the stated properties of the normative
economic-SMT and economic-register MODELS.

It does NOT constitute a machine-checked refinement proof that the
shipping Rust implements those models.

And the Lean module is not reported as "axiom-free" without qualification: the quorum results are; the rest rest on the declared symbolic abstraction and, in the bridge, on local hypotheses. #print axioms reports only propext (and Quot.sound for quorum) precisely because the crypto assumptions travel in signatures rather than as file-global axioms — so their absence from the earlier sections is a checkable fact.

Verification

  • Lean gatelean -DwarningAsError=true on all 11 modules, count matches the bumped CI expectation. That flag is the sorry check, and it also makes linter warnings fatal.
  • TLA gatecargo run -p dsm_vertical_validation -- tla-check: all 13 specs PASSED, including the five expected-to-fail.
  • Boardcargo test --locked --workspace --exclude dsm_storage_node --release: 4,074 passed, 0 failed across 76 test binaries (dsm lib 1743, dsm_sdk 1855).
  • dsm_storage_node (excluded from the board) — 278 passed, 0 failed.
  • Lintcargo fmt --all --check and cargo clippy --all-targets -- -D warnings on the pinned 1.98.0, both clean. Commit G exists because the default toolchain missed two lints the pin catches; verifying on whatever toolchain is active is not verifying the gate.

Also records a source-tree finding: economic/tree.rs:29 says "Since every present leaf is a BLAKE3 output, all-zero is unreachable as a present value." The conclusion does not follow — being a BLAKE3 output is what makes all-zero possible; preimage resistance is what makes it unreachable in practice. The construction is fine; the comment asserts a theorem where an assumption belongs.

…g one that did not

lean4/DSMNonInterference.lean's operation_locality was vacuous. It read

    theorem operation_locality (s1 s2 : PairState) (amount : Nat) :
        let _ := pairCommit s1 amount
        s2 = s2 := by rfl

The `let _ :=` binder is discarded, so the goal is `s2 = s2` and the proof is
`rfl`. It mentions the operation nowhere and holds for any operation whatsoever
-- yet the file header advertises it as "the mathematical core of Paper Lemma
3.1", and docs/reports/2026-04-28-formal-verification-report.md listed it among
proven theorems. It passed CI because sorry-free is not the same as non-vacuous.

The root cause is that PairState is a single pair with no notion of the others,
so "committing here does not touch there" was not expressible over it. This adds
PairWorld -- pair states indexed by relationship key -- and commitAt, which
updates exactly one key. operation_locality now states

    j != k  ->  commitAt w k amount j = w j

with both the operation and both sides of the equation present.

Three theorems establish the repaired statement is not free:

  commit_is_not_a_noop            the operation CHANGES its own target, so
                                  locality does not hold because nothing happens
  commit_hits_its_own_target      the target key IS the committed state; an
                                  implementation that discarded the operation --
                                  exactly what the old `let _ :=` did -- makes
                                  this unprovable
  distinct_pairs_do_not_interfere the paper's own form, over relKey, where
                                  relKey_injective makes key distinctness mean
                                  distinct unordered pairs

Mutation-controlled: dropping the j != k hypothesis leaves
`(if j = k then pairCommit (w j) amount else w j) = w j` unsolved, so the
hypothesis is load-bearing. The old theorem admitted no such control.

Also records in the header that relKey_injective is about the min/max
NORMALIZATION of an unordered pair, not about a hash -- this module contains no
hash, no SMT and no domain separation. Amendment 2c-C2 ruling G currently cites
this file as proving relationship-SMT leaf-key injectivity; that citation is
corrected separately.

Zero axiom, zero opaque, unchanged. #print axioms reports every theorem
depending only on propext. All ten modules pass `lean -DwarningAsError=true`.
347 domain tags are declared; 326 reached all_tags(). Every uniqueness and
prefix-freedom check in domain_tags/mod.rs traverses that registry, so 21 tags
were covered by nothing -- including two PRODUCTION signing domains:

    DSM/add-device-admission      genesis_identity/device.rs:17
    DSM/add-device-self-attest    genesis_identity/device.rs:22

both used by common/device_admission.rs:66,107 for §16.3 additional-device
enrolment. The remaining 19 are test/bench/trace fixtures in misc/testing.rs,
which is NOT cfg(test)-gated -- they compile into production builds and share
the one namespace, so a fixture colliding with a protocol tag would have been a
real defect that no test could see.

All 21 are now registered. Three checks replace the implicit trust:

  every_declared_domain_tag_reaches_the_registry
      scans the crate source INDEPENDENTLY of the TAGS arrays and compares
      declared against registered as a MULTISET of tag bytes. Multiset, because
      set equality absorbs a duplicate declaration or a duplicated entry --
      multiplicity disappears. Bytes rather than (name, bytes), because
      all_tags() returns TaggedHashDomain values and does not retain names;
      all_tags() is not redesigned merely to make a CI check expressible.
      The scan is source-wide rather than scoped to domain_tags/, since nothing
      constrains a declaration to that directory and scoping it there would make
      the guard depend on the convention it exists to enforce. Both declaration
      forms in the tree are matched: tagged_domain! and from_static.

  declared_domain_tag_constant_names_are_unique
      source-side, since the registry carries no names.

  the_declared_domain_tag_count_is_the_expected_one
      EXPECTED_TAG_COUNT = 347, a consciously-bumped change-control tripwire --
      the same idiom as the CI Lean gate's hardcoded module count. Explicitly
      NOT a completeness guard.

  the_domains_a_proof_depends_on_are_registered
      the eight economic-SMT domains plus the two add-device signing domains,
      asserted present by name. The economic-SMT separation proof reduces its
      cross-domain obligations to "these tags are pairwise distinct", and that
      premise is only as strong as the registry the distinctness test walks.

Mutation-controlled twice. Unregistering DSM/add-device-admission reddens the
completeness guard with the offending tag named. More importantly, DECLARING a
new tag and forgetting the array -- the case a count provably cannot catch,
since the registry still holds 347 -- leaves the count test GREEN and reddens
only the completeness guard:

    the_declared_domain_tag_count_is_the_expected_one ... ok
    every_declared_domain_tag_reaches_the_registry ... FAILED
    declared domain tags that never reach all_tags(): ["DSM/mutation-probe"]

That is the class of bug the previous arrangement admitted, and it is why the
count is the weakest of the three rather than the guard.
The two economic register read endpoints stamped neither the register
incarnation nor an asserted absence, so on the live path NO economic read was
countable and every one degraded to Unavailable.

The client requires two axes to attribute an answer -- answer_counts_for
(dsm_sdk/src/sdk/storage_node_sdk.rs:663-669) counts a member only when the
echoed node id AND the echoed register incarnation both match the member the
vault committed -- and counts a 404 as an absence only when the response
asserts `x-dsm-slot-outcome: absent` (:1072-1082). The blanket layer supplies
the node id; nothing supplied the incarnation, and Ok(None) answered with a
bare 404.

The consequence composes with two further call sites into exactly the collapse
amendment 2c-C2 forbids:

    every economic read -> Unavailable
        -> read_cell_quorum's  Unavailable => Ok(None)     economic_registers.rs:232
        -> winning_faucet_ticket's .ok().flatten()          peer_lineage.rs:165-169
        -> the verifier sees EMPTINESS

A quorum-unavailable read delivered as emptiness is the precise failure the
four-valued observation exists to prevent.

api/economic/mod.rs gains cell_read_response, which stamps the incarnation on
EVERY branch and asserts the absence on the empty one. Stamping only the held
case would leave the dangerous one -- a rebuilt member reporting emptiness --
indistinguishable from the real member reporting it, which is the whole reason
the second axis exists. A 500 carries the incarnation but asserts no outcome: a
failure is not an absence.

The incarnation comes from state.own_register_incarnation, set unconditionally
at main.rs:541 from the same DB value that builds the storage set at :553, and
NodeStorageSet::new refuses a mismatch (lib.rs:92) -- so the two sources agree
by construction and this one is present even when no set is configured.

Tests assert both branches on both registers, and both test_state helpers now
record the incarnation so every existing test's answers are countable too.

Mutation-controlled with the two realistic slips, each reddening the named test
at the exact assertion:

    stamp only the held branch   -> FAILED at the absent-branch incarnation
    drop the absence assertion   -> FAILED at the slot-outcome assertion

(Removing the stamp outright is a COMPILE error, since the crate is
#![deny(warnings)] and `state` goes unused -- a stronger signal, but not
evidence the test catches a realistic regression, which is why both mutations
above keep the code compiling.)

Clippy in this crate goes 117 errors -> 75: the two economic test modules never
carried the `disallowed_methods` allow that api/vault/settlement_slot.rs:237
has, and adding it for the new tests also covers the pre-existing test code.
278 dsm_storage_node lib tests pass; fmt clean.
…ons named and local

Discharges amendment 2c-C2 ruling G. lean4/ and tla/ contained ZERO references
to the economic tree; this is a THIRD tree, with a distinct leaf-key family, a
distinct node domain, and an all-zero ABSENT_LEAF that is the deliberate
opposite of the modelled one. 64 theorems, zero axiom, zero opaque, no Mathlib,
no imports -- matching every other module in the tree.

Three layers, and the separation between them IS the honesty claim:

  §1-2   byte layer      real byte lists, real tag literals     0 assumptions
  §3-10  symbolic layer  free term algebra over the encoder     the abstraction
                                                                is declared, not
                                                                hidden
  §11    adequacy bridge the only place cryptography appears    LOCAL hypotheses

THE STRUCTURAL SPINE. Tags are NUL-free by type and the encoder appends exactly
one 0x00, so the first NUL sits at len(tag) and the (tag, message) split is
unique FOR ARBITRARY MESSAGES. This is stated as INJECTIVITY IN (tag, message)
and disjoint preimage spaces -- deliberately NOT called "prefix-freedom", which
it is not in either direction: `DSM/x||00||a` IS a prefix of `DSM/x||00||ab`,
and `DSM/foo` / `DSM/foo/bar` are both NUL-free with one a prefix of the other.
Literal tag prefix-freedom is a separate hygiene property with its own Rust test.

Obligations 2/3/4 then reduce to pairwise tag distinctness, DECIDED by the
kernel over the verbatim Rust tag literals. Mutation-checked: making smtNode's
tag collide with smtLeaf's fails the build with

    Tactic `decide` proved that the proposition
      ¬EconDomain.smtLeaf.tag = EconDomain.smtNode.tag
    is false

OBLIGATION 1 GETS A BYTE-LAYER BRIDGE. In a free term algebra, same-domain
injectivity holds because CONSTRUCTORS are injective -- which says nothing about
whether the real byte grammar maps distinct declared inputs to distinct message
bytes. Four per-key lemmas close that gap from fixed-width fields alone, so a
key collision requires a hash collision rather than an ambiguous concatenation.

THE HASH ASSUMPTIONS ARE SPLIT AND LOCAL. A single global "injective Digest ->
bytes" is stronger than collision resistance AND silently re-absorbs the
zero-sentinel assumption the inductive datatype was chosen to expose, because
absent maps to 0^256. Kept apart:

    digest_width    definitional; PROVED (encodeDigs_length), not assumed
    h_collision_xy  for the CONCRETE preimages of a statement
    h_not_absent_x  for a CONCRETE populated preimage

Neither is global. A universal "distinct preimages give distinct 256-bit
outputs" would be FALSE, not merely unproven -- collisions necessarily exist by
pigeonhole. And "no populated economic preimage produces 0^256" is not implied
by preimage resistance, which speaks to infeasibility of FINDING one.
`global_injectivity_would_smuggle_in_the_zero_claim` exhibits the conflation so
the decision to split is visible rather than accidental.

THREE CORRECTIONS TO RULING G, all recorded in the module header:

  * Obligation 4 presupposes a root domain that does not exist.
    empty_economic_root() = default_node(256) and every non-empty root is an
    econ_node output, so node and root are ONE domain --
    root_lives_in_the_node_domain proves it. A model separating them would
    prove something false.
  * The leaf-state domain is missing from ruling G's list but is step 2 of the
    frozen five-step chain; it is included in the disjointness set.
  * Obligation 1's input domain is not closed for settlement-receipt and
    consumed-source, whose receipt_id and source_id are themselves domain
    hashes. Injectivity is stated relative to the declared 32-byte inputs.

OBLIGATION 6 REACHES THE TREE. A map-level frame condition alone could be proved
for a standalone Map with no connection to R_econ -- repairing one vacuous
theorem and introducing a subtler one. All four layers are present: map
(frame + own-key hit), tree (fold_path_injective: a root plus its direction bits
determines the leaf AND every off-path sibling), canonicality, and anti-vacuity
witnesses that an update actually changes the root.

Obligation 7 is stated over QUALIFYING quorums (at least q), which is what the
protocol deals with; exact-q is kept as the underlying lemma. The predicate form
needs no Nodup to PROVE -- predicates cannot inflate their own cardinality -- but
carries members.Nodup as a `_`-prefixed hypothesis, the house signal for
not-load-bearing-in-the-proof, because it is what licenses reading S.length as
the number of DISTINCT members. Without it the lemma stays true and n stops
denoting membership.

#print axioms on every headline theorem reports only Lean's own logical axioms
(propext, and Quot.sound for the quorum results); beta_quorum_is_canonical
depends on none. That is because the cryptographic assumptions travel in §11
signatures rather than as file-global axioms -- so their ABSENCE from §1-§10 is
a checkable fact, not a claim. The summary block says explicitly that the module
must not be reported as "axiom-free" without that qualification.

Also records a source-tree finding: economic/tree.rs:29 says "Since every
present leaf is a BLAKE3 output, all-zero is unreachable as a present value."
The conclusion does not follow -- being a BLAKE3 output is what makes all-zero
POSSIBLE; preimage resistance is what makes it unreachable in practice. The
construction is fine; the comment asserts a theorem where an assumption belongs.

CI's hardcoded lean4/ module count moves 10 -> 11, the intended tripwire. All
eleven modules pass `lean -DwarningAsError=true`, which is the sorry check --
and, as the plan anticipated, also makes linter warnings fatal.
…failures finally gated

Models the BEHAVIOURAL half of the frozen observe_cell semantics: the write-once
economic register read while claimants are concurrently writing, members are
failing, and members are REBUILDING their registers.

The algebraic half -- canonical quorum, and 2q > n forcing any two qualifying
quorums to intersect -- is a universal statement over all n and stays in
lean4/DSMEconomicSmtSeparation.lean §10. Neither restates the other. Quorum is a
CONSTANT here, exactly as observe_cell takes it as an argument, and there is
deliberately NO operator computing a quorum from Cardinality(Member): a local
majority-of-catalog rule is the verifier's opinion, not the vault's.

What TLC uniquely buys is the ROUND -- what can happen to the register between
sampling one member and the next. NoEmptyAtQuorumAfterClaimed is the statement
only a model checker can make: a cell observed Claimed is never later observed
empty, across every interleaving of claims, outages and rebuilds. observe_cell
applied to one fixed list of reads is a pure function and is Lean's business.

Faithful config: 2,021,284 states, 135,349 distinct, depth 14, no error.

EXPECTED-TO-FAIL CONFIGS ARE NOW MACHINE-GATED. Previously the guarded family
shipped two configs asserted to fail in a README table, and nothing checked it.
TlaSpec gains expect_violation: Option<String>, and run_all inverts the verdict
-- the run passes only if TLC reports EXACTLY the named invariant. Three ways to
fail: no violation at all (the invariant is decoration), the wrong invariant
(the config is not modelling what it claims), or a TLC error.

Five falsifications, each modelling a real shipped defect:

  _FlattenCollapse      peer_lineage.rs:165-169 .ok().flatten()
                        -> EmptinessIsGrounded
  _UnavailableIsNone    economic_registers.rs:232 Unavailable => Ok(None)
                        -> EmptinessIsGrounded
  _ErrorIsEmpty         an unusable answer classified as "no value", the
                        historical defect cell_observation.rs exists to remove
                        -> EmptyAtQuorumIsWitnessed
  _NoIncarnationEcho    attribution on node id alone -- the live economic read
                        path before the incarnation header was stamped
                        -> EmptyAtQuorumIsWitnessed
  _Reachability         NON-VACUITY: Conflict must be REACHABLE from two
                        claimants racing one write-once cell, with no
                        misbehaviour at all. Without it the four above could
                        pass for the wrong reason
                        -> ConflictUnreachable

_NoIncarnationEcho violates EmptyAtQuorumIsWitnessed rather than
NoEmptyAtQuorumAfterClaimed: dropping the incarnation requirement makes a
rebuilt member's absence count as attributable when ground truth says it is not,
so the witnessed check fires first. Recorded as observed, not as predicted.

The gate machinery is itself mutation-controlled. Neutering _FlattenCollapse
back to the faithful consumer makes tla-check report

    FAILED [expected-to-fail] (2021284 states, 135349 distinct, depth 14)
      ERROR: falsification config must violate EmptinessIsGrounded,
             but saw no violation at all -- the invariant is decoration

`cargo run -p dsm_vertical_validation -- tla-check`: all 13 specs PASSED.
…o of its obligations were wrong

Amendment 2c-C2's ruling G cited lean4/DSMNonInterference.lean as proving
"leaf-key injectivity for the relationship SMT". It does not. relKey is a
min/max sort of two naturals and relKey_injective proves that sorting an
unordered pair is injective; the file contains no hash, no SMT, no domain
separation and zero axioms. A reader following that sentence would have been
misled about both trees -- believing the relationship tree covered, and
believing the economic obligations had a template that did not exist.

Two of the obligations were also wrong as written, found while discharging them:

  Obligation 4 presupposes a ROOT domain that does not exist.
      empty_economic_root() = default_node(256) and every non-empty root is an
      econ_node output, so node and root are ONE domain. The Lean module proves
      that rather than asserting it; a model separating them would prove
      something false. The real content is node vs leaf vs leaf-state.

  The leaf-state domain DSM/economic-leaf-state/v1 is absent from the list but
      is step 2 of the frozen five-step chain, so it belongs in the disjointness
      set. Now included.

Plus one scoping caveat: obligation 1's input domain is not closed for the
settlement-receipt and consumed-source keys, whose receipt_id and source_id are
themselves domain hashes. Injectivity is discharged relative to the DECLARED
32-byte inputs; the nested derivations are a recorded dependency.

Status moves from "claims NO model coverage" to discharged, and the claim
boundary is stated in ruling G, in tla/PROOF_CLAIMS.md alongside the existing
boundaries, and in the verification report:

    Formal coverage establishes the stated properties of the normative
    economic-SMT and economic-register MODELS. It does NOT constitute a
    machine-checked refinement proof that the shipping Rust implements those
    models. Implementation correspondence is supported separately by
    conformance vectors, source-level invariants, tests, and deliberate
    falsification controls.

And the Lean module is explicitly NOT reported as axiom-free without
qualification. The quorum results are; the hash and non-aliasing results rest on
the symbolic abstraction declared in the module header, with the adequacy bridge
resting on a LOCAL collision hypothesis and, for the zero-sentinel obligation
only, a LOCAL preimage hypothesis. Neither is a global claim about BLAKE3: a
universal "distinct preimages give distinct 256-bit outputs" is FALSE by
pigeonhole over a 256-bit codomain, and "no populated economic preimage produces
the all-zero digest" is not implied by preimage resistance, which speaks only to
the infeasibility of FINDING one.

Documentation only.
… one does not

The domain-tag source scanner tripped clippy::sliced_string_as_bytes and
clippy::unnecessary_to_owned under `rustup run 1.98.0 cargo clippy --all-targets
-- -D warnings`, which is what `make lint` runs.

Recorded because the miss is instructive: `cargo clippy -p dsm` on the DEFAULT
toolchain reported clean, and both lints only fire on the pin. Verifying a lint
on whatever toolchain happens to be active is not verifying the gate -- the same
shape as running a filtered test subset and calling it the board.
…unreachable by hashing

economic/tree.rs said:

    Since every present leaf is a BLAKE3 output, all-zero is unreachable as a
    present value.

The conclusion does not follow from the premise. Being a BLAKE3 output is
exactly what makes the all-zero digest POSSIBLE -- a fixed-width hash over an
unbounded domain has preimages of every value. What makes it unreachable in
practice is preimage resistance, which is an assumption, not a consequence of
the construction.

The comment now says ABSENT_LEAF RESERVES the all-zero digest, and that a
populated leaf could take that value only by producing a valid economic preimage
whose BLAKE3 digest is all zero -- which security treats as computationally
infeasible.

This aligns the source with the formal model's actual assumption boundary.
lean4/DSMEconomicSmtSeparation.lean draws the same line: the symbolic model puts
`absent` outside the image of the hash BY CONSTRUCTION, and the adequacy bridge
carries the byte-level claim as an explicit LOCAL hypothesis (h_not_absent_x)
rather than deriving it. Having the Rust assert a theorem while the Lean
carefully declines to would have left the two disagreeing about what is proved.

No behaviour change. The construction was never defective -- only the comment
overstated it, which is the same failure mode as the peer_lineage.rs:61-62
comment amendment 2c-C2 already records: a comment that certifies to the next
auditor something the code does not establish.

fmt and clippy clean on the pinned 1.98.0; the [`ABSENT_LEAF`] intra-doc link
resolves.
@cryptskii
cryptskii merged commit 6574fe5 into main Sep 8, 2026
19 checks passed
@cryptskii
cryptskii deleted the test/formalize-economic-smt-and-register branch September 8, 2026 20:44
cryptskii added a commit that referenced this pull request Sep 9, 2026
…ts (#786)

* docs(ccb): land amendment 2c-C2's owed registry edits

C2 was merged as PR #785 but its "Registry and cross-document edits"
section never landed. All six of its markers returned zero against
origin/main. C1's edits did land, so the asymmetry was easy to miss --
and the sentence C2 declares false was still sitting in the registry
telling implementers the opposite of the ruling.

Seven edits, one per obligation C2 recorded:

  §3.1  signature_alg 0x0001 is DSM BLAKE3-SPHINCS+-SPX256F, NOT
        FIPS-205 SLH-DSA-256f. The widths coincide exactly, which is
        what makes the substitution silent: a foreign verifier linking
        a standards-only SLH-DSA library gets byte-identical key and
        signature lengths and fails every signature with nothing to
        diagnose. The old closing sentence -- "the algorithm and the
        key bytes stand or fall together" -- is replaced; it was false
        as written, and false in the direction that made the
        substitution look safe.

  §2.10 BindingRecordWireV1 recorded as the ONE named non-CCB grammar,
        with its Class N argument and an explicit statement that it is
        not permission for arbitrary protobuf-derived identities.

  §2.11 New: the retrieval obligation. Recompute the identity over the
        bytes returned and compare to the address asked for, BEFORE
        reading any field. Consumer's obligation, never the fetcher's.
        Must be discharged by a single named construct, not by
        open-coding.

  §15.3 Reconciled in §2.11: Rev 15's H(N ‖ P) names no hash function,
        has no separator and no length discipline, so it is not
        injective in N. The frozen form is H_dom(N, P). Injectivity is
        a property of the NUL-free tag AND the separator together --
        the separator alone would push the same ambiguity one byte
        deeper -- so §2.9's NUL-free condition is cited, not assumed.

  §3.2  New: normative network parameters. The dsm-testnet
        root-register profile -- three (member_id,
        register_incarnation_id) pairs, n = 3, q = 2, and the derived
        storage_set_id. A network absent from the table is unknown,
        not permissive.

  §5.2  Cross-references §3.2, so the frozen element encoding and the
        values instantiating it can be read against each other.

  §7    2c-C2 bullet added and marked WRITTEN, recording that
        authenticated retrieval is frozen as a rule and NOT met by the
        implementation. The decomposition's "P0-P6" citation is
        corrected to the authority-resolver contract; C2 ruling A
        declines to elevate P0-P6 into a SoFi interface.

§4's counts do not move: framework and namespace content only, no
field table added, changed or burned.

Documentation only. Prerequisite for 2c-C3, which cites the registry.

* docs(sofi): amendment 2c-C3 — freeze ValidDlvSuccessorCore

C2 froze how a verifier obtains and authenticates the substrate and
deliberately did not decide what makes a DLV continuation valid. C3
decides exactly that.

Specification-first, on the owner's ordering: complete the read-only
source reconciliation, freeze every clause, write the normative
predicate, then implement. That ordering earned its keep twice --
both findings were invisible to clause-by-clause transcription and
both change what the predicate must contain:

  - §8's Req 8.1 is a successor-validity condition, not background:
    "a claim is consumed at most once and its exact removal must be
    visible in the successor state". No earlier draft carried it.
  - Def 6.1's terminal close requires crediting released reserves to
    owner balance EXACTLY ONCE -- a property of the owner's balance,
    not of V_{n+1}, so no predicate over the successor can express it.
    Declared, owner named, explicitly not discharged.

The expected "15-clause predicate" does not exist in the source. Def
6.1 is ten common checks plus kind-specific tails: sixteen items at
its own granularity, ~34 atomically. FIFTEEN IS THE VaultStateV2 TUPLE
ARITY AND THE REVISION NUMBER. That framing is revoked; no clause
count is normative.

Four Rev 15 errata corrected as exact predicates, two of which made
the predicate unformulable as written:

  D1  the parent-reserves-digest operand does not exist -- it was part
      of p_v, which Req 6.6 burned. c_n commits the whole tuple.
  D2  Retired(V) := reserve_a = 0 ∧ reserve_b = 0. No new field;
      unambiguous BECAUSE market admissibility forbids a = 0, so a
      market successor can never zero both legs.
  D3  fee_t ≡ 0 in §7.1 for the beta market family. The fee stays in
      the reserves, so it is inside Σ R^(n+1), not subtracted from it.
      A verifier applying §7.1 literally REJECTS EVERY VALID BETA
      MARKET SUCCESSOR.
  D4  direction is carried nowhere and is derived from the policy-
      commit set equality. Total, because beta_constant_product
      refuses an unordered or equal pair, so the same check that
      establishes membership also refutes input = output.

Canon(expected) = Canon(supplied) is the SINGLE AUTHORITATIVE
SUCCESSOR-STATE CORRESPONDENCE test -- one conjunct, not the whole
predicate. Authority, signatures, the parent relationship, binding
finality and evidence validity stand alongside it. DeriveExpected is
typed and partial (Derived | Invalid | Incomplete | SafetyViolation);
the byte comparison happens only on the Derived arm, so failed
arithmetic and unavailable evidence cannot hide inside an apparently
total call.

successor_ccb resolved unconditionally. canon() is encode_to_vec(), so
the field IS frozen wire and IS the bundle-shape discriminator -- the
opposite of the earlier draft's assumption. It is not deleted from a
frozen encoding; the encoding is superseded in full, and 2c-A already
prohibits it: "do not carry both the complete successor and a second
independently encoded successor digest". Until that cut lands, b is
computed over prost bytes, which §2.10 forbids. Recorded, not relied
upon.

C3 receives typed prerequisite RESULTS, not success-only facts. A
component that owns INVALID / INCOMPLETE / SAFETY_VIOLATION cannot be
handed only established successes; it would be blind to the outcomes
it classifies.

Two conjuncts are declared and undischarged -- TokenPolicyValid and
the owner credit -- named so "C3 is closed" can never be read as "DLV
succession is fully verified".

Records the implementation debt this lands on: vault_state_composition
.rs:572 binds the bundle's declared successor and discards it with
`let _ = transition;`, folding its own derived next_state instead. The
comparison this amendment makes normative does not exist today.

Documentation only. No Rust, no proto, no tests.

* test(formal): the DLV successor predicate, with its encoding hazards proved

Phase C of amendment 2c-C3: definitions and theorem statements for
ValidDlvSuccessorCore. Strictly additive -- new module, no existing
proof touched. CI Lean count 11 -> 12.

What it machine-checks:

  BRIDGE       canonVault_injective. Canonical equality implies
               structural equality, so "every preserved field equals
               V_n" derived from a byte comparison carries no unstated
               assumption. Ruling B names this obligation precisely
               because omitting it would hide one.

  ENCODING     The encumbrance set sits at field 10, in the MIDDLE of
               the tuple, so its §2.4 count prefix is load-bearing.
               Two genuinely different states -- one encumbered, one
               holding a live budget -- have byte-identical encodings
               without it, because the claim list borrows the presence
               marker's byte:

                   no claims, β = some 0  ->  [] ‖ (1 ‖ 0)  =  [1,0]
                   claim [1], β = none    ->  [1] ‖ (0)     =  [1,0]

               A verifier comparing those bytes accepts a successor
               that silently converted a budget into an encumbrance.

  D2           market_never_derives_retired. This is the whole
               argument for erratum D2, formalized: admissibility
               forbids a = 0, so the input leg is R_in + a > 0 and at
               least one leg stays strictly positive. Both-zero is
               therefore reachable ONLY by the close family, which is
               what makes Retired an unambiguous marker with no new
               tuple field.

  D4           direction_refutes_equal_commitments. The set-equality
               check that establishes membership also refutes
               input = output, because token_a < token_b strictly.
               The derivation is total.

  DERIVED      Preserved and mutated field equalities are obtained
               FROM correspondence, not checked beside it -- theorems,
               not acceptance conjuncts, so the independence
               obligation is satisfiable for them.

  TAXONOMY     Each failing arm propagates its EXACT class and reason.
               Stated per constructor, because "propagates its class"
               is the claim that must not be approximate.

One theorem was WRONG on the first pass and the compiler caught it: I
had claimed correspondence's class always agrees with the derivation's
class. It does not -- a successful derivation whose bytes mismatch is
invalid, so the derived arm downgrades valid to invalid. That
asymmetry IS the comparison's purpose. Replaced with
correspondence_valid_iff_bytes_match, which states it exactly.

Ruling H is formalized rather than promised:
core_does_not_discharge_token_policy exhibits an input satisfying the
core predicate while the complete predicate fails. The module cannot
be read as claiming C3 closed a dependency it does not own.

Mutation controls executed, not asserted. Both produced the strongest
available result -- the kernel proving the NEGATION of a named
theorem, not merely a broken proof:

  strip the count prefix   -> canonVault_separates_the_samples proved
                              FALSE; canonVault_injective falls back
                              on sorryAx
  remove the comparison    -> preserved_field_mutation_is_rejected,
                              encumbrance_introduction_is_rejected,
                              budget_introduction_is_rejected and
                              market_successor_not_accepted_under_close
                              each proved FALSE

The encumbrance and budget controls matter most: beta's E is always
empty and no Allocation type exists in Rust, so no beta-shaped test
can exercise Req 8.1 at all. Stated at the model level or not tested
anywhere.

Axioms reported per theorem, never as a blanket claim. No result
depends on sorryAx or Classical.choice; six depend on no axioms at
all. propext and Quot.sound are Lean's logic, not assumptions added
here.

Gate: all 12 modules pass lean -DwarningAsError=true.

* docs(sofi): correct three defects in 2c-C3 before it becomes authority

A read-only survey of the Rust surface, run before any Phase D code was
written, found three defects in this amendment. Two of them were
confident, specific and wrong. Correcting them here rather than merging
and issuing an erratum: this is the freeze point, and a normative
document should not enter the record carrying a false factual claim.

1. THE PRODUCTION-DEBT STATEMENT WAS FALSE.

   The draft said vault_state_composition.rs:572 "binds the bundle's
   declared successor and discards it ... Both values are live at that
   point."

   There is no declared successor on the wire at all. successor_ccb is
   32 bytes that do not commit the successor state: production market
   bundles write the route-set commitment x (dlv_routes.rs:3249, whose
   own comment says "there is no such commitment to name, so it carries
   the trade identity and nothing reads it as a successor"), and
   production closes write x_close (settlement_bind.rs:161), a pure
   function of (vault_id, parent_generation). Both composition arms
   derive the successor locally.

   So VDS.COMMON.10.a is not merely unimplemented -- it is
   UNIMPLEMENTABLE against the current encoding, and is now marked:

       NORMATIVE               YES
       FORMALLY SPECIFIED      YES
       PRODUCTION IMPLEMENTED  NO
       BLOCKED ON              the 2c-A canonical encoder cut

   with the exact blocker, the owning prerequisite, and what unblocks
   it. Blocked must not mean accepted-without: production may not
   report ValidDlvSuccessorCore = VALID for any path whose validity
   requires the conjunct until the byte comparison is actually
   performed. That gap is implementation status
   (BlockedOnCanonicalSuccessorEncoding), never a protocol reason code
   -- the INVALID/INCOMPLETE/SAFETY_VIOLATION taxonomy is not
   contaminated because a prerequisite has not landed.

   Also recorded: successor_ccb cannot simply be repurposed.
   is_close_transition derives BundleShape from exactly that field, so
   writing a real successor commitment into it silently reclassifies
   every close bundle as Market. The shape discriminator must move
   first, and that is 2c-A's to do.

2. RULING J NAMED THE WRONG OBSERVATION TYPE.

   The draft called binding_observation "C2's four-valued
   CellObservation, unchanged". Two distinct types exist over two
   distinct key spaces:

       CellObservation     economic register cell     FOUR arms
       BindingObservation  DLV parent-binding slot    FIVE arms

   The settle path reads the five-valued one. All five arms are now
   mapped normatively:

       Free          -> INVALID          NO_BINDING_ESTABLISHED
       BoundFinal    -> proceeds to VDS.COMMON.10.a
       Conflict      -> SAFETY_VIOLATION DUPLICATE_BINDING_FINALITY
       Undetermined  -> INCOMPLETE       BINDING_UNDETERMINED
       Unavailable   -> INCOMPLETE       BINDING_EVIDENCE_UNAVAILABLE

   Two of those are easy to get backwards and the defining module says
   so. Undetermined is neither emptiness nor forgery -- two quorums
   intersect, but one READ need not see the intersection, so a value
   chosen behind a down member lands there; reading it as Free composes
   past a live bind, and reading it as invalid "would make every
   concurrent settle permanently invalid". Free is INVALID rather than
   INCOMPLETE because a quorum of authenticated explicit absences is
   positive evidence that nothing is chosen.

   The Lean module was corrected first and the document follows it. Two
   new theorems pin the arms that invert:
   undetermined_is_incomplete_never_valid_never_invalid and
   free_is_invalid_not_incomplete.

3. THE CORRESPONDENCE CONDITION NEEDED A BYTE-LEVEL STATEMENT.

   Semantic successor equality and canonical-byte equality come apart
   here. decode_vault_state NORMALIZES rather than refuses --
   StorageSetMembers::new and EncumbranceSet::new both sort, rejecting
   only duplicates, never bad order -- and CCB has no decode/re-encode
   equality check, though ~10 sibling modules apply exactly that
   discipline and one names it "the settlement-wire discipline". So two
   byte strings decode to one VaultStateV2 and a round-trip comparison
   launders non-canonical input.

   The acceptance condition is now stated as equality of canonical
   BYTES, with the round trip permitted only under the frozen normative
   encoder -- which does not exist: repo-wide there is exactly one
   `fn canon`, and it is protobuf. Phase D must build it.

Ruling E additionally now forbids mapping a FAILED SIGNATURE to an
absence, because the shipped code does it deliberately:
fetch_verified_receipt discards a failed SPHINCS+ receipt verification
with .ok()?, which becomes MarketRealization::Absent, breaks the fold
and returns Ok(...) -- so a forged receipt is not an error at all. Its
doc defends the collapse as sound about FOLDING, which it is; it is
wrong about CLASSIFICATION.

The amendment carries a "Corrections made before it merged" section
recording all three rather than silently rewriting a circulated draft.

All 12 Lean modules pass lean -DwarningAsError=true. No result depends
on sorryAx or Classical.choice.
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