Skip to content

refactor(sofi): vault genesis acceptance is formal only - #955

Merged
cryptskii merged 1 commit into
mainfrom
refactor/sofi-genesis-acceptance-is-formal-only
Sep 21, 2026
Merged

cryptskii merged 1 commit into
mainfrom
refactor/sofi-genesis-acceptance-is-formal-only

Conversation

@cryptskii

Copy link
Copy Markdown
Collaborator

Vault genesis acceptance is formal only

genesis_accepted was a twelve-conjunct predicate that took a presented creation operation and an owner root and returned a vault id. It could never acquire a production caller, because VaultCreation carries no asset commitments: proving the creation leaf establishes the amounts and never which balances were debited, so a caller could hand in an A/B-shaped sibling of an operation that actually debited X/Y.

Three artifacts disagreed about what to do. A CI gate failed the build on any production caller. The G1 baseline told R14 to wire it or delete it. Spec §28.5 and §30.1 required it. Wiring it failed the gate; deleting it contradicted the spec.

Per the ruling in §44.4: the standalone production predicate was the stale piece, not the gate.

What goes, and what does not

Deleted from Rust: genesis_accepted, CreationFunding, the eight GenesisError variants only it raised, and its six tests. The three variants build_vault_create actually uses stay, and the enum's doc now says what it is rather than what it was.

The rule is not deleted. It is stated in the formal layer, which already carried it: GenesisAccepted and genesis_requires_validated_creation in Lean, GenesisCanonicalOnlyIfCreationValid in TLA. ci/sofi_genesis_acceptance_binding.sh is repurposed to keep both halves true — no Rust predicate, and the formal definitions still present. A deletion that quietly took the statement with it would be the same hole by another route.

What production actually does, stated plainly

It recognizes, it does not accept. A trader fetches a vault's genesis by its locator and keeps the first candidate whose recomputed vault_id is that locator, then compares genesis_root(v, state) against the parent root its own precommit names.

vault_id is H(tag ‖ G_o ‖ DevID_o ‖ p_create) and does not depend on the vault state. So those three public coordinates are enough for any party to index a preimage with arbitrary reserves and policies under the right locator, and the recognizer keeps whichever arrives first. Nothing yet binds a genesis to the owner transition that funded it.

This is not an argument from reading. The TLA fault _RegisteredGenesisAccepted models a walk that starts from a merely stored genesis, which is exactly what production does, and it violates GenesisCanonicalOnlyIfCreationValid over 1,758,084 states: a vault's parent is consumed though its creation was never validated. Closing it is F10's VerifiedVaultCreation, bound to the exact accepted owner transition at p_create. §19.8 now records the gap instead of implying it is closed.

Controls

Mutation Result
A Rust genesis-acceptance predicate returns the gate fails, naming the file and line
The formal statement is renamed away the gate fails, naming the definition

The second passed on its first run: the gate grepped for the bare name, and a use site still spelled it after the definition was renamed. It now requires the definition form. That is the third time in this step that matching a spelling instead of the thing produced a green gate, and it is the same defect the reachability gate carried for thirteen steps.

Gates

  • Core (release): sofi:: 175/0. SDK (release, --features test-utils): sdk::sofi_* 48/0.
  • make lint exit 0; ci/production_safety_checks.sh passed; G1 baseline 7 → 6 lines.

Spec

§19.8 gains the rule that this predicate has no Rust form and why, with the executed fault. §28 step 5 and §30 step 1 say recognize rather than accept. §41.3's row records the deletion.

genesis_accepted was a twelve-conjunct predicate that took a PRESENTED
creation operation and an owner root and returned a vault id. It could
never acquire a production caller, because VaultCreation carries no asset
commitments: proving the creation leaf establishes the AMOUNTS and never
which balances were debited, so a caller could hand in an A/B-shaped
sibling of an operation that actually debited X/Y. A CI gate existed to
keep it callerless, and the G1 baseline told R14 to wire or delete it.
Wiring failed the gate; deleting contradicted the spec.

Owner ruling §44.4: the standalone production predicate was the stale
piece, not the gate. It is deleted from Rust with CreationFunding, the
eight GenesisError variants only it used, and its six tests. The three
variants build_vault_create actually uses stay.

The rule is not deleted. It is stated in the formal layer, which already
carried it: Lean GenesisAccepted and genesis_requires_validated_creation,
TLA GenesisCanonicalOnlyIfCreationValid. The binding gate is repurposed
to keep BOTH halves true — no Rust predicate, and the formal definitions
still present, because a deletion that quietly took the statement with it
would be the same hole by another route.

