Skip to content

feat(economic): R_econ primitives — tree, keys, leaf states, witness verifier, classifier - #724

Merged
cryptskii merged 1 commit into
mainfrom
feat/economic-primitives
Aug 25, 2026
Merged

feat(economic): R_econ primitives — tree, keys, leaf states, witness verifier, classifier#724
cryptskii merged 1 commit into
mainfrom
feat/economic-primitives

Conversation

@cryptskii

Copy link
Copy Markdown
Collaborator

Step 3, sub-PR 1 of 5. The primitives an economic root is built from — and deliberately nothing that depends on rules not yet defined.

What this establishes, and what it does not

Establishes: a closed write set. Given an authenticated pre-root and an ordered list of leaf mutations, recompute the post-root those mutations actually produce.

Does not establish: credit provenance. A closed write set proves what changed, never why a credit may appear. A verifier running only what is in this PR would accept a trader crediting itself from nothing, because every mutation in that write set is individually well-formed. The six-arm CreditSource algebra is a separate and conjunctive obligation, and lands with sub-PR 4. This is stated in the module header rather than left for a reader to discover.

Contents

Area What landed
Domain tags Nine DSM/economic-* tags. The economic tree shares no domain with the relationship SMT, so a relationship inclusion proof cannot be replayed as an economic one.
CCB classes Discriminants 0x001B0x0029 allocated in the single §3 namespace. Next free stays 0x0030.
Key derivations Four, each the only place its key class is computed. Every one binds G ‖ DevID, so the key space is per-identity by construction.
Leaf states 0x001F0x0022, with the zero asymmetry below.
Tree A non-evicting 256-deep SMT.
Mutation EconomicLeafMutation 0x001E.
Verifier The sequential witness verifier.
Claim / manifest EconomicRootClaimBody 0x001B, EconomicAdmissionManifest 0x001C.
Classifier Exhaustive EconomicEffect with no wildcard arm, plus the structural tripwire.

Zero is not one thing

balance amount == 0   =>  the leaf is ABSENT
reserve amount == 0   =>  the leaf is PRESENT as { amount: 0, vault_sequence: n }

The asymmetry is load-bearing. A zero balance carries no information beyond its own absence, so admitting a zero-valued balance leaf would give one economic state two encodings and therefore two roots. A zero reserve is different: vault_sequence is still meaning, and a close has to be able to say which generation it zeroed. EconomicBalanceState::new(pc, 0) therefore has no canonical bytes at all.

Why not the existing SMT

Two independent reasons. SparseMerkleTree::update_leaf is a bounded FIFO cache that evicts the oldest key over capacity — survivable for relationship tips, which can be refetched; not survivable for a balance, which would vanish from the root with no record it existed. And that tree's leaf hash commits the value but not the key, so a proof is portable across positions holding the same value. Two assets routinely hold the same amount, so econ_leaf binds the key.

The mutation carries no key

The key is derived from the state objects the mutation carries, so "valid object filed at the wrong position" is unrepresentable rather than merely checked for. Sibling count is exactly 256 as a validity condition, not a bound: a short path proves membership in a shallower tree.

One edge into the evidence

The claim names admission_manifest_addr and nothing else. transition_witness_digest and admission_evidence_digest are deleted — a second digest beside the first is a second place for the claim and the manifest to disagree, with no principled resolution when they do.

What is deliberately NOT encoded — and why that is enforced, not noted

Class 0x001D (EconomicTransitionWitness) and the provenance classes 0x00230x0029 are allocated but have no encoder. Their credit_sources field tables do not exist: the plan fixes those classes' semantic roles, not an encoding exact enough to burn as protocol. A witness names its credit sources, so the witness encoding cannot be fixed before theirs. Writing either now would fix the provenance wire format by implementation accident — the "encoder defines the protocol" inversion ccb/mod.rs refuses in its own header.

This is structural, not a comment. The discriminants live in a new ccb::reserved module, separate from ccb::class:

  • A CcbObject impl needs a CLASS constant, so writing an encoder for a reserved class requires physically moving the constant into class — a deliberate, reviewable diff that must arrive with the field table earning it.
  • reserved_classes_have_no_encoder asserts every economic type that can produce canonical bytes draws from the live namespace. Positive-controlled: pointing EconomicLeafMutation::CLASS at reserved::ECONOMIC_TRANSITION_WITNESS fails it with class 0x001d has an encoder but is listed as reserved.
  • a_reserved_class_is_unusable_on_the_wire_until_its_schema_is_installed pins reserved::ALL to the exact set, so promotion cannot happen silently, and asserts 0x0030 is not reserved — it is the next free class, and conflating free with reserved wastes a discriminant.

0x001D is therefore not merely unimplemented: it has no canonical bytes to produce, hash-address, nest or sign. The verifier takes EconomicMutationSequence — a verification input, not a wire object: no class, no canonical bytes, nothing to sign.

The provenance wire freeze is the immediately following PR (3.2), not a distant one, because the admission manifest and evidence DAG need an exact, hash-addressable transition witness. Carrying a noncanonical witness through lineage and admission would mean retrofitting its hash identity afterwards — the same mistake, later and more expensively.

Frozen sequence: 3.1 primitives (this) → 3.2 provenance wire freeze (encoders/decoders only, no acceptance semantics) → 3.3 lineage/register → 3.4 admission lifecycle + evidence DAG → 3.5 provenance semantics + ordinary ops → 3.6 DLV integration.

Verification

24 tests, and three gates proven load-bearing by mutation — remove the gate, watch a named test go red by performing the forbidden action, restore it:

