fix(economic): refuse issuance at the accepting layer until 0x0029 exists - #747
Merged
cryptskii merged 1 commit intoAug 29, 2026
Merged
Conversation
…ists
`token.mint` credited arbitrary non-builtin assets into the device head with no
admission. The 3.5b plan recorded that as a harmless residual — "purely local
self-strand" — and two facts that postdate the note falsify it.
`dlv.create` funds vaults from `head.balance()` rather than `R_econ`, so minted
units became vault reserves in a market no verifier can accept. And `activate()`
refuses a device whose balance map is non-empty, so holding them is a ONE-WAY
DOOR: the identity can never enter the economic model at all. `R_econ` itself
was never inflated — a trader settling against such a vault needs the owner's
root Validated, which a minting owner can never have — so the guarantee held.
What failed was the product: a parallel unadmittable market plus a permanent
lockout, reachable from a live production route.
The refusal belongs at the accepting layer. A route guard binds one caller and a
write-set rule binds only paths that build a write set; `DeviceState::advance`
is the one layer that sees every caller, present and future. Positive `Mint` now
refuses for every asset, with the builtin reason kept distinct from the new one
— a builtin's issuance is not self-authorizable at all, while a user asset's is
authorizable in principle and simply has no predicate yet.
THE SAME SHAPE EXISTED TWICE. `CreateToken { initial_supply > 0 }` was refused
at a route guard and in the write-set builder, but `validate_conservation`
explicitly PERMITS the issuance leg and nothing in `advance` refused it. No
production caller can reach it — the single constructor passes only the fee
debit — which is why it was free to close now rather than after a future caller
closed it in the wrong direction. Fenced symmetrically.
Both refusals stay narrow: a zero-amount mint creates nothing and is not
claimed, and zero-supply token creation remains an ordinary fee spend.
The `token.mint` route body is DELETED, not left unreachable. It signed a
self-authorization with the caller's own device key — the shape that must change
completely once issuance carries `0x0029` evidence — so keeping it would
preserve a path whose only remaining purpose was the thing being refused.
`token.burn` still uses the shared helpers, so nothing is orphaned.
Test integrity, found by an adversarial fixture audit rather than by the suite,
which was green throughout: adding a gate near the top of a chokepoint silently
disables every test whose subject is a check further down. `advance_rejects_
balance_overflow` minted into a `u64::MAX` balance and asserted `is_err()`; the
new refusal preempted the `checked_add`, so it stopped testing overflow while
staying green. `mint_of_an_unknown_token_fails_closed` became vacuous, since the
route refuses before resolving the token. Both now assert on the REASON, so a
future upstream gate breaks them loudly.
Eight core tests used a mint as a funding or credit vehicle: funding-only moved
to `with_balance_for_testing`, value-bearing advances to burns, and the one whose
subject IS a credit to the production shape — a credit-direction Transfer with
its Prepared admission attached.
Deliberately NOT fenced: `DlvSettle` credits un-admitted and self-authorized,
but unlike issuance it must debit a different asset (creating nothing) and
`R_econ` can already fund it through the `DlvSettle` write set and the 0x0026
arm. It is a held cutover, not a missing predicate, and fencing it would break
the trader settle path with no replacement.
cryptskii
deleted the
fix/fence-unadmitted-issuance-at-the-accepting-layer
branch
August 29, 2026 18:48
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.
Refuse issuance at the accepting layer until
0x0029existsFrozen-path step 1.
0x0030is not parameterized,0x0029is not started, andCreateTokenis not redesigned — only the missing chokepoint refusal your "confirm there is no alternate accepting path" instruction surfaced.The defect, and why the plan's note about it was wrong
token.mintcredited arbitrary non-builtin assets into the device head with no admission. The 3.5b plan recorded that as "purely local self-strand". Two facts that postdate the note falsify it:dlv.createfunds vaults fromhead.balance(), notR_econ(dlv_routes.rs:598-613) — so minted units become vault reserves in a market no verifier can accept.activate()refuses a device whose balance map is non-empty (economic_admission_flow.rs:118-124) — so holding them is a one-way door: the identity can never enter the economic model at all.R_econwas never inflated: a trader settling against such a vault needs the owner's root Validated, which a minting owner can never have. The guarantee held. What failed was the product — a parallel unadmittable market plus a permanent lockout, reachable from a live route (app_router_impl.rs:3447).The fence, and the second instance of the same shape
A route guard binds one caller; a write-set rule binds only paths that build a write set.
DeviceState::advanceis the one layer that sees every caller, present and future. PositiveMintnow refuses there for every asset, keeping the two reasons distinct — a builtin's issuance is not self-authorizable at all, while a user asset's is authorizable in principle and simply has no predicate yet.CreateToken { initial_supply > 0 }had exactly the same gap, and I would not have looked without your instruction to confirm it.validate_conservationexplicitly permits the issuance leg (device_state.rs:614-628) and nothing inadvancerefused it; both existing refusals — the route guard attoken_routes.rs:1583andCreateTokenInitialSupplyRequiresIssuancePredicateatwrite_set.rs:397— sit outside the chokepoint. No production caller can reach it, since the single constructor passes only the ERA fee debit, which is precisely why it was free to close now rather than after some future caller closed it in the wrong direction.Both refusals stay narrow: a zero-amount mint creates nothing and is not claimed; zero-supply token creation remains an ordinary fee spend.
The
token.mintroute body is deleted, not left unreachable — it signed a self-authorization with the caller's own device key, the shape that must change completely once issuance carries0x0029evidence.token.burnstill uses the shared helpers, so nothing is orphaned.Mutation controls (red-then-restored)
no_asset_mints_from_air_and_the_two_refusals_stay_distinct— deleting theMintblock turns it red by actually crediting: the outcome carriedbalances: 1. It fails on the forbidden action, not on a message, so a gate moved somewhere useless would not keep it green.creating_a_token_with_initial_supply_is_refused_at_the_accepting_layer— deleting theCreateTokenblock turns it red by issuing 500 units of a brand-new asset from air.Test integrity — the part the suite could not catch
An adversarial fixture audit (four independent search modalities, each with a refutation pass) found that the new gate silently broke two tests while the suite stayed green:
advance_rejects_balance_overflowminted 1 into au64::MAXbalance and assertedis_err(). The new refusal preempts the delta loop'schecked_add, so it stopped exercising overflow entirely — green, proving nothing.mint_of_an_unknown_token_fails_closedbecame vacuous: the route refuses before resolving the token, so an unknown ticker and a valid one take the identical path.Both now assert on the reason (
err.contains("overflow");msg.contains("0x0029")for a known token), so a future upstream gate breaks them loudly instead of quietly inheriting them.Eight core tests used a mint as a funding or credit vehicle: funding-only moved to
with_balance_for_testing, value-bearing advances to burns, and the one whose subject genuinely is a credit moved to the production shape — a credit-directionTransferwith its Prepared admission attached.What the sweep cleared, and one thing it did not
Cleared (stated so the claim is falsifiable): the dBTC tap —
resolve_policy_commit_strictresolves "dBTC" to the builtin commit first, so the tap's mint is refused by name with no fallback credit, no direct head write, and no non-builtin wrapper path. Alsodlv.claim/dlv.invalidate(conservation catch-all), theDSM_DEV_SEEDpath,token_sdk's mint,canonical_rebuild(fabricates admissions only for Transfer/FaucetClaim), and the cfg-quarantined balance installers.Not fenced, deliberately:
DlvSettle. It credits un-admitted and self-authorized —require_attached_dsm_admissionstill has only two call sites — but it is structurally different from issuance:Mint(fenced)DlvSettle(deferred)R_econfund it0x0023fails closed,0x0029unwrittenDlvSettlewrite set and the0x0026arm landed in PR3 (#743)So it is a held cutover (step 5), not a missing predicate; fencing it now would break the trader settle path with no replacement. The residual is real and recorded: a trader settling today strands its own activation, which is a concrete reason step 5 must run on fresh identities.
Boards (exact CI commands, final tree, pinned 1.98.0)
cargo test --locked --workspace --exclude dsm_storage_node -- --nocapture --test-threads=1— 3904 passed / 0 failed across 71 suites, exit 0cargo test --locked -p dsm_storage_node --no-default-features --features local-dev,strict -- --nocapture— 270 passed / 0 failed, exit 0make lint(repo root) — exit 0; pinned repo-rootcargo clippy --all-targets— exit 0bash ci/production_safety_checks.sh— exit 0Next (step 2): implement
0x0029behind the existing0x0023arm — the verifier reruns policy enforcement (no caller-suppliedpolicy_passed), supply facts derive from canonical economic state, and authorization is never treated as backing.