feat(economic): economic lineage and register - #727
Merged
Conversation
Step 3 sub-PR 3.3 of 6. The pure protocol coordinate and register semantics.
The durable state machine is 3.4 and is deliberately not here.
Registered is not validated
---------------------------
Writing a claim into a write-once cell establishes NON-EQUIVOCATION and
nothing else: this identity named one root at this position and can never
name a second. It says nothing about whether that root resulted from a valid
transition — a malicious trader registers an arbitrary root perfectly
consistently, and the register accepts it.
That separation is enforced by the compiler. ValidatedEconomicRoot has a
private field and NO public constructor taking a RegisteredEconomicRoot: no
From, no into_validated, no assume_valid. accepted_root != valid_root is the
load-bearing claim of the whole economic design, and a convenience conversion
added later "just for this call site" is exactly how it would stop being true.
Making the coercion unwritable costs nothing.
No successor constructor
------------------------
Advancing a validated root requires the local acceptance substrate to verify
AND to bind the same operation_digest as the witness. Both are 3.4. Without
the shared digest a trader presents a valid successor and a valid economic
transition DESCRIBING DIFFERENT OPERATIONS, so a constructor missing that
check would be strictly worse than none. Nothing in the module can produce
position k+1, so nothing can claim validation it has not performed. The full
predicate is in the module docs with the two missing conditions marked.
Two separate properties, not one
--------------------------------
K_root identity-scopes the cell
claimant attribution prevents third-party preemption of that cell
K_root = H(tag || G || DevID || u64_be(position)) gives each identity its own
coordinate space. It confers NO exclusivity: every input is public, so
K_root(G_victim, D_victim, k) is derivable by anyone, and the register being
write-once means one accepted value there burns the position forever. What
makes that write impossible is the member-side attribution check —
claimant_public_key and trader_devid must be the authenticated caller's — and
that is only as strong as the P0-P6 proof of the caller's key and device.
Register resolution fails closed
--------------------------------
An unknown network is refused rather than defaulted: a default register is one
an attacker can steer traffic into. resolve_for_trader additionally refuses a
network mismatch, which is what stops a trader minting a genesis under another
network whose profile names a different register. Member IDENTITIES live here;
dlv::beta_storage_profile keeps cardinality and threshold only, and the quorum
is READ from it rather than restated.
Claim envelope
--------------
EconomicRootClaimV1 carries the body's EXACT 0x001B CCB bytes, not a nested
proto mirror — a mirror would give one object two canonical forms with the
signature covering only one. Strict decode -> re-encode equality, because at a
write-once cell two byte-different encodings of "the same" claim are two
different values.
Activation
----------
The only way to obtain a ValidatedEconomicRoot. Succeeds exactly when the
device holds nothing, yielding position 0 at the canonical empty root. A
device already holding value cannot activate: calling its current holdings
position 0 would let it assert its own opening balances, which is self-rooting
at the base of the lineage. Migration is future work and must never be an
implicit snapshot.
Verification
------------
11 tests. Six gates mutation-proven, each with a positive control asserting
the mutation edit landed:
network mismatch -> a_trader_cannot_settle_under_a_network_it_did_not_
commit_to
unknown network -> an_unknown_network_fails_closed_rather_than_
defaulting
claimant attribution -> a_member_refuses_a_claim_that_is_not_the_callers
device attribution -> a_member_refuses_a_claim_that_is_not_the_callers
activation clean -> a_device_already_holding_value_cannot_call_its_
holdings_position_zero
envelope canonical -> a_decodable_but_noncanonical_envelope_is_refused
The canonical check initially SURVIVED its mutation. The test was dead, not
the gate: the tamper appended 0x00, which makes prost fail to decode, so the
call returned from a layer above the check. A negative test for a strictness
gate must fail AT that gate — the covering input is decodable but
non-canonical, i.e. an unknown protobuf field that prost silently skips.
Board: 66 suites, 3788 passed, 0 failed, 19 ignored, exit 0.
make lint: exit 0, 0 errors.
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 3.3 of 6. The pure protocol coordinate and register semantics. The durable state machine is 3.4 and is deliberately absent.
Registered is not validated, and the compiler enforces it
Writing a claim into a write-once cell establishes non-equivocation and nothing else: this identity named one root at this position and can never name a second. It says nothing about whether that root resulted from a valid transition — a malicious trader registers an arbitrary root perfectly consistently, and the register accepts it.
ValidatedEconomicRoottherefore has a private field and no public constructor taking aRegisteredEconomicRoot: noFrom, nointo_validated, noassume_valid.accepted_root != valid_rootis the load-bearing claim of the whole economic design, and a convenience conversion added later "just for this call site" is exactly how it would stop being true. Making the coercion unwritable costs nothing and deletes the failure mode.No successor constructor, on purpose
Advancing a validated root requires the local acceptance substrate to verify and to bind the same
operation_digestas the witness. Both are 3.4. Without the shared digest a trader presents a valid successor and a valid economic transition describing different operations, so a constructor missing that check would be strictly worse than none.Nothing in the module can produce position
k+1. The full predicate is written out in the module docs with the two missing conditions marked, so the gap is legible rather than implied.Two separate properties, not one
K_root = H(tag ‖ G ‖ DevID ‖ u64_be(position))gives each identity its own coordinate space. It confers no exclusivity: every input is public, soK_root(G_victim, D_victim, k)is derivable by anyone, and write-once storage means one accepted value there burns the position forever. What makes that write impossible is the member-side attribution check —claimant_public_keyandtrader_devidmust be the authenticated caller's — and that is only as strong as the P0–P6 proof of the caller's key and device.An earlier draft of these docs credited
K_rootwith the anti-squatting property. That was wrong, and wrong in a way worth naming: crediting the wrong component makes the real gate look redundant, which is how a load-bearing check gets deleted by a later reader.Register resolution fails closed
An unknown network is refused rather than defaulted — a default register is one an attacker can steer traffic into.
resolve_for_traderadditionally refuses a network mismatch, which is what stops a trader minting a genesis under another network whose profile names a different register and presenting roots from there as though they came from here.Member identities live in
economic::register;dlv::beta_storage_profilekeeps cardinality and threshold only, and the quorum is read from it rather than restated. Two modules asserting membership would be two places for the fleet to be described differently.Claim envelope
EconomicRootClaimV1carries the body's exact0x001BCCB bytes, not a nested proto mirror — a mirror would give one object two canonical forms with the signature covering only one. Strict decode → re-encode equality, because at a write-once cell two byte-different encodings of "the same" claim are two different values, and a member storing a padded one holds bytes that can never win a quorum.Activation
The only way to obtain a
ValidatedEconomicRoot. Succeeds exactly when the device holds nothing, yielding position 0 at the canonical empty root — verifier-derived, never trader-chosen. A device already holding value cannot activate: calling its current holdings position 0 would let it assert its own opening balances, which is self-rooting at the base of the lineage. Migration is future work and must never be an implicit snapshot.Deferred to 3.4 (agreed)
Economic position persistence. A durable
economic_positionhas no correct transition semantics until 3.4 answers when it increments, what commits atomically with it, what happens on a crash between DSM acceptance and root registration, and whether a pending position can be reused.Verification
11 tests. Six gates mutation-proven, each with a positive control asserting the mutation edit actually landed:
a_trader_cannot_settle_under_a_network_it_did_not_commit_toan_unknown_network_fails_closed_rather_than_defaultinga_member_refuses_a_claim_that_is_not_the_callersa_member_refuses_a_claim_that_is_not_the_callersa_device_already_holding_value_cannot_call_its_holdings_position_zeroa_decodable_but_noncanonical_envelope_is_refusedThe canonical check initially survived its mutation — deletable with every test still green. The test was dead, not the gate: the tamper appended
0x00, which makes prost fail to decode, so the call returned from a layer above the check and never reached it. A negative test for a strictness gate must fail at that gate; the covering input is decodable-but-non-canonical, i.e. an unknown protobuf field that prost silently skips. Added, and the mutation now goes red.make lintalso caught a real one that tests could not: clippytype_complexityon a fn-pointer array. The two halves cover genuinely different failures.Board — both halves, on this exact tree
An earlier board went green before the wording correction and the lint fix landed, and was discarded rather than reported — it described a different tree.