Gate removed Test that goes red
post-root equality a_changed_leaf_missing_from_the_write_set_fails
pre-state membership a_pre_state_that_never_existed_fails
strict-ascending ordering a_descending_write_set_is_refused_even_when_every_path_verifies, a_repeated_key_is_two_disagreeing_claims_about_one_leaf

The first mutation initially reported green. That was correct reporting of a broken test file, not a passing gate: an earlier edit of mine had silently deleted that control test between two functions it replaced. The mutation run is what surfaced it, which is the argument for running mutations rather than trusting a green suite around a gate.

Notable negative controls beyond the gates:

  • a_descending_write_set_is_refused_even_when_every_path_verifies — the adversarial case where a producer builds paths in descending key order so every individual mutation authenticates. Still refused, because canonical ordering is what makes one write set have one representation.
  • a_witness_cannot_be_replayed_under_another_identity — the same mutation bytes fail under a different G or DevID.
  • a_zero_balance_is_the_absence_of_the_leaf_not_a_leaf_holding_zero — reaching zero returns the tree to exactly the root it had before the leaf existed.
  • The tripwire is checked for all four leaf families, and holds OfflineAccountOnly to the same standard as None: the offline allocation lives outside R_econ, so an operation claiming to move only allocation and yet touching a leaf has broken the regime separation.

Board — both halves, on this exact tree

cargo test --locked --workspace --exclude dsm_storage_node -- --nocapture --test-threads=1
  64 suites, 3761 passed, 0 failed, 19 ignored, WORKSPACE_EXIT=0

make lint   (repo root; fmt --check + clippy --all-targets as a pair)
  LINT_EXIT=0, 0 errors, "Lint passed."

An earlier board ran green before the final two changes landed and was discarded rather than reported — it described a different tree. make lint also caught one real thing here: EconomicSettlementReceiptState::new at 8 args over clippy's 7, resolved with #[allow(clippy::too_many_arguments)] matching 73 existing uses in this repo, including protocol-object constructors whose arity is fixed by a registry field table rather than by factoring.

…verifier, classifier

Step 3 sub-PR 1 of 5. The primitives an economic root is built from, and
nothing that depends on rules not yet defined.

What this establishes
---------------------
A closed write set: given an authenticated pre-root and an ordered list of
leaf mutations, recompute the post-root the mutations actually produce.

What it deliberately does NOT establish
---------------------------------------
Credit provenance. A closed write set proves WHAT changed, never WHY a credit
may appear — a verifier running only this would accept a trader crediting
itself from nothing, because each mutation in that write set is individually
well-formed. The six-arm CreditSource algebra is a separate, conjunctive
obligation and lands with sub-PR 4.

Contents
--------
- Nine DSM/economic-* domain tags. The tree shares no domain with the
  relationship SMT, so a relationship proof cannot be replayed as an
  economic one.
- CCB class discriminants 0x001B-0x0029 allocated in the single §3 namespace.
- Four key derivations, each the only place its key class is computed. Every
  one binds G || DevID, so the key space is per-identity by construction.
- Leaf states 0x001F-0x0022 with the zero asymmetry: a zero balance is the
  ABSENCE of the leaf; a zero reserve is PRESENT, because vault_sequence is
  still meaning. A settlement receipt recomputes its own receipt_id rather
  than carrying one.
- A non-evicting 256-deep SMT. The relationship SMT is a bounded FIFO cache
  that silently evicts over capacity, which is survivable for tips and not
  for balances.
- EconomicLeafMutation 0x001E. Carries no key — the key is derived from the
  states it carries, so "valid object at the wrong position" is
  unrepresentable rather than merely checked for. Exactly 256 siblings, as a
  validity condition: a short path proves a shallower tree.
- The sequential verifier. Each mutation authenticates against the root the
  previous one left, not against the transition pre-root.
- EconomicRootClaimBody 0x001B and EconomicAdmissionManifest 0x001C. The
  claim names admission_manifest_addr and nothing else about the evidence;
  transition_witness_digest and admission_evidence_digest are deleted,
  because a second digest beside the first is a second place to disagree.
- An exhaustive EconomicEffect classifier with NO wildcard arm, so a new
  Operation variant fails to compile until classified, plus the structural
  tripwire: a classification claiming no economic write is contradicted by
  any economic write.

Class 0x001D (EconomicTransitionWitness) is allocated but NOT encoded. Its
credit_sources field references classes 0x0023-0x0028 whose field tables do
not exist, and an encoder written now would fix how a witness references its
provenance by implementation accident — the "encoder defines the protocol"
inversion ccb/mod.rs refuses in its own header. The verifier therefore takes
an explicit non-wire input; the encoder lands with the rules it would
otherwise be inventing.

Verification
------------
22 tests. Three gates mutation-proven — remove the gate, a NAMED test goes
red by performing the forbidden action, restore it:

  post-root equality removed  -> a_changed_leaf_missing_from_the_write_set_fails
  pre-state membership removed -> a_pre_state_that_never_existed_fails
  strict-ascending removed     -> a_descending_write_set_is_refused_even_when_
                                  every_path_verifies
                                  a_repeated_key_is_two_disagreeing_claims_
                                  about_one_leaf

The first mutation initially reported green, which was correct reporting of a
broken test file: an earlier edit had silently deleted that control. The
mutation run is what surfaced it.
@cryptskii
cryptskii merged commit 6b32242 into main Aug 25, 2026
18 checks passed
@cryptskii
cryptskii deleted the feat/economic-primitives branch August 25, 2026 04:36
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