What production does instead is RECOGNIZE, not accept: the genesis is
kept if its recomputed vault_id is the locator it was found under, and
genesis_root is compared to the parent root the trader's own precommit
names. vault_id does not depend on the vault state, so those three public
coordinates let any party index a preimage under the right locator and
the recognizer keeps the first that arrives. Nothing yet binds a genesis
to the owner transition that funded it; F10's VerifiedVaultCreation owes
that. Executed against the gap: the TLA fault _RegisteredGenesisAccepted,
which models a walk starting from a merely stored genesis, violates
GenesisCanonicalOnlyIfCreationValid (1,758,084 states) — a vault's parent
is consumed though its creation was never validated.

Gate controls, each restored byte-identical:
  M1 a Rust genesis-acceptance predicate returns -> the gate fails, naming it
  M2' the formal statement is renamed away       -> the gate fails, naming it

M2 first passed while the definition had been renamed away, because a use
site still spelled the name. The gate now requires the DEFINITION. That is
the third time in this step that matching a spelling instead of the thing
produced a green gate, and it is the same defect G1 carried for thirteen.

G1 baseline 7 -> 6 lines. Spec §19.8, §28.5, §30.1, §41.3.
@cryptskii
cryptskii merged commit 4090ab6 into main Sep 21, 2026
21 checks passed
@cryptskii
cryptskii deleted the refactor/sofi-genesis-acceptance-is-formal-only branch September 21, 2026 13:45
@cryptskii

Copy link
Copy Markdown
Collaborator Author

Correction to the scope of the gap, and the design that closes it

The PR body and the commit message overstate what the recognition weakness permits. The correct bound, ruled today:

A token must be pre-added to the device's validated state before any trade or vault operation can use it. A SoFi operation cannot introduce a new token by naming an asset id, so A and B must already be established assets in the owner's state. The gap is therefore not "arbitrary tokens can be conjured at vault creation". It is exactly and only the binding:

VaultGenesis(v, A, x, B, y)  ⟷  AcceptedCreationTransition  ⟷  the exact debited balances

What closes it, and it needs no new proof system. The creation operation carries, or commits to, the ordinary SMT inclusion proofs against the creator's actual validated predecessor root — Balance(A) = a with a ≥ x, Balance(B) = b with b ≥ y. Core verifies them, applies the exact debits a' = a − x and b' = b − y, recomputes the creator's post-root, and requires the genesis to bind the same amounts as reserves. Predecessor root, inclusion proofs, exact leaf mutations, recomputed post-root, genesis bound to those debits — all machinery that already exists. The bytes need not be inline in VaultCreation so long as the operation commits to content-addressed evidence Core can acquire and verify deterministically. VerifiedVaultCreation is then derived, never asserted.

That also explains this PR cleanly: genesis_accepted was handed a creation object after the fact, without the evidence required to prove which predecessor assets were debited. That is why it could never take a production caller, and why deleting it was right rather than weakening it until it became callable.

Recorded as spec §44.5, with §19.8 corrected to state the bound rather than imply a wider one.

One enforcement question I could not answer from source

I could not find where the pre-add rule is enforced on the SoFi resolved path, and I would rather ask than assume.

  • R_econ holds Balance, ConsumedSource, Relationship and VaultCreation leaves. There is no token-registry leaf.
  • A SofiFulfill never reaches advance_validated, so it never crosses that path's token-policy conjunct: write_set.rs refuses it by name with SofiWriteSetBelongsToTheResolvedPath, and the root is selected by advance_resolved.
  • sofi/validation.rs performs no registry lookup, and balance_after treats an absent leaf as 0 before crediting, so a settlement can create a balance leaf for a token the trader held none of.

So either the rule is enforced at a layer I have not found — the device-level token registry, or the market policy being a content-addressed object the vault commits — or it is the second half of the same binding F10 owes. Which is it?

cryptskii added a commit that referenced this pull request Sep 22, 2026
`SofiVaultCreate` already planned two real balance debits against the
admitted `R_econ` and bound the funded AMOUNTS to the genesis reserves,
on the owner's path and on foreign walks alike. Five other facts it
stated were taken on the caller's word.

The sharpest was the funding ASSETS. `funding_a/b_policy_commit` went
straight into `pair_legs`, which checks only canonical order and
non-zero, and `write_set.rs` named `market_policy` nowhere. So a
creation could debit X and Y while declaring a market in A and B — and
a later close credits the owner the MARKET's pair, which it never
funded. Fund with junk, close for real assets.

That binding had existed, in `genesis_accepted`:

    if funding.pair() != (token_a, token_b) {
        return Err(GenesisError::FundingIsNotTheMarketPair);
    }

#955 deleted that function correctly — it could not derive provenance
and was never callable — but the deletion took a working check with it.
Only the KEPT error variants were checked for remaining references, and
those references were the SDK producer. Deleting an architectural dead
end without relocating the valid invariant it happened to enforce is
the defect, not the deletion.

Core now refuses unless all of it holds, before any debit is planned:
the three preimages strictly decode; the owner coordinates are the
authenticated local ones; `create_position` is the position the
operation actually lands at; the record names the vault the preimage
derives; `genesis_root` is recomputed rather than accepted; the funded
amounts are the genesis reserves; the state's owner coordinates equal
the preimage's; the carried policy re-addresses to what the state
commits; and the decoded pair is the pair being funded.

The operation carries `market_policy_preimage`, so acceptance is a
function of the operation's bytes and the authenticated pre-state
alone — no resolver, no storage availability, no foreign-walk liveness
between a local decision and its answer. 72 fixed-width bytes against
the 49,856-byte signature already there. Coverage is automatic:
`operation_signing_bytes` is the whole canonical encoding with the
signature cleared, so one line in the encode arm is necessary and
sufficient — and a test proves it by altering only that field after
signing.

No decode-and-re-encode check. `MarketPolicy` is 72 bytes with no
optional position, its decoder pins the envelope and the beta family
before reconstruction, enforces `a < b` and refuses trailing bytes, so
`encode(decode(P)) == P` always and the assertion would have no
reachable rejection case.

ADDRESS EQUALITY AND PAIR EQUALITY ARE SEPARATE BINDINGS. One proves
the carried bytes are the policy object the state named; the other
proves the assets debited are the two that policy authorizes. Their
mutations red disjoint test sets.

The creation fixture now takes the MARKET pair separately from the
FUNDING pair. A fixture that conflated them could not express the
defect, which is why seven existing tests passed over it.
cryptskii added a commit that referenced this pull request Sep 22, 2026
`SofiVaultCreate` already planned two real balance debits against the
admitted `R_econ` and bound the funded AMOUNTS to the genesis reserves,
on the owner's path and on foreign walks alike. Five other facts it
stated were taken on the caller's word.

The sharpest was the funding ASSETS. `funding_a/b_policy_commit` went
straight into `pair_legs`, which checks only canonical order and
non-zero, and `write_set.rs` named `market_policy` nowhere. So a
creation could debit X and Y while declaring a market in A and B — and
a later close credits the owner the MARKET's pair, which it never
funded. Fund with junk, close for real assets.

That binding had existed, in `genesis_accepted`:

    if funding.pair() != (token_a, token_b) {
        return Err(GenesisError::FundingIsNotTheMarketPair);
    }

#955 deleted that function correctly — it could not derive provenance
and was never callable — but the deletion took a working check with it.
Only the KEPT error variants were checked for remaining references, and
those references were the SDK producer. Deleting an architectural dead
end without relocating the valid invariant it happened to enforce is
the defect, not the deletion.

Core now refuses unless all of it holds, before any debit is planned:
the three preimages strictly decode; the owner coordinates are the
authenticated local ones; `create_position` is the position the
operation actually lands at; the record names the vault the preimage
derives; `genesis_root` is recomputed rather than accepted; the funded
amounts are the genesis reserves; the state's owner coordinates equal
the preimage's; the carried policy re-addresses to what the state
commits; and the decoded pair is the pair being funded.

The operation carries `market_policy_preimage`, so acceptance is a
function of the operation's bytes and the authenticated pre-state
alone — no resolver, no storage availability, no foreign-walk liveness
between a local decision and its answer. 72 fixed-width bytes against
the 49,856-byte signature already there. Coverage is automatic:
`operation_signing_bytes` is the whole canonical encoding with the
signature cleared, so one line in the encode arm is necessary and
sufficient — and a test proves it by altering only that field after
signing.

No decode-and-re-encode check. `MarketPolicy` is 72 bytes with no
optional position, its decoder pins the envelope and the beta family
before reconstruction, enforces `a < b` and refuses trailing bytes, so
`encode(decode(P)) == P` always and the assertion would have no
reachable rejection case.

ADDRESS EQUALITY AND PAIR EQUALITY ARE SEPARATE BINDINGS. One proves
the carried bytes are the policy object the state named; the other
proves the assets debited are the two that policy authorizes. Their
mutations red disjoint test sets.

The creation fixture now takes the MARKET pair separately from the
FUNDING pair. A fixture that conflated them could not express the
defect, which is why seven existing tests passed over it.
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