feat(economic): credit provenance semantics - #729
Merged
Conversation
Step 3 sub-PR 3.5a of 6. Why a credit may appear, as a conjunctive obligation
on validation.
The finding this PR surfaces
----------------------------
Wiring provenance into `advance_validated` proves that NO VALUE CAN CURRENTLY
ENTER A VALIDATED LINEAGE:
activate() gives position 0 at the EMPTY root, the only entry point
-> a first transition's mutations are all INSERTIONS
-> inserting a balance or reserve with amount > 0 IS a positive credit
-> every credit needs a funding source
-> the only source that CREATES units is AuthorizedIssuance
-> its predicate does not exist; 0x0029 stays reserved
Valueless transitions still validate — a record-leaf insertion is not a credit
— so the machinery is proven end to end. But the wallet is unfundable. This is
the system correctly refusing to create value from nothing, and it is the same
absence that makes `advance()` refuse builtin ERA/dBTC issuance. Captured as
`value_cannot_enter_a_lineage_without_an_issuance_predicate`.
Acyclicity is a type property
-----------------------------
An external source must resolve from an ALREADY-VALIDATED root and must never
depend on validating the transition it funds. That is enforced by the type
system, not by review: the resolver returns ValidatedPeerTransition, which
CONTAINS a ValidatedEconomicRoot — a type with a private field and no
constructor except activate/advance_validated. A resolver cannot fabricate a
peer root it has not validated.
ValidatedEconomicRoot was made unconstructible in 3.3 for a DIFFERENT invariant
(registered must not coerce into validated). It carried this one for free.
The residual forgery the type system cannot stop alone — a genuine validated
root paired with an unrelated witness — is checked explicitly.
What a source must establish
----------------------------
Not that it exists: that it funds THIS credit, same asset and same amount. A
real debit of 30 cannot fund a credit of 500. That is where a plausible-looking
provenance object stops being sufficient.
Consumed-source records are required for the EXTERNAL arms only. A
SameTransitionMove's debit is inside the same write set, consumed by
construction, and could never be re-presented; demanding a record would be
bookkeeping for an impossibility. Where required, the leaf must be written FROM
ZERO and name THIS operation as consumer — both checkable from the witness
alone, with nothing fetched.
AuthorizedIssuance refuses rather than inventing a predicate. Inventing one
here would be exactly the disguised backdoor the mint repair avoided.
A fixture was wrong, not the verifier
-------------------------------------
Four tests failed on first run because the fixture debited ERA and credited
SOFI. `SameTransitionMove` is a MOVE, not a swap: it relocates the SAME asset,
the DlvCreateFundedV2 balance-to-reserve shape. The fixture asserted a
semantics the protocol does not have. Rebuilt it, and kept the cross-asset case
as its own named refusal test rather than losing the boundary.
Verification
------------
19 tests across two suites. Five gates mutation-proven, each with a positive
control asserting the edit landed:
amount match -> a_source_must_fund_this_credit_not_merely_exist
asset match -> an_asset_mismatch_is_refused
issuance refusal -> authorized_issuance_cannot_be_resolved_by_anyone
duplicate source id -> duplicate_source_ids_are_refused
provenance call path -> value_cannot_enter_a_lineage_without_an_issuance_
predicate
Stated exactly: the SOURCE VERIFIER rejects unsupported issuance, and
`advance_validated` is proven to REQUIRE provenance resolution. That is one
predicate plus one mandatory call path, not two independent issuance
predicates.
Board: 68 suites, 3811 passed, 0 failed, 19 ignored, WORKSPACE_EXIT=0.
make lint: LINT_EXIT=0, 0 errors.
Each half run separately with its own captured exit code.
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.5a of 6. Why a credit may appear, as a conjunctive obligation on validation.
Read this first: no value can currently enter a validated lineage
Wiring provenance into
advance_validatedproves it, link by link:Valueless transitions still validate — a record-leaf insertion is not a credit — so the machinery is proven end to end. But the wallet is unfundable.
This is the system correctly refusing to create value from nothing, and it is the same absence that makes
advance()refuse builtin ERA/dBTC issuance. It is captured as a named test,value_cannot_enter_a_lineage_without_an_issuance_predicate, rather than left to be discovered during integration. It is also what redirected the faucet design: distribution from a finite pre-allocated source, never issuance.Acyclicity is a type property, and it came free
An external source must resolve from an already-validated root and must never depend on validating the transition it funds. Enforced by the type system rather than by review:
ValidatedEconomicRootwas made unconstructible in 3.3 for a different invariant — stopping a registered root coercing into a validated one. It carried this one for free. Worth noting as a pattern: an unconstructible type built for one invariant may already carry another.The one forgery the type system cannot stop alone — a genuine validated root paired with an unrelated witness — is checked explicitly:
witness.post_economic_rootmust equalvalidated_root.economic_root().What a source must establish
Not that it exists — that it funds this credit: same asset, same amount. A real debit of 30 cannot fund a credit of 500. That is where a plausible-looking provenance object stops being sufficient.
Consumed-source records are required for the external arms only. A
SameTransitionMove's debit sits inside the same write set, is consumed by construction, and could never be re-presented; demanding a record would be bookkeeping for an impossibility. Where required, the leaf must be written from ZERO and name this operation as consumer — both checkable from the witness alone, nothing fetched.AuthorizedIssuancerefuses rather than inventing a predicate. Inventing one here would be exactly the disguised backdoor the mint repair avoided.A fixture was wrong, not the verifier
Four tests failed on first run because the fixture debited ERA and credited SOFI.
SameTransitionMoveis a move, not a swap — it relocates the same asset, theDlvCreateFundedV2balance-to-reserve shape. The fixture asserted a semantics the protocol does not have.The fast read — "my new code broke the tests" — would have led to weakening a correct check. Rebuilt the fixture, and kept the cross-asset case as its own named refusal test so the boundary is preserved rather than lost.
Verification
19 tests across two suites. Five gates mutation-proven, each with a positive control asserting the edit landed at the mutated site:
a_source_must_fund_this_credit_not_merely_existan_asset_mismatch_is_refusedauthorized_issuance_cannot_be_resolved_by_anyoneduplicate_source_ids_are_refusedvalue_cannot_enter_a_lineage_without_an_issuance_predicateStated exactly, because the looser phrasing would overclaim: the source verifier rejects unsupported issuance, and
advance_validatedis proven to require provenance resolution. That is one predicate plus one mandatory call path — not two independent issuance predicates.Board — both halves, run separately, each with its own exit code
An earlier run of these was discarded rather than reported:
make linthad failed withtoo_many_arguments (8/7)onadvance_validatedafter it gained the resolver parameter, and my command had piped that half throughtailand chained both halves under a single$?, so the failure was invisible. Each half is now captured separately. The fix is an#[allow]with a comment recording why the arity is real — every argument is a distinct authenticated input to the conjunctive predicate, and bundling any of them would hide which facts a caller must establish independently.