Skip to content

feat(economic): credit provenance semantics - #729

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

feat(economic): credit provenance semantics#729
cryptskii merged 1 commit into
mainfrom
feat/economic-provenance-semantics

Conversation

@cryptskii

Copy link
Copy Markdown
Collaborator

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_validated proves it, link by link:

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. 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:

ProvenanceResolver returns ValidatedPeerTransition
  which CONTAINS a ValidatedEconomicRoot
    which has 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 — 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_root must equal validated_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.

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.

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:

Gate removed Test that goes red
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, because the looser phrasing would overclaim: 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 — both halves, run separately, each with its own exit code

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

make lint   (repo root)
  LINT_EXIT=0, 0 errors, "Lint passed."

An earlier run of these was discarded rather than reported: make lint had failed with too_many_arguments (8/7) on advance_validated after it gained the resolver parameter, and my command had piped that half through tail and 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.

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.
@cryptskii
cryptskii merged commit 39f3edb into main Aug 25, 2026
17 of 18 checks passed
@cryptskii
cryptskii deleted the feat/economic-provenance-semantics branch August 25, 2026 20:33
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