feat(economic): R_econ primitives — tree, keys, leaf states, witness verifier, classifier - #724
Merged
Merged
Conversation
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
CreditSourcealgebra 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
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.0x001B–0x0029allocated in the single §3 namespace. Next free stays0x0030.G ‖ DevID, so the key space is per-identity by construction.0x001F–0x0022, with the zero asymmetry below.EconomicLeafMutation0x001E.EconomicRootClaimBody0x001B,EconomicAdmissionManifest0x001C.EconomicEffectwith no wildcard arm, plus the structural tripwire.Zero is not one thing
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_sequenceis 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_leafis 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, soecon_leafbinds 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_addrand nothing else.transition_witness_digestandadmission_evidence_digestare 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 classes0x0023–0x0029are allocated but have no encoder. Theircredit_sourcesfield 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" inversionccb/mod.rsrefuses in its own header.This is structural, not a comment. The discriminants live in a new
ccb::reservedmodule, separate fromccb::class:CcbObjectimpl needs aCLASSconstant, so writing an encoder for a reserved class requires physically moving the constant intoclass— a deliberate, reviewable diff that must arrive with the field table earning it.reserved_classes_have_no_encoderasserts every economic type that can produce canonical bytes draws from the live namespace. Positive-controlled: pointingEconomicLeafMutation::CLASSatreserved::ECONOMIC_TRANSITION_WITNESSfails it withclass 0x001d has an encoder but is listed as reserved.a_reserved_class_is_unusable_on_the_wire_until_its_schema_is_installedpinsreserved::ALLto the exact set, so promotion cannot happen silently, and asserts0x0030is not reserved — it is the next free class, and conflating free with reserved wastes a discriminant.0x001Dis therefore not merely unimplemented: it has no canonical bytes to produce, hash-address, nest or sign. The verifier takesEconomicMutationSequence— 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:
a_changed_leaf_missing_from_the_write_set_failsa_pre_state_that_never_existed_failsa_descending_write_set_is_refused_even_when_every_path_verifies,a_repeated_key_is_two_disagreeing_claims_about_one_leafThe 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 differentGorDevID.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.OfflineAccountOnlyto the same standard asNone: the offline allocation lives outsideR_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
An earlier board ran green before the final two changes landed and was discarded rather than reported — it described a different tree.
make lintalso caught one real thing here:EconomicSettlementReceiptState::newat 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.