Repository navigation
fix(sofi): a vault creation cannot contradict itself (F9.x) - #961
Merged
Merged
Conversation
`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.
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.
Split out of F10 by owner ruling, because it protects a different invariant.
What was already there
semantic_write_set'sSofiVaultCreatearm already planned two real balance debits against the admittedR_econ(refusingInsufficientBalance) plus an insert-only creation leaf, with the verifier half requiring exactly those three mutations — on the owner's path and on foreign walks alike.S_pre −(x,y)→ S_postwas derivable, and the funded amounts were bound to the genesis reserves. The framing "creation carries no balance evidence" is wrong about amounts.The asset-substitution door
funding_a/b_policy_commitpassed straight intopair_legs, which checks onlyleg_a.0 < leg_b.0and non-zero.write_set.rsreferencedmarket_policynowhere. So a creation could debit X and Y while its genesis declared a market in A and B — and a later close credits the ownerpolicies.market.token_a()/token_b(). Fund with junk, close for real assets.That binding had existed, in
genesis_accepted:#955 deleted that function correctly — it could not derive provenance and was never callable — but the deletion took a working check with it. I verified only that the kept error variants were still referenced, 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.
The rule, in Core, before any debit is planned
Three preimages strictly decode · owner genesis and DevID are the authenticated local ones ·
create_positionis the position the operation lands at ·record.vault_id == preimage.vault_id()·record.genesis_root == lineage::genesis_root(vault_id, state)·record.amount_{a,b} == state.reserve_{a,b}· the inner state's owner coordinates equal the outer preimage's ·policy_object_address(MARKET_POLICY, market_policy_preimage) == state.market_policy· the decoded pair equals the funding pair · the pair stays canonically ordered with both amounts non-zero.Address equality and pair equality are separate bindings and are not collapsed. 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 wire change
Operation::SofiVaultCreatecarriesmarket_policy_preimage: Vec<u8>, so acceptance is a function of the operation's bytes and the authenticated pre-state alone — no resolver, storage-availability or foreign-walk liveness assumption between a local decision and its answer. 72 fixed-width bytes against the 49,856-byte SPHINCS+ signature already there.Signature coverage is automatic —
operation_signing_bytes(op) = op.with_cleared_signature().to_bytes(), the verify arm passes the whole operation, no separate signed-field list — so one line in the encode arm is necessary and sufficient. A test proves it isn't tautological by altering only that field after signing.No decode-and-re-encode check.
MarketPolicyis 72 bytes with no optional position;decode_market_policypins class and schema, pins the beta family and version before reconstruction (load-bearing, sincebeta_constant_productoverwrites them), enforcesa < b, andfinish_exactrefuses trailing bytes.encode(decode(P)) == Pfor every acceptedP, so the assertion would have no reachable rejection case — and an assertion no mutation can red is a finding about the test.Mutation controls
Each alone, grep-verified present, restored. M1–M6 ran against the full 1,787-test
dsmlib.funding_assets_that_are_not_the_markets_pair_are_refused,a_funding_pair_out_of_canonical_order_is_refusedpolicy_bytes_that_are_not_the_ones_the_state_names_are_refused,a_market_policy_preimage_that_is_not_canonical_is_refusedcreate_positionnot held to the landing positiona_creation_naming_a_position_it_does_not_land_at_is_refuseda_state_naming_other_owner_coordinates_than_its_preimage_is_refusedgenesis_rootis trusteda_creation_stating_a_genesis_root_the_state_does_not_derive_is_refusedaltering_the_carried_market_policy_after_signing_is_refused,a_creation_round_trips_both_funding_commits_in_order,every_sofi_operation_round_trips_through_the_byte_codecThe fixture change is the substantive one
sofi_genesis_stateandsofi_create_operationnow take the market pair separately from the funding pair. A fixture that conflated them could not express this defect — which is why seven existing tests passed straight over it, treating funding assets as free parameters.Added at the real seam:
funding_assets_outside_the_declared_market_are_refused_even_when_funded— funded from assets the device genuinely holds, canonically ordered, in the exact reserve amounts, declaring a market in two different assets. Every other binding holds. Still refused.Two corrections to my own earlier reports
Both from judging coverage by
cargo test --lib, which does not compiletests/*.rs:dsm/tests/economic_write_set.rshad a real creation fixture and seven tests. What was true and sharper: the fixture took funding assets as free parameters.dsm/tests/sofi_v8_operations.rshas two. I deleted the duplicate I had added tooperations.rsand extended the real one instead, which is why MS now reds three tests rather than two.The vector corpus is not the obligation here
Five copies of
tests/vectors/v1exist and the corpus contains zero operation-encoding vectors — all 8 cases are Envelope-v3Invoke/ReceiptCommit, never touchingOperation::to_bytes. Regeneration would produce byte-identical files, so it is not done. Two separate repository-hygiene defects recorded and deliberately not folded in: the generator writes four of the five copies andfrontend/public/vectors/v1has no writer at all; and no CI job ever invokesvector_builder, so corpus staleness cannot be detected.Gates
Run locally on the identical tree before the accidental push, and preserved here as evidence rather than as a substitute for this PR's CI: the board — the exact CI command,
cargo test --locked --workspace --exclude dsm_storage_node --release— 3,834 passed, 0 failed, exit 0;cargo fmt --all --check0;cargo clippy --all-targets -D warnings0;ci/production_safety_checks.sh0 including TLA+; G1 87 reachable / 0 baselined. Re-running against884cef68, and PR CI is the board that decides.make lintstops after the Rust half in this worktree (nonode_modules); zero frontend files are touched, so CI's Frontend job is the authority there.