diff --git a/dsm_client/deterministic_state_machine/dsm/src/ccb/mod.rs b/dsm_client/deterministic_state_machine/dsm/src/ccb/mod.rs index 5a7484af5..72c159024 100644 --- a/dsm_client/deterministic_state_machine/dsm/src/ccb/mod.rs +++ b/dsm_client/deterministic_state_machine/dsm/src/ccb/mod.rs @@ -233,6 +233,12 @@ pub mod class { /// root proves the creation, and a second creation of the same policy /// commit on that lineage cannot insert it again. pub const ECONOMIC_TOKEN_CREATION_STATE: u16 = 0x0060; + /// `0x0061` — a trader's balance of one token before a trade (SoFi + /// Amendment S12). `T°` states that balance only by the hash of its leaf, + /// so the exercise carries the value as this object, named in + /// `𝒞_E^pre` and accepted only because it hashes to the leaf the core + /// states. + pub const SOFI_TRADER_PRE_BALANCE: u16 = 0x0061; } /// Discriminants **allocated but not encodable** — see [`class`] for the ones diff --git a/dsm_client/deterministic_state_machine/dsm/src/common/domain_tags/dsm/misc/sofi.rs b/dsm_client/deterministic_state_machine/dsm/src/common/domain_tags/dsm/misc/sofi.rs index 2cc30e1bc..f2b44e635 100644 --- a/dsm_client/deterministic_state_machine/dsm/src/common/domain_tags/dsm/misc/sofi.rs +++ b/dsm_client/deterministic_state_machine/dsm/src/common/domain_tags/dsm/misc/sofi.rs @@ -144,6 +144,11 @@ pub const TAG_DSM_SOFI_POLICY_FULFILLMENT_OBJECT: TaggedHashDomain<'static> = /// The signed fulfillment envelope, indexed under `FulfillmentId`. pub const TAG_DSM_SOFI_FULFILLMENT_OBJECT: TaggedHashDomain<'static> = crate::tagged_domain!(b"DSM/sofi/fulfillment-object/v1"); +/// Immutable-store namespace of a `TraderPreBalance` (SoFi Amendment S12): +/// `addr = immutable_addr(tag, CCB bytes)`, the address `𝒞_E^pre` names it +/// by. It has no locator: the closure names the address. +pub const TAG_DSM_SOFI_TRADER_PRE_BALANCE_OBJECT: TaggedHashDomain<'static> = + crate::tagged_domain!(b"DSM/sofi/trader-pre-balance-object/v1"); // ── The DLV tree's leaves, and the route digest (P15-4, P15-8) ───────────── @@ -205,6 +210,7 @@ pub(crate) const SOFI_TAGS: &[TaggedHashDomain<'static>] = &[ TAG_DSM_SOFI_PREIMAGE_OBJECT, TAG_DSM_SOFI_POLICY_FULFILLMENT_OBJECT, TAG_DSM_SOFI_FULFILLMENT_OBJECT, + TAG_DSM_SOFI_TRADER_PRE_BALANCE_OBJECT, TAG_DSM_SOFI_VAULT_CREATION_KEY, TAG_DSM_SOFI_VAULT_STATE_KEY, TAG_DSM_SOFI_VAULT_LEAF_STATE, diff --git a/dsm_client/deterministic_state_machine/dsm/src/common/domain_tags/mod.rs b/dsm_client/deterministic_state_machine/dsm/src/common/domain_tags/mod.rs index 1a0995256..e786408f7 100644 --- a/dsm_client/deterministic_state_machine/dsm/src/common/domain_tags/mod.rs +++ b/dsm_client/deterministic_state_machine/dsm/src/common/domain_tags/mod.rs @@ -169,7 +169,8 @@ mod tests { // it outside the registry). // -1 with the client-edited storage node list: its placement seed was the // network hash domain's only input. - const EXPECTED_TAG_COUNT: usize = 350; + // +1 with the TraderPreBalance object namespace (SoFi Amendment S12). + const EXPECTED_TAG_COUNT: usize = 351; /// Scan the crate source for every declared domain-tag constant. /// diff --git a/dsm_client/deterministic_state_machine/dsm/src/economic/peer_lineage.rs b/dsm_client/deterministic_state_machine/dsm/src/economic/peer_lineage.rs index a53471bd2..e25ad56f4 100644 --- a/dsm_client/deterministic_state_machine/dsm/src/economic/peer_lineage.rs +++ b/dsm_client/deterministic_state_machine/dsm/src/economic/peer_lineage.rs @@ -38,7 +38,7 @@ use crate::economic::authority_evidence::{verify_authority_evidence, AuthorityEv use crate::economic::claim::AdmissionSubstrate; use crate::economic::decode::decode_admission_manifest; use crate::economic::lineage::{ - activate, advance_validated, AcceptedSubstrate, EconomicActivationSnapshot, + activate, advance_validated, AcceptedClaim, AcceptedSubstrate, EconomicActivationSnapshot, EconomicValidationError, ValidatedEconomicRoot, }; use crate::economic::provenance::{ @@ -389,6 +389,7 @@ fn walk_positions( [u8; 32], [u8; 32], [u8; 32], + AcceptedClaim, )> = None; for position in first_position..=target_position { if state.steps_remaining == 0 { @@ -560,13 +561,21 @@ fn walk_positions( verified.c_dsm_plus, verified.embedded_parent, manifest_addr, + advanced.claim, )); } - let (witness, proven_ak, verified_operation, c_dsm_plus, embedded_parent, manifest_addr) = last - .ok_or_else(|| { - incomplete("walk had no steps — the start memo already covers the target") - })?; + let ( + witness, + proven_ak, + verified_operation, + c_dsm_plus, + embedded_parent, + manifest_addr, + accepted_claim, + ) = last.ok_or_else(|| { + incomplete("walk had no steps — the start memo already covers the target") + })?; // SINGLE-ROOT BY CONSTRUCTION, not by label. Every position this walk // traversed decoded as a single-root claim: a conditional `C_q` is refused // above with `Unresolved`, resolved or not, because resolution is @@ -582,6 +591,7 @@ fn walk_positions( embedded_parent, verified_operation, manifest_addr, + accepted_claim, )) } diff --git a/dsm_client/deterministic_state_machine/dsm/src/economic/provenance.rs b/dsm_client/deterministic_state_machine/dsm/src/economic/provenance.rs index 8e2b923c4..2d0148777 100644 --- a/dsm_client/deterministic_state_machine/dsm/src/economic/provenance.rs +++ b/dsm_client/deterministic_state_machine/dsm/src/economic/provenance.rs @@ -47,7 +47,7 @@ use crate::common::domain_tags::{TAG_DSM_ECON_SOURCE_VALIDATED_PEER_DEBIT}; use crate::crypto::blake3::dsm_domain_hasher; use crate::economic::credit::CreditSource; -use crate::economic::lineage::ValidatedEconomicRoot; +use crate::economic::lineage::{AcceptedClaim, ValidatedEconomicRoot}; use crate::economic::mutation::EconomicLeafMutation; use crate::economic::state::EconomicLeafState; use crate::economic::witness::EconomicTransitionWitness; @@ -142,6 +142,9 @@ pub struct PeerTransitionFacts { /// as the walk verified it. A release that names another manifest for /// the same root is refused against this, not against a re-read cell. admission_manifest_addr: [u8; 32], + /// The claim `advance_validated` accepted at this position: what a + /// setup of this peer names by `claim_ref` (SoFi Amendment S9). + accepted_claim: AcceptedClaim, } impl ValidatedPeerTransition { @@ -164,6 +167,7 @@ impl ValidatedPeerTransition { embedded_parent: [u8; 32], verified_operation: crate::types::operations::Operation, admission_manifest_addr: [u8; 32], + accepted_claim: AcceptedClaim, ) -> Self { Self::SingleRoot(PeerTransitionFacts { peer_genesis, @@ -175,6 +179,7 @@ impl ValidatedPeerTransition { embedded_parent, verified_operation, admission_manifest_addr, + accepted_claim, }) } @@ -205,6 +210,12 @@ impl ValidatedPeerTransition { self.facts().admission_manifest_addr } + /// The claim the walk accepted at this position, as `advance_validated` + /// produced it. + pub fn accepted_claim(&self) -> &AcceptedClaim { + &self.facts().accepted_claim + } + pub fn witness(&self) -> &EconomicTransitionWitness { &self.facts().witness } diff --git a/dsm_client/deterministic_state_machine/dsm/src/sofi/conformance.rs b/dsm_client/deterministic_state_machine/dsm/src/sofi/conformance.rs index dd9d9b6e3..ebedaa508 100644 --- a/dsm_client/deterministic_state_machine/dsm/src/sofi/conformance.rs +++ b/dsm_client/deterministic_state_machine/dsm/src/sofi/conformance.rs @@ -31,7 +31,6 @@ use super::wire::{ ParentClaimRef, next_position, DlvPolicyFulfillmentBody, SettlementPreimage, SofiResolutionClaim, SofiWireError, TraderFulfillmentBody, TraderPrecommitBody, ValidationRef, }; -use crate::ccb::decode::policy_object_address; type D32 = [u8; 32]; @@ -501,7 +500,7 @@ impl Items { fn closure_object_verifies(reference: &ValidationRef, bytes: &[u8]) -> Option { Some(match reference { ValidationRef::ContentAddr { object_class, addr } => { - policy_object_address(*object_class, bytes)? == *addr + derive::closure_content_address(*object_class, bytes)? == *addr } ValidationRef::SingleRootClaim { claim_ref } => derive::claim_ref(bytes) == *claim_ref, ValidationRef::ConditionalClaim { @@ -971,10 +970,10 @@ mod tests { let mut refs: Vec = closure.keys().copied().collect(); refs.sort_by_key(ValidationRef::encode); let f = swap_fixture_with(2, PreEClosureIndex::new(refs).unwrap()); - // The fixture adds the single-root parent P names; the closure holds - // its exact envelope under that reference. + // The fixture adds the single-root parent P names and the trader's + // balances before the trade; the closure holds their exact bytes. let mut closure = closure; - closure.insert(f.precommit.parent_reference(), f.parent_claim.clone()); + closure.extend(f.closure_objects()); let precommit = f.precommit; let precommit_sig = sign_p(&precommit); let e = derive::recompute_e(&f.preimage).unwrap(); @@ -1251,7 +1250,7 @@ mod tests { refs.sort_by_key(ValidationRef::encode); let f = swap_fixture_with(1, PreEClosureIndex::new(refs).unwrap()); let mut closure = extra; - closure.insert(f.precommit.parent_reference(), f.parent_claim.clone()); + closure.extend(f.closure_objects()); (f, closure) } diff --git a/dsm_client/deterministic_state_machine/dsm/src/sofi/derive.rs b/dsm_client/deterministic_state_machine/dsm/src/sofi/derive.rs index 21c7267bb..abc9b2e1c 100644 --- a/dsm_client/deterministic_state_machine/dsm/src/sofi/derive.rs +++ b/dsm_client/deterministic_state_machine/dsm/src/sofi/derive.rs @@ -20,17 +20,19 @@ use crate::common::domain_tags::{ TAG_DSM_SOFI_STORAGE_SEED_V4, TAG_DSM_SOFI_SUCC_ATTEMPT, TAG_DSM_SOFI_SUCC_CELL_V2, TAG_DSM_SOFI_TRADER_CORE_V3, TAG_DSM_SOFI_TRADER_PRECOMMIT_ID, TAG_DSM_SOFI_ROUTE_DIGEST, TAG_DSM_SOFI_TRADER_PRECOMMIT_SIGN, TAG_DSM_SOFI_VAULT_GENESIS_LOCATOR, TAG_DSM_SOFI_VAULT_ID, - TAG_DSM_SOFI_VAULT_LEAF_STATE, TAG_DSM_SOFI_VAULT_STATE_KEY, + TAG_DSM_SOFI_TRADER_PRE_BALANCE_OBJECT, TAG_DSM_SOFI_VAULT_LEAF_STATE, + TAG_DSM_SOFI_VAULT_STATE_KEY, }; use crate::common::domain_tags::TAG_DSM_ECONOMIC_LEAF_STATE; +use crate::storage_object::immutable_addr; use crate::crypto::blake3::dsm_domain_hasher; use crate::crypto::domain::TaggedHashDomain; use super::wire::{ DlvPolicyFulfillmentBody, RouteDigestPreimage, RouteLegSet, SettlementBody, SettlementPreimage, RouteLegEntry, SofiResolutionClaim, SofiSetupBody, SofiWireError, TraderFulfillmentBody, - TraderPrecommitBody, TraderRelationshipLeaf, VaultRelationshipLeaf, VaultStateLeaf, - CANONICAL_MAX_LEGS, ROUTE_MIN_LEGS, + TraderPreBalance, TraderPrecommitBody, TraderRelationshipLeaf, VaultRelationshipLeaf, + VaultStateLeaf, CANONICAL_MAX_LEGS, ROUTE_MIN_LEGS, }; type D32 = [u8; 32]; @@ -84,6 +86,25 @@ pub fn trader_relationship_leaf_value(leaf: &TraderRelationshipLeaf) -> D32 { *hasher.finalize().as_bytes() } +/// The address `𝒞_E^pre` names a `TraderPreBalance` by (SoFi Amendment +/// S12): `immutable_addr(DSM/sofi/trader-pre-balance-object/v1, CCB bytes)`. +pub fn trader_pre_balance_addr(balance: &TraderPreBalance) -> D32 { + immutable_addr(TAG_DSM_SOFI_TRADER_PRE_BALANCE_OBJECT, &balance.encode()) +} + +/// The address a closure `ContentAddr` naming `object_class` gives `bytes`, +/// under that class's own namespace, so the address binds the kind. `None` +/// for a class with no content-addressing rule, which no bytes satisfy. +pub fn closure_content_address(object_class: u16, bytes: &[u8]) -> Option { + match object_class { + crate::ccb::class::SOFI_TRADER_PRE_BALANCE => Some(immutable_addr( + TAG_DSM_SOFI_TRADER_PRE_BALANCE_OBJECT, + bytes, + )), + other => crate::ccb::decode::policy_object_address(other, bytes), + } +} + /// `X_route = H(route-digest/v1 ‖ CCB(RouteDigestPreimage))`. pub fn route_digest(preimage: &RouteDigestPreimage) -> Result { Ok(h(TAG_DSM_SOFI_ROUTE_DIGEST, &[&preimage.encode()?])) diff --git a/dsm_client/deterministic_state_machine/dsm/src/sofi/exercise.rs b/dsm_client/deterministic_state_machine/dsm/src/sofi/exercise.rs index 93a0d0a0d..73bfef36a 100644 --- a/dsm_client/deterministic_state_machine/dsm/src/sofi/exercise.rs +++ b/dsm_client/deterministic_state_machine/dsm/src/sofi/exercise.rs @@ -28,7 +28,7 @@ use super::publication::{ }; use super::wire::{ DlvPolicyFulfillmentBody, SettlementPreimage, SofiExercise, TraderFulfillmentBody, - TraderPrecommitBody, + TraderPrecommitBody, ValidationRef, }; type D32 = [u8; 32]; @@ -47,6 +47,24 @@ pub struct RecognizedExercise { pub external_commitment: D32, } +impl RecognizedExercise { + /// The closure objects this exercise carries, each under the reference + /// in `𝒞_E^pre` it answers: the exercise carries them in reference order + /// (Section 17.5), and recognition requires one per reference. Nothing is + /// trusted because it is here: Core re-derives every reference from the + /// bytes it is handed. + pub fn closure_objects(&self) -> std::collections::BTreeMap> { + self.preimage + .settlement() + .closure() + .refs() + .iter() + .copied() + .zip(self.closure.iter().cloned()) + .collect() + } +} + /// Rebuild the objects an exercise carries and check their binding to one /// another. `None` for bytes that are not one exercise of one operation. pub fn recognize_exercise(bytes: &[u8]) -> Option { @@ -375,8 +393,9 @@ pub(crate) mod fixtures { .iter() .map(DlvPolicyFulfillmentBody::encode) .collect(), - // 𝒞_E^pre references the parent P names; the exercise carries it. - vec![f.parent_claim.clone()], + // 𝒞_E^pre references the parent P names and the trader's + // balances before the trade; the exercise carries them. + f.closure_in_order(), ) .unwrap(); Built { diff --git a/dsm_client/deterministic_state_machine/dsm/src/sofi/facts.rs b/dsm_client/deterministic_state_machine/dsm/src/sofi/facts.rs index 35220cf00..554e94395 100644 --- a/dsm_client/deterministic_state_machine/dsm/src/sofi/facts.rs +++ b/dsm_client/deterministic_state_machine/dsm/src/sofi/facts.rs @@ -103,9 +103,6 @@ pub enum NotEstablished { ConformanceEvidence(Vec), /// Route evidence not in hand after the retry budget. RouteEvidence(Vec), - /// Route evidence with no source this verifier can acquire it from: the - /// leaf pre values of another trader's route. - RouteEvidenceHasNoSource(Vec), /// `P` names a conditional parent this verifier has not resolved. ParentUnresolved { fulfillment_id: D32 }, /// A leg's attempt cell is not decided by its reads yet. diff --git a/dsm_client/deterministic_state_machine/dsm/src/sofi/publication.rs b/dsm_client/deterministic_state_machine/dsm/src/sofi/publication.rs index e8a33969d..b34f4176a 100644 --- a/dsm_client/deterministic_state_machine/dsm/src/sofi/publication.rs +++ b/dsm_client/deterministic_state_machine/dsm/src/sofi/publication.rs @@ -31,8 +31,8 @@ use crate::common::domain_tags::{ TAG_DSM_SOFI_FULFILLMENT_OBJECT, TAG_DSM_SOFI_POLICY_FULFILLMENT_OBJECT, TAG_DSM_SOFI_PRECOMMIT_OBJECT, TAG_DSM_SOFI_PREIMAGE_LOCATOR, TAG_DSM_SOFI_PREIMAGE_OBJECT, TAG_DSM_SOFI_REL_INDEX, TAG_DSM_SOFI_SETUP_OBJECT, TAG_DSM_SOFI_SETUP_REF, - TAG_DSM_SOFI_TRADER_PRECOMMIT_ID, TAG_DSM_SOFI_VAULT_GENESIS_LOCATOR, - TAG_DSM_SOFI_VAULT_GENESIS_OBJECT, + TAG_DSM_SOFI_TRADER_PRECOMMIT_ID, TAG_DSM_SOFI_TRADER_PRE_BALANCE_OBJECT, + TAG_DSM_SOFI_VAULT_GENESIS_LOCATOR, TAG_DSM_SOFI_VAULT_GENESIS_OBJECT, }; use crate::crypto::domain::TaggedHashDomain; use crate::storage_object::immutable_addr; @@ -41,7 +41,7 @@ use super::derive; use super::signature::{verify_fulfillment, verify_precommit, verify_setup}; use super::wire::{ DlvPolicyFulfillmentBody, SettlementPreimage, SignedSofiObject, SofiSetupBody, SofiWireError, - TraderFulfillmentBody, TraderPrecommitBody, VaultGenesisPreimage, + TraderFulfillmentBody, TraderPreBalance, TraderPrecommitBody, VaultGenesisPreimage, }; type D32 = [u8; 32]; @@ -112,6 +112,10 @@ pub enum Publication<'a> { class: VaultPolicyClass, bytes: &'a [u8], }, + /// A trader's balance of one token before a trade (Amendment S12), bare, + /// found by the address `𝒞_E^pre` names. The exercise carries it too; + /// publishing it lets a reader that holds only the closure fetch it. + TraderPreBalance(&'a TraderPreBalance), } fn envelope( @@ -150,6 +154,7 @@ impl Publication<'_> { ), Self::VaultGenesis(preimage) => preimage.encode(), Self::VaultPolicy { bytes, .. } => Ok(bytes.to_vec()), + Self::TraderPreBalance(balance) => Ok(balance.encode()), } } @@ -167,6 +172,7 @@ impl Publication<'_> { VaultPolicyClass::Fee => TAG_DSM_FEE_POLICY_OBJECT, VaultPolicyClass::Release => TAG_DSM_RELEASE_POLICY_OBJECT, }, + Self::TraderPreBalance(_) => TAG_DSM_SOFI_TRADER_PRE_BALANCE_OBJECT, } } @@ -216,7 +222,7 @@ impl Publication<'_> { index_namespace: TAG_DSM_SOFI_VAULT_GENESIS_LOCATOR.source_bytes(), locator: derive::vault_genesis_locator(&preimage.vault_id()), }], - Self::VaultPolicy { .. } => Vec::new(), + Self::VaultPolicy { .. } | Self::TraderPreBalance(_) => Vec::new(), }) } } diff --git a/dsm_client/deterministic_state_machine/dsm/src/sofi/resolve.rs b/dsm_client/deterministic_state_machine/dsm/src/sofi/resolve.rs index 622f642ba..e34274f29 100644 --- a/dsm_client/deterministic_state_machine/dsm/src/sofi/resolve.rs +++ b/dsm_client/deterministic_state_machine/dsm/src/sofi/resolve.rs @@ -57,12 +57,12 @@ use super::registration::{ use super::resolution::{walk, AttemptWalk, KeyFacts, RecordedGeneration, VaultChain, WalkOutcome}; use super::storage::{Discovered, Resolved}; use super::validation::{ - route_validation, vault_post_states, Evidence, EvidenceNeeds, Missing, TraderLeafPre, - VaultLeafPre, VaultPostState, + route_validation, setup_lineage, vault_post_states, Evidence, EvidenceNeeds, Missing, + SetupLineage, VaultLeafPre, VaultPostState, }; use super::wire::{ - ParentClaimRef, SettlementPreimage, TraderCore, TraderFulfillmentBody, TraderPrecommitBody, - ValidationRef, VaultGenesisPreimage, VaultStateLeaf, + CoreEntry, ParentClaimRef, SettlementPreimage, TraderCore, TraderFulfillmentBody, + TraderPreBalance, TraderPrecommitBody, ValidationRef, VaultGenesisPreimage, VaultStateLeaf, }; type D32 = [u8; 32]; @@ -202,14 +202,18 @@ pub trait SofiReads { root: &D32, keys: &BTreeSet, ) -> Result, ReadFailure>; - /// The claim this device's own lineage accepted at `position` of trader - /// `(genesis, device_id)`, if it accepted one there. + /// The claim lineage validation accepted at `position` of trader + /// `(genesis, device_id)`: from this device's own admitted store when the + /// trader is this device, and from the peer lineage walk otherwise — + /// never this device's claim under another trader's name. A failure + /// keeps the class lineage validation gave it; Core reads which classes + /// are verdicts (`validation::setup_lineage`, SoFi Amendment S13). fn accepted_claim_at( &self, genesis: &D32, device_id: &D32, position: u64, - ) -> Result, ReadFailure>; + ) -> Result; /// The generations this device recorded for `vault_id`, contiguous from /// zero, in generation order. fn recorded_generations( @@ -286,32 +290,50 @@ impl LocalLeaves { self.root } - /// Evidence holding the pre value of every key `core` names, from these - /// leaves alone: what `Fold(T°, E)` reads, before anything is published. - /// A key holding a write-once record has no trader-leaf pre value and is - /// refused. - pub fn trader_evidence( + /// The `TraderPreBalance` of every balance `core` states as present + /// before the operation (SoFi Amendment S12): the values the exercise + /// carries, so that a verifier that is not this device can judge it. A + /// key the core writes as a balance that holds any other leaf is refused. + pub fn pre_balances( &self, core: &TraderCore, - ) -> Result { - let mut trader_leaves = BTreeMap::new(); + ) -> Result, LeavesDoNotRecomputeTheRoot> { + let mut out = Vec::new(); for entry in core.entries() { - let key = entry.key(); - let pre = self.pre(&key).ok_or_else(|| { - LeavesDoNotRecomputeTheRoot( - "a trader core names a key holding a write-once record".to_string(), - ) - })?; - trader_leaves.insert(key, pre); + match entry { + CoreEntry::Relationship { .. } => {} + CoreEntry::Mutation { .. } | CoreEntry::Read { .. } => { + match self.leaves.get(&entry.key()) { + None => {} + Some(EconomicLeafState::Balance(b)) => out.push( + TraderPreBalance::new( + self.genesis, + self.device_id, + b.policy_commit, + b.amount, + ) + .map_err(|e| { + LeavesDoNotRecomputeTheRoot(format!( + "a balance before the trade: {e}" + )) + })?, + ), + Some( + EconomicLeafState::Relationship(..) + | EconomicLeafState::ConsumedSource(..) + | EconomicLeafState::VaultCreation(..) + | EconomicLeafState::TokenCreation(..), + ) => { + return Err(LeavesDoNotRecomputeTheRoot( + "a trader core writes a balance at a key holding another leaf" + .to_string(), + )) + } + } + } + } } - Ok(Evidence::acquired( - BTreeMap::new(), - trader_leaves, - BTreeMap::new(), - BTreeMap::new(), - BTreeMap::new(), - BTreeMap::new(), - )) + Ok(out) } /// Every relationship leaf this device holds: the vaults it is set up @@ -334,30 +356,6 @@ impl LocalLeaves { Some(..) | None => None, } } - - /// Whether `precommit` is this device's own: the only routes whose trader - /// leaves and accepted claims this device holds. - fn owns(&self, precommit: &TraderPrecommitBody) -> bool { - *precommit.genesis() == self.genesis && *precommit.device_id() == self.device_id - } - - /// The pre value at `key` as Core reads a trader leaf. The tree is whole, - /// so a key it does not hold is absent. A key holding a write-once record - /// has no trader-leaf pre value: Core reads trader leaves only at balance - /// and relationship keys, which are domain-separated from every record - /// key. - pub fn pre(&self, key: &D32) -> Option { - match self.leaves.get(key) { - None => Some(TraderLeafPre::Absent), - Some(EconomicLeafState::Balance(b)) => Some(TraderLeafPre::Balance(b.clone())), - Some(EconomicLeafState::Relationship(r)) => Some(TraderLeafPre::Relationship(*r)), - Some( - EconomicLeafState::ConsumedSource(..) - | EconomicLeafState::VaultCreation(..) - | EconomicLeafState::TokenCreation(..), - ) => None, - } - } } /// Leaves that do not form the root they were said to form. @@ -400,10 +398,6 @@ pub enum Acquired { /// operation fails on the network. The caller evaluates nothing and /// records nothing. Exhausted(Vec), - /// These items have no source this verifier can acquire them from, so no - /// retry can supply them. The caller evaluates nothing and records - /// nothing. - NoSource(Vec), } /// The objects one exercise's conformance is decided over: the trader's @@ -423,7 +417,7 @@ pub struct ExerciseObjects<'a> { } /// What the verifier brings: its reads, the network's pinned set, the -/// network, its own leaves, and the position it resolved itself. Every +/// network, and the position it resolved itself. Every /// field is an established fact of THIS verifier; none is trusted because /// somebody sent it. pub struct Verifier<'a, R: SofiReads> { @@ -434,11 +428,6 @@ pub struct Verifier<'a, R: SofiReads> { pub members: &'a StorageSetMembers, pub set_id: D32, pub network_id: &'a [u8], - /// This device's own `R_econ` leaves, for the trader-leaf pre values of - /// its own routes; `None` for a verifier that is not a trader (a relay, - /// a reader of another trader's position), whose routes then have no - /// source for those values. - pub local: Option<&'a LocalLeaves>, /// This verifier's own admitted position, when it resolved a conditional /// one: what a `P` naming that fulfillment as its parent was built on. /// Core reads what it selected; nothing else resolves a parent, and a @@ -727,7 +716,11 @@ impl Verifier<'_, R> { let mut closure = BTreeMap::new(); for reference in objects.preimage.settlement().closure().refs() { let bytes = match reference { - ValidationRef::ContentAddr { addr, .. } => self.reads.stored_bytes(addr)?, + ValidationRef::ContentAddr { addr, .. } => match objects.own_objects.get(reference) + { + Some(bytes) => Some(bytes.clone()), + None => self.reads.stored_bytes(addr)?, + }, ValidationRef::Setup { setup_ref } => match self.reads.setup_bytes(setup_ref)? { Resolved::Kept(bytes) => Some(bytes), Resolved::None | Resolved::Unavailable => None, @@ -798,32 +791,24 @@ impl Verifier<'_, R> { Ok(Acquired::Exhausted(missing)) } - /// Acquire everything `P` and `P(E)` need, from storage and this device's - /// own state, and ask the predicate whether it is complete. `Complete` - /// once `route_validation` reaches a verdict over it; `Exhausted` naming - /// what is still missing after [`ACQUIRE_ROUNDS`] rounds; `NoSource` for - /// another trader's route: `TraderSideValid` reads the trader's leaf pre - /// values, the trader core carries only their hashes, and no section - /// names where a verifier that is not the trader gets them (SoFi §17.5, - /// an open hole) — nothing is supplied in their place. + /// Acquire everything `P` and `P(E)` need, from storage and the objects + /// the operation carries, and ask the predicate whether it is complete. + /// `Complete` once `route_validation` reaches a verdict over it; + /// `Exhausted` naming what is still missing after [`ACQUIRE_ROUNDS`] + /// rounds. Any verifier judges any trader's route the same way: the + /// trader's balances before the trade come from the `TraderPreBalance` + /// objects `𝒞_E^pre` names (SoFi Amendment S12), and `carried` are the + /// closure objects the exercise, or the producer's own draft, holds. pub fn acquire_evidence( &self, precommit: &TraderPrecommitBody, preimage: &SettlementPreimage, + carried: &BTreeMap>, ) -> Result, VerifierFailure> { let needs = EvidenceNeeds::of(precommit, preimage); - if !self.local.is_some_and(|local| local.owns(precommit)) { - return Ok(Acquired::NoSource( - needs - .trader_keys - .iter() - .map(|key| Missing::TraderLeaf { key: *key }) - .collect(), - )); - } let mut missing = Vec::new(); for round in 1..=ACQUIRE_ROUNDS { - let evidence = self.gather(precommit, preimage, &needs)?; + let evidence = self.gather(precommit, preimage, &needs, carried)?; match route_validation(precommit, preimage, &evidence) { Ok(Validation::Valid | Validation::Invalid) => { return Ok(Acquired::Complete(evidence)) @@ -839,31 +824,37 @@ impl Verifier<'_, R> { Ok(Acquired::Exhausted(missing)) } - /// One round of reading every item `needs` names, for this device's own - /// route: trader leaf pre values from its own leaves, vault leaf pre - /// values from the vault's accepted genesis at `R_0` or the generation - /// this device established at the root the core names, policy objects - /// from the immutable store under the addresses the vault state commits, - /// token policies rooted by this device, setups at each `ρ`, and the - /// claims this device's lineage accepted at each setup's position. + /// One round of reading every item `needs` names: the trader's + /// `TraderPreBalance` objects from what the operation carries or the + /// immutable store, vault leaf pre values from the vault's accepted + /// genesis at `R_0` or the generation this device established at the + /// root the core names, policy objects from the immutable store under the + /// addresses the vault state commits, token policies rooted by this + /// device, setups at each `ρ`, and the claims the trader's lineage + /// accepted at each setup's position. fn gather( &self, precommit: &TraderPrecommitBody, preimage: &SettlementPreimage, needs: &EvidenceNeeds, + carried: &BTreeMap>, ) -> Result { - let trader_leaves: BTreeMap = needs - .trader_keys - .iter() - .filter_map(|key| { - self.local - .and_then(|local| local.pre(key)) - .map(|pre| (*key, pre)) - }) - .collect(); + let mut objects: BTreeMap> = BTreeMap::new(); + for addr in &needs.trader_pre_balances { + let reference = ValidationRef::ContentAddr { + object_class: crate::ccb::class::SOFI_TRADER_PRE_BALANCE, + addr: *addr, + }; + let bytes = match carried.get(&reference) { + Some(bytes) => Some(bytes.clone()), + None => self.reads.stored_bytes(addr)?, + }; + if let Some(bytes) = bytes { + objects.insert(*addr, bytes); + } + } let mut vault_leaves: VaultLeaves = BTreeMap::new(); - let mut objects: BTreeMap> = BTreeMap::new(); let mut token_policies: BTreeMap> = BTreeMap::new(); for (vault_id, keys) in &needs.vaults { let Some(state) = self.vault_pre(preimage, vault_id, keys, &mut vault_leaves)? else { @@ -902,19 +893,25 @@ impl Verifier<'_, R> { } let mut setups: BTreeMap> = BTreeMap::new(); - let mut accepted_claims: BTreeMap = BTreeMap::new(); + let mut setup_lineages: BTreeMap = BTreeMap::new(); for setup_ref in &needs.setups { let Resolved::Kept(bytes) = self.reads.setup_bytes(setup_ref)? else { continue; }; if let Some((.., signed)) = recognize_setup(&bytes) { let position = signed.body.position(); - if let Some(claim) = self.reads.accepted_claim_at( + let validated = self.reads.accepted_claim_at( precommit.genesis(), precommit.device_id(), position, - )? { - accepted_claims.insert(position, claim); + ); + if let Some(lineage) = setup_lineage( + *precommit.genesis(), + *precommit.device_id(), + position, + validated, + ) { + setup_lineages.insert(position, lineage); } } setups.insert(*setup_ref, bytes); @@ -922,11 +919,10 @@ impl Verifier<'_, R> { Ok(Evidence::acquired( objects, - trader_leaves, vault_leaves, setups, token_policies, - accepted_claims, + setup_lineages, )) } @@ -1201,9 +1197,13 @@ impl Verifier<'_, R> { exercise: &RecognizedExercise, ) -> Result, VerifierFailure> { let precommit = &exercise.precommit.body; - let evidence = match self.acquire_evidence(precommit, &exercise.preimage)? { + let evidence = match self.acquire_evidence( + precommit, + &exercise.preimage, + &exercise.closure_objects(), + )? { Acquired::Complete(evidence) => evidence, - Acquired::Exhausted(missing) | Acquired::NoSource(missing) => { + Acquired::Exhausted(missing) => { log::info!("[sofi chain] a consumption's evidence is not in hand: {missing:?}"); return Ok(None); } @@ -1461,15 +1461,7 @@ impl Verifier<'_, R> { // What FulfillmentConformance reads (R7), the exercise supplying the // objects only its trader held. - let own: BTreeMap> = exercise - .preimage - .settlement() - .closure() - .refs() - .iter() - .copied() - .zip(exercise.closure.iter().cloned()) - .collect(); + let own = exercise.closure_objects(); let objects = ExerciseObjects { precommit, precommit_signature: &exercise.precommit.signature, @@ -1480,18 +1472,15 @@ impl Verifier<'_, R> { }; let conformance = match self.acquire_conformance_evidence(&objects)? { Acquired::Complete(evidence) => evidence, - Acquired::Exhausted(missing) | Acquired::NoSource(missing) => { + Acquired::Exhausted(missing) => { return Ok(Err(NotEstablished::ConformanceEvidence(missing))) } }; // What RouteValidation reads (R5). - let evidence = match self.acquire_evidence(precommit, &exercise.preimage)? { + let evidence = match self.acquire_evidence(precommit, &exercise.preimage, &own)? { Acquired::Complete(evidence) => evidence, Acquired::Exhausted(missing) => return Ok(Err(NotEstablished::RouteEvidence(missing))), - Acquired::NoSource(missing) => { - return Ok(Err(NotEstablished::RouteEvidenceHasNoSource(missing))) - } }; // Every leg of P at the attempt F fixed for it: its cell, and the diff --git a/dsm_client/deterministic_state_machine/dsm/src/sofi/validation.rs b/dsm_client/deterministic_state_machine/dsm/src/sofi/validation.rs index 33527797a..92e037763 100644 --- a/dsm_client/deterministic_state_machine/dsm/src/sofi/validation.rs +++ b/dsm_client/deterministic_state_machine/dsm/src/sofi/validation.rs @@ -40,8 +40,9 @@ use super::derive; use super::smt::{verify_batch, FoldEntry, FoldError}; use super::wire::{ next_position, CoreEntry, DlvCore, OwnerAuthority, SettlementBody, SettlementPreimage, SwapHop, - TraderCore, TraderPrecommitBody, TraderRelationshipLeaf, VaultRelationshipLeaf, VaultStateLeaf, - MAX_SETTLEMENT_PREIMAGE_BYTES, VAULT_STATUS_ACTIVE, VAULT_STATUS_RETIRED, + TraderCore, TraderPreBalance, TraderPrecommitBody, TraderRelationshipLeaf, ValidationRef, + VaultRelationshipLeaf, VaultStateLeaf, MAX_SETTLEMENT_PREIMAGE_BYTES, VAULT_STATUS_ACTIVE, + VAULT_STATUS_RETIRED, }; type D32 = [u8; 32]; @@ -127,11 +128,25 @@ pub enum Invalid { /// A leg's setup names a claim at its position other than the one the /// trader's lineage accepted there (SoFi §16, Amendment S9). SetupClaimRefIsNotTheAcceptedClaim, + /// Lineage validation established the trader's lineage Invalid at or + /// before the setup's position, so no claim was accepted there and the + /// setup cannot be valid (SoFi Amendment S13). + SetupLineageIsInvalid, /// A token's committed policy does not parse. TokenPolicyDoesNotParse { token: D32 }, /// A token's committed policy forbids transfer, so it cannot be a market /// leg — the vault's, or any hop's through it (SoFi §19.5, §49). TokenNotTransferable { token: D32 }, + /// `𝒞_E^pre` does not name exactly one `TraderPreBalance` for each + /// balance `T°` states as present before the trade: one is missing, one + /// matches no such entry, or two name one entry (SoFi Amendment S12). + /// Known from the committed bytes, so never Unavailable. + TraderPreBalanceSetNotExact, + /// Bytes that re-derive the address `𝒞_E^pre` names are not a + /// `TraderPreBalance`: `E` committed an object that has no reading. + TraderPreBalanceDoesNotDecode(crate::ccb::decode::DecodeError), + /// A `TraderPreBalance` names another trader than `P`'s. + TraderPreBalanceNotThisTrader, } /// The conjunction, as an accumulator: any Invalid dominates, and only in its @@ -195,8 +210,9 @@ impl Verdict { pub enum Missing { /// A policy object named by a vault state, by content address. Policy { addr: D32 }, - /// The pre-state of an `R_econ` leaf a core writes. - TraderLeaf { key: D32 }, + /// A `TraderPreBalance` `𝒞_E^pre` names, by its content address (SoFi + /// Amendment S12). + TraderPreBalance { addr: D32 }, /// The pre-state of a vault leaf a core writes. VaultLeaf { vault_id: D32, key: D32 }, /// The vault's own state leaf, which every branch needs. @@ -204,7 +220,8 @@ pub enum Missing { /// The signed setup envelope stored at `ρ` for one of P's legs. Setup { setup_ref: D32 }, /// The claim this verifier accepted at a position of P's trader, which a - /// setup names by `claim_ref` (SoFi Amendment S9). + /// setup names by `claim_ref`, or the lineage verdict that none can be + /// (SoFi Amendments S9, S13). AcceptedClaim { economic_position: u64 }, /// Bytes were supplied for an address but do not authenticate to it. They /// establish NOTHING — note 9: a non-verifying candidate can never prove @@ -235,12 +252,12 @@ impl Refusal { } } -/// What a trader's `R_econ` leaf held before the operation. +/// What a trader's balance leaf held before the operation, as the exercise +/// carries it (SoFi Amendment S12). #[derive(Debug, Clone, PartialEq, Eq)] pub enum TraderLeafPre { Balance(EconomicBalanceState), - Relationship(TraderRelationshipLeaf), - /// The leaf held nothing — a first credit, or a first relationship. + /// The leaf held nothing: a first credit of that token. Absent, } @@ -259,17 +276,18 @@ pub enum VaultLeafPre { /// /// Gate G2: `Default` exists only under `cfg(test)`. Production code builds /// this ONLY from fetched bytes, through [`Evidence::acquired`] at the end of -/// an acquisition (rebuild step R5): trader leaf pre values from the -/// verifier's own validated tree, vault leaf pre values from the vault +/// an acquisition (rebuild step R5): vault leaf pre values from the vault /// lineage it fetched, policy objects from the immutable store under the -/// address the vault state commits. Nothing is defaulted or filled in. +/// address the vault state commits, and the trader's `TraderPreBalance` +/// objects under the addresses `𝒞_E^pre` names. The trader's leaf pre values +/// are not supplied at all: `validate` derives them from `T°` and those +/// objects (SoFi Amendment S12). Nothing is defaulted or filled in. #[derive(Debug, Clone)] #[cfg_attr(test, derive(Default))] pub struct Evidence { - /// Canonical bytes by content address — the policy objects a vault names. + /// Canonical bytes by content address: the policy objects a vault names, + /// and the `TraderPreBalance` objects `𝒞_E^pre` names. pub objects: BTreeMap>, - /// Pre-states of the trader's own leaves, by `R_econ` key. - pub trader_leaves: BTreeMap, /// Pre-states of vault leaves, by `(vault_id, key)`. pub vault_leaves: BTreeMap<(D32, D32), VaultLeafPre>, /// The signed setup envelope stored at `ρ`, for every leg of P: what @@ -280,10 +298,11 @@ pub struct Evidence { /// (SoFi §19.5, §49). Re-hashed to the commit under `TAG_DSM_POLICY` when /// consumed: bytes supplied under a commit prove nothing by themselves. pub token_policies: BTreeMap>, - /// The claims this verifier accepted on P's trader's lineage, by - /// position: what each setup's `claim_ref` is checked against (SoFi - /// Amendment S9). Only lineage validation produces an [`AcceptedClaim`]. - pub accepted_claims: BTreeMap, + /// What lineage validation established about P's trader at each setup's + /// position: the claim it accepted there, which the setup's `claim_ref` + /// is checked against, or the verdict that the lineage is invalid (SoFi + /// Amendments S9, S13). Only lineage validation produces either. + pub setup_lineages: BTreeMap, } /// What a settlement preimage needs fetched before `validate` can reach a @@ -297,8 +316,8 @@ pub struct Evidence { /// less (an item not fetched is `Unavailable`). #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct EvidenceNeeds { - /// `R_econ` keys of the trader's own leaves. - pub trader_keys: BTreeSet, + /// The addresses of the `TraderPreBalance` objects `𝒞_E^pre` names. + pub trader_pre_balances: BTreeSet, /// Per vault: the keys of its leaves, the state key included. pub vaults: BTreeMap>, /// The `ρ` of every leg of P: each leg's setup envelope, for `SetupValid`. @@ -308,12 +327,7 @@ pub struct EvidenceNeeds { impl EvidenceNeeds { pub fn of(precommit: &TraderPrecommitBody, preimage: &SettlementPreimage) -> Self { let setups = precommit.legs().iter().map(|leg| leg.setup_ref).collect(); - let trader_keys = preimage - .trader_core() - .entries() - .iter() - .map(CoreEntry::key) - .collect(); + let trader_pre_balances = named_pre_balances(preimage).into_iter().collect(); let mut vaults: BTreeMap> = BTreeMap::new(); for core in preimage.dlv_cores() { let keys = vaults.entry(*core.vault_id()).or_default(); @@ -327,7 +341,7 @@ impl EvidenceNeeds { .insert(derive::vault_state_key(vault_id)); } Self { - trader_keys, + trader_pre_balances, vaults, setups, } @@ -349,6 +363,69 @@ impl EvidenceNeeds { } } +/// What lineage validation established about a trader at one setup's +/// position (SoFi Amendments S9, S13). Not established is not one of these: +/// the verifier holds nothing for the position and waits. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SetupLineage { + /// The claim lineage validation accepted at the position. + Accepted(AcceptedClaim), + /// Lineage validation established the lineage Invalid at or before the + /// position. + Invalid(InvalidLineage), +} + +/// The verdict that a trader's lineage is invalid at or before a position, +/// as lineage validation returned it. No accepted claim is synthesized in +/// its place: the negative fact is the verdict itself (SoFi Amendment S13). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct InvalidLineage { + genesis: D32, + device_id: D32, + position: u64, + reason: String, +} + +impl InvalidLineage { + /// Why lineage validation refused the lineage, as it reported it. + pub fn reason(&self) -> &str { + &self.reason + } +} + +/// What lineage validation of trader `(genesis, device_id)` up to `position` +/// establishes for a setup there (SoFi Amendment S13), or `None` while +/// nothing is established: +/// - an accepted claim is the claim the setup is checked against; +/// - `Invalid`, evidence that verified as wrong, and `Quarantined`, a +/// divergent write-once register cell, are the verdict that the lineage is +/// invalid at or before `position`; +/// - `Incomplete` (evidence not in hand) and `Unresolved` (a SoFi position +/// the walk cannot yet pass) establish nothing, so the setup waits. +pub fn setup_lineage( + genesis: D32, + device_id: D32, + position: u64, + validated: Result, +) -> Option { + use crate::economic::provenance::PeerLineageFailure as F; + match validated { + Ok(claim) => Some(SetupLineage::Accepted(claim)), + Err(F::Invalid(reason) | F::Quarantined(reason)) => { + Some(SetupLineage::Invalid(InvalidLineage { + genesis, + device_id, + position, + reason, + })) + } + Err(failure @ (F::Incomplete(..) | F::Unresolved(..))) => { + log::info!("[sofi verifier] no lineage verdict at {position} yet: {failure}"); + None + } + } +} + impl Evidence { /// The one production constructor: what an acquisition fetched. Every /// item is checked again when consumed — an object against its address, @@ -356,19 +433,17 @@ impl Evidence { /// nothing by itself. pub fn acquired( objects: BTreeMap>, - trader_leaves: BTreeMap, vault_leaves: BTreeMap<(D32, D32), VaultLeafPre>, setups: BTreeMap>, token_policies: BTreeMap>, - accepted_claims: BTreeMap, + setup_lineages: BTreeMap, ) -> Self { Self { objects, - trader_leaves, vault_leaves, setups, token_policies, - accepted_claims, + setup_lineages, } } @@ -397,10 +472,26 @@ impl Evidence { Ok(bytes) } - fn trader_leaf(&self, key: &D32) -> Result<&TraderLeafPre, Refusal> { - self.trader_leaves - .get(key) - .ok_or(Refusal::Incomplete(Missing::TraderLeaf { key: *key })) + /// The `TraderPreBalance` at `addr`, AUTHENTICATED against it. Bytes not + /// in hand, or that do not re-derive `addr`, supply nothing (note 9). + /// Bytes that do re-derive it are the object `E` committed, so one that + /// does not decode is known to have no reading: Invalid. + fn pre_balance(&self, addr: &D32) -> Result { + let bytes = + self.objects + .get(addr) + .ok_or(Refusal::Incomplete(Missing::TraderPreBalance { + addr: *addr, + }))?; + if derive::closure_content_address(crate::ccb::class::SOFI_TRADER_PRE_BALANCE, bytes) + != Some(*addr) + { + return Err(Refusal::Incomplete(Missing::NonVerifyingObject { + addr: *addr, + })); + } + TraderPreBalance::decode(bytes) + .map_err(|why| Refusal::Invalid(Invalid::TraderPreBalanceDoesNotDecode(why))) } fn vault_leaf(&self, vault_id: &D32, key: &D32) -> Result<&VaultLeafPre, Refusal> { @@ -514,10 +605,21 @@ fn setup_valid( let missing_claim = Refusal::Incomplete(Missing::AcceptedClaim { economic_position: body.position(), }); - let accepted = evidence - .accepted_claims - .get(&body.position()) - .ok_or(missing_claim.clone())?; + let accepted = match evidence.setup_lineages.get(&body.position()) { + None => return Err(missing_claim), + Some(SetupLineage::Invalid(verdict)) => { + // A verdict about another trader, or another position, says + // nothing about this setup. + if verdict.genesis != *body.genesis() + || verdict.device_id != *body.device_id() + || verdict.position != body.position() + { + return Err(missing_claim); + } + return Err(Refusal::Invalid(Invalid::SetupLineageIsInvalid)); + } + Some(SetupLineage::Accepted(accepted)) => accepted, + }; if accepted.genesis() != *body.genesis() || accepted.device_id() != *body.device_id() || accepted.economic_position() != body.position() @@ -597,7 +699,6 @@ pub fn route_invalid_in_hand( BTreeMap::new(), BTreeMap::new(), BTreeMap::new(), - BTreeMap::new(), ); match validate(precommit, preimage, ¬hing) { Err(Refusal::Invalid(why)) => Some(why), @@ -845,6 +946,105 @@ pub struct TraderBalanceChange { /// a balance leaf — the change the settlement made to it. type TraderPost = (D32, Option, Option); +/// The addresses of the `TraderPreBalance` objects `𝒞_E^pre` names, in +/// reference order (SoFi Amendment S12). +pub fn named_pre_balances(preimage: &SettlementPreimage) -> Vec { + let mut out = Vec::new(); + for reference in preimage.settlement().closure().refs() { + match reference { + ValidationRef::ContentAddr { object_class, addr } + if *object_class == crate::ccb::class::SOFI_TRADER_PRE_BALANCE => + { + out.push(*addr) + } + ValidationRef::ContentAddr { .. } + | ValidationRef::SingleRootClaim { .. } + | ValidationRef::ConditionalClaim { .. } + | ValidationRef::Setup { .. } => {} + } + } + out +} + +/// The trader's balances before the operation, for every balance entry of +/// `T°` (SoFi Amendment S12), read from the `TraderPreBalance` objects +/// `𝒞_E^pre` names and from nothing else — for every verifier, the trader +/// included. `T°` states each balance only by the hash of its leaf, so the +/// value arrives as an object. It counts only because it hashes to the leaf +/// the core states: [`check_trader_balances`] compares each with the value +/// `T°` states before the trade, and the post arithmetic binds it again. +/// +/// - An entry that states its balance as absent is `Absent`, with no object. +/// - Every other balance entry has exactly one object, naming `P`'s trader +/// and keyed to the entry by `balance_key`. +/// - A count that differs, an object naming another trader, and an object +/// keyed to no such entry are Invalid: `E` commits all of it, so none of it +/// waits on the network. A missing number is wrong, never undecided. +/// - An object whose bytes are not in hand, or do not re-derive its address, +/// is missing (note 9): it proves nothing either way. +fn trader_pre_balances( + precommit: &TraderPrecommitBody, + preimage: &SettlementPreimage, + evidence: &Evidence, +) -> Result, Refusal> { + let mut stated_pre: BTreeMap> = BTreeMap::new(); + for entry in preimage.trader_core().entries() { + match entry { + CoreEntry::Mutation { .. } | CoreEntry::Read { .. } => { + stated_pre.insert(entry.key(), stated(entry).0); + } + CoreEntry::Relationship { .. } => {} + } + } + let present = stated_pre.values().filter(|value| value.is_some()).count(); + let named = named_pre_balances(preimage); + require(named.len() == present, Invalid::TraderPreBalanceSetNotExact)?; + + let mut verdict = Verdict::default(); + let mut held: BTreeMap = BTreeMap::new(); + for addr in &named { + let Some(balance) = verdict.get(evidence.pre_balance(addr)) else { + continue; + }; + if balance.trader_genesis() != precommit.genesis() + || balance.trader_device_id() != precommit.device_id() + { + verdict.note(Err(Refusal::Invalid( + Invalid::TraderPreBalanceNotThisTrader, + ))); + continue; + } + held.insert( + balance_key( + precommit.genesis(), + precommit.device_id(), + balance.policy_commit(), + ), + EconomicBalanceState { + policy_commit: *balance.policy_commit(), + amount: balance.amount(), + }, + ); + } + verdict.finish()?; + + // Exactly one object for each balance the core states as present, and + // none beside them. With the count above, an object keyed to no such + // entry, or two keyed to one, leaves a stated balance without its number. + let mut out = BTreeMap::new(); + for (key, value) in stated_pre { + let pre = match (value, held.remove(&key)) { + (None, None) => TraderLeafPre::Absent, + (Some(_), Some(state)) => TraderLeafPre::Balance(state), + (None, Some(_)) | (Some(_), None) => { + return Err(Refusal::Invalid(Invalid::TraderPreBalanceSetNotExact)) + } + }; + out.insert(key, pre); + } + Ok(out) +} + /// The one derivation behind [`trader_post_states`] and /// [`trader_balance_changes`]: each post state recomputed from the pre state /// the evidence holds and the movement the settlement commits, then bound to @@ -856,6 +1056,7 @@ fn trader_posts( ) -> Result, Refusal> { let e = *precommit.external_commitment(); let movements = trader_movements(preimage, evidence)?; + let pre = trader_pre_balances(precommit, preimage, evidence)?; let core = preimage.trader_core(); let mut out = Vec::with_capacity(core.entries().len()); for entry in core.entries() { @@ -882,10 +1083,10 @@ fn trader_posts( balance_key(precommit.genesis(), precommit.device_id(), t) == key }) .ok_or(Refusal::Invalid(Invalid::WriteSetNotExact { core: "T°" }))?; - let before = match evidence.trader_leaf(&key)? { - TraderLeafPre::Balance(b) if b.policy_commit == *token => b.amount, - TraderLeafPre::Absent => 0, - TraderLeafPre::Balance(_) | TraderLeafPre::Relationship(_) => { + let before = match pre.get(&key) { + Some(TraderLeafPre::Balance(b)) if b.policy_commit == *token => b.amount, + Some(TraderLeafPre::Absent) => 0, + Some(TraderLeafPre::Balance(_)) | None => { return Err(Refusal::Invalid(Invalid::LeafPreValueMismatch)) } }; @@ -1200,9 +1401,6 @@ fn balance_after( b.amount } TraderLeafPre::Absent => 0, - TraderLeafPre::Relationship(_) => { - return Err(Refusal::Invalid(Invalid::LeafPreValueMismatch)) - } }; let after = before .checked_add(credit) @@ -1213,7 +1411,6 @@ fn balance_after( let pre_value = match pre { TraderLeafPre::Absent => None, TraderLeafPre::Balance(b) => Some(leaf_value_of_balance(b)?), - TraderLeafPre::Relationship(_) => unreachable!("refused above"), }; let post_value = if after == 0 { // A zero balance is the leaf's absence, never a leaf holding zero. @@ -1284,10 +1481,14 @@ fn relationship_bases(entries: &[CoreEntry]) -> BTreeMap { /// Turn a trader core's entries into fold entries, resolving each relationship /// post through BindExt. This is the only place `E` enters a leaf value. +/// +/// Everything comes from the core itself. A balance entry states its pre and +/// post values; a relationship entry's leaf before the operation is the one +/// its `base` names (SoFi Amendment S12), and the fold against `T°.pre_root` +/// is what proves the trader held it. fn trader_fold_entries( core: &TraderCore, external_commitment: &D32, - evidence: &Evidence, ) -> Result, Refusal> { let mut out = Vec::with_capacity(core.entries().len()); for entry in core.entries() { @@ -1295,21 +1496,12 @@ fn trader_fold_entries( let (pre, post) = match entry { CoreEntry::Mutation { .. } | CoreEntry::Read { .. } => stated(entry), CoreEntry::Relationship { vault_id, base, .. } => { - let held = evidence.trader_leaf(&key)?; - let pre = match held { - TraderLeafPre::Absent => None, - TraderLeafPre::Relationship(r) => { - // The base a core states must be the leaf it holds. - require( - r.leaf == *base && r.vault_id == *vault_id, - Invalid::RelationshipBaseMismatch, - )?; - Some(derive::trader_relationship_leaf_value(r)) - } - TraderLeafPre::Balance(_) => { - return Err(Refusal::Invalid(Invalid::LeafPreValueMismatch)) - } - }; + let pre = Some(derive::trader_relationship_leaf_value( + &TraderRelationshipLeaf { + vault_id: *vault_id, + leaf: *base, + }, + )); let next = derive::relationship_leaf_next(base, external_commitment); let post = derive::trader_relationship_leaf_value(&TraderRelationshipLeaf { vault_id: *vault_id, @@ -1423,15 +1615,11 @@ fn fold_core(entries: &[FoldEntry], pre_root: &D32) -> Result { } /// `Fold(T°, E)`: the root a trader core's entries fold to under `E`, each -/// relationship advanced by `relationship_leaf_next` against the leaf the -/// trader holds — what `P.R_realize` must be. The producer computes it with -/// this function and the verifier checks it with the same one. -pub fn realize_root( - core: &TraderCore, - external_commitment: &D32, - evidence: &Evidence, -) -> Result { - let entries = trader_fold_entries(core, external_commitment, evidence)?; +/// relationship advanced by `relationship_leaf_next` from the base the core +/// states — what `P.R_realize` must be. The producer computes it with this +/// function and the verifier checks it with the same one. +pub fn realize_root(core: &TraderCore, external_commitment: &D32) -> Result { + let entries = trader_fold_entries(core, external_commitment)?; fold_core(&entries, core.pre_root()) } @@ -1717,25 +1905,27 @@ fn validate_swap( }), ); } - verdict.note(check_trader_balances( - precommit, - trader_core, - evidence, - &[ - (intent.token_out, intent.exact_out, 0), - (intent.token_in, 0, intent.amount_in), - ], - hops.len(), - )); - verdict.note( - realize_root(trader_core, &e, evidence).and_then(|post_root| { - require( - post_root == *precommit.realize_root(), - Invalid::RealizeRootIsNotTheFold, + trader_pre_balances(precommit, preimage, evidence).and_then(|pre| { + check_trader_balances( + precommit, + trader_core, + &pre, + &[ + (intent.token_out, intent.exact_out, 0), + (intent.token_in, 0, intent.amount_in), + ], + hops.len(), ) }), ); + + verdict.note(realize_root(trader_core, &e).and_then(|post_root| { + require( + post_root == *precommit.realize_root(), + Invalid::RealizeRootIsNotTheFold, + ) + })); } /// The trader core holds exactly the named balance movements and one @@ -1743,7 +1933,7 @@ fn validate_swap( fn check_trader_balances( precommit: &TraderPrecommitBody, core: &TraderCore, - evidence: &Evidence, + pre: &BTreeMap, movements: &[(D32, u64, u64)], relationships: usize, ) -> Result<(), Refusal> { @@ -1758,8 +1948,13 @@ fn check_trader_balances( .iter() .find(|e| e.key() == key) .ok_or(Refusal::Invalid(Invalid::WriteSetNotExact { core: "T°" }))?; - let (want_pre, want_post) = - balance_after(evidence.trader_leaf(&key)?, token, *credit, *debit)?; + let (want_pre, want_post) = balance_after( + pre.get(&key) + .ok_or(Refusal::Invalid(Invalid::WriteSetNotExact { core: "T°" }))?, + token, + *credit, + *debit, + )?; let (got_pre, got_post) = stated(entry); require(got_pre == want_pre, Invalid::LeafPreValueMismatch)?; require(got_post == want_post, Invalid::LeafPostValueMismatch)?; @@ -1849,16 +2044,20 @@ fn validate_close( .as_ref() .and_then(|state| verdict.get(Policies::resolve(evidence, state))); if let Some(policies) = policies.as_ref() { - verdict.note(check_trader_balances( - precommit, - trader_core, - evidence, - &[ - (*policies.market.token_a(), reserve_a, 0), - (*policies.market.token_b(), reserve_b, 0), - ], - 1, - )); + verdict.note( + trader_pre_balances(precommit, preimage, evidence).and_then(|pre| { + check_trader_balances( + precommit, + trader_core, + &pre, + &[ + (*policies.market.token_a(), reserve_a, 0), + (*policies.market.token_b(), reserve_b, 0), + ], + 1, + ) + }), + ); } let bases = relationship_bases(trader_core.entries()); match bases.get(vault_id) { @@ -1879,14 +2078,12 @@ fn validate_close( } } - verdict.note( - realize_root(trader_core, &e, evidence).and_then(|post_root| { - require( - post_root == *precommit.realize_root(), - Invalid::RealizeRootIsNotTheFold, - ) - }), - ); + verdict.note(realize_root(trader_core, &e).and_then(|post_root| { + require( + post_root == *precommit.realize_root(), + Invalid::RealizeRootIsNotTheFold, + ) + })); } #[cfg(test)] @@ -2174,6 +2371,39 @@ pub(crate) mod fixtures { pub(crate) parent_claim: Vec, } + impl Fixture { + /// The closure objects the fixture's trader holds, each under the + /// reference `𝒞_E^pre` names it by: the parent claim `P` names, and + /// every `TraderPreBalance` (SoFi Amendment S12). + pub(crate) fn closure_objects(&self) -> BTreeMap> { + let mut out = + BTreeMap::from([(self.precommit.parent_reference(), self.parent_claim.clone())]); + for addr in named_pre_balances(&self.preimage) { + out.insert( + ValidationRef::ContentAddr { + object_class: crate::ccb::class::SOFI_TRADER_PRE_BALANCE, + addr, + }, + self.evidence.objects[&addr].clone(), + ); + } + out + } + + /// Those objects in `𝒞_E^pre` reference order, as an exercise + /// carries them. + pub(crate) fn closure_in_order(&self) -> Vec> { + let objects = self.closure_objects(); + self.preimage + .settlement() + .closure() + .refs() + .iter() + .map(|reference| objects[reference].clone()) + .collect() + } + } + /// The trader's registered claim at [`P_POS`] over `root`, signed under /// the trader's key: the parent every fixture operation extends. pub(crate) fn parent_claim_envelope(root: D32) -> Vec { @@ -2201,6 +2431,24 @@ pub(crate) mod fixtures { .expect("a closure with room for the parent") } + /// The reference `𝒞_E^pre` names `balance` by (SoFi Amendment S12). + pub(crate) fn pre_balance_ref(balance: &TraderPreBalance) -> ValidationRef { + ValidationRef::ContentAddr { + object_class: crate::ccb::class::SOFI_TRADER_PRE_BALANCE, + addr: derive::trader_pre_balance_addr(balance), + } + } + + /// `closure` with the reference of `balance`, in canonical order. + pub(crate) fn with_pre_balance( + closure: PreEClosureIndex, + balance: &TraderPreBalance, + ) -> PreEClosureIndex { + closure + .with(pre_balance_ref(balance)) + .expect("a closure with room for the balance") + } + /// One vault's worth of a swap: its tree, its core, and the hop it prices. pub(crate) struct VaultParts { pub(crate) vault_id: D32, @@ -2329,18 +2577,15 @@ pub(crate) mod fixtures { let out_key = balance_key(&G, &DEV, &intent_out); let mut trader_tree = EconomicSmt::new(); trader_tree.insert(in_key, balance_leaf_value(intent_in, 50_000)); - let trader_rels: Vec = parts - .iter() - .enumerate() - .map(|(j, part)| { - let leaf = TraderRelationshipLeaf { - vault_id: part.vault_id, - leaf: base_of(j), - }; - trader_tree.insert(part.rel_key, derive::trader_relationship_leaf_value(&leaf)); - leaf - }) - .collect(); + for (j, part) in parts.iter().enumerate() { + let leaf = TraderRelationshipLeaf { + vault_id: part.vault_id, + leaf: base_of(j), + }; + trader_tree.insert(part.rel_key, derive::trader_relationship_leaf_value(&leaf)); + } + // The balance spent, as the exercise carries it (SoFi Amendment S12). + let pre_balance = TraderPreBalance::new(G, DEV, intent_in, 50_000).unwrap(); let mut trader_entries = vec![ CoreEntry::Mutation { @@ -2369,7 +2614,7 @@ pub(crate) mod fixtures { let trader_core = TraderCore::new(G, DEV, P_POS + 1, trader_tree.root(), trader_entries).unwrap(); let parent_claim = parent_claim_envelope(trader_tree.root()); - let closure = with_parent(closure, &parent_claim); + let closure = with_pre_balance(with_parent(closure, &parent_claim), &pre_balance); let mut cores: Vec = parts.iter().map(|p| p.core.clone()).collect(); cores.sort_by_key(|c| *c.vault_id()); @@ -2392,13 +2637,8 @@ pub(crate) mod fixtures { let preimage = SettlementPreimage::new(settlement, trader_core.clone(), cores).unwrap(); let e = derive::recompute_e(&preimage).unwrap(); - let mut trader_leaves = BTreeMap::from([ - (in_key, TraderLeafPre::Balance(balance(intent_in, 50_000))), - (out_key, TraderLeafPre::Absent), - ]); let mut vault_leaves = BTreeMap::new(); - for (part, rel) in parts.iter().zip(trader_rels) { - trader_leaves.insert(part.rel_key, TraderLeafPre::Relationship(rel)); + for part in &parts { vault_leaves.insert( (part.vault_id, part.state_key), VaultLeafPre::State(part.state.clone()), @@ -2408,9 +2648,13 @@ pub(crate) mod fixtures { VaultLeafPre::Relationship(part.relationship), ); } + let mut objects = policy_objects_over(&pairs); + objects.insert( + derive::trader_pre_balance_addr(&pre_balance), + pre_balance.encode(), + ); let evidence = Evidence { - objects: policy_objects_over(&pairs), - trader_leaves, + objects, vault_leaves, setups: (0..hops) .map(|j| (parts[j].hop.setup_ref, setup_envelope_of(&setup(j)))) @@ -2422,12 +2666,15 @@ pub(crate) mod fixtures { }) .cloned() .collect(), - accepted_claims: BTreeMap::from([(SETUP_POS, accepted_setup_claim())]), + setup_lineages: BTreeMap::from([( + SETUP_POS, + SetupLineage::Accepted(accepted_setup_claim()), + )]), }; // The realize root is what the core folds to UNDER E, so it cannot be // chosen: BindExt fills the relationship posts and the fold does the rest. let realize_root = { - let entries = trader_fold_entries(&trader_core, &e, &evidence).unwrap(); + let entries = trader_fold_entries(&trader_core, &e).unwrap(); batch_fold(&entries).unwrap().post_root }; let legs: Vec = { @@ -2504,11 +2751,10 @@ mod tests { let (market, _, _) = policies(0); let mut without_policy = Evidence { objects: f.evidence.objects.clone(), - trader_leaves: f.evidence.trader_leaves.clone(), vault_leaves: f.evidence.vault_leaves.clone(), setups: f.evidence.setups.clone(), token_policies: f.evidence.token_policies.clone(), - accepted_claims: f.evidence.accepted_claims.clone(), + setup_lineages: f.evidence.setup_lineages.clone(), }; without_policy.objects.remove(&policy_addr( crate::ccb::class::MARKET_POLICY, @@ -2525,16 +2771,17 @@ mod tests { let mut without_leaf = Evidence { objects: f.evidence.objects.clone(), - trader_leaves: f.evidence.trader_leaves.clone(), vault_leaves: BTreeMap::new(), setups: f.evidence.setups.clone(), token_policies: f.evidence.token_policies.clone(), - accepted_claims: f.evidence.accepted_claims.clone(), + setup_lineages: f.evidence.setup_lineages.clone(), }; - without_leaf.trader_leaves.clear(); + for addr in named_pre_balances(&f.preimage) { + without_leaf.objects.remove(&addr); + } assert!(matches!( route_validation(&f.precommit, &f.preimage, &without_leaf), - Err(Missing::TraderLeaf { .. } | Missing::VaultLeaf { .. }) + Err(Missing::TraderPreBalance { .. } | Missing::VaultLeaf { .. }) )); } @@ -3049,11 +3296,10 @@ mod tests { let state_key = derive::vault_state_key(&vault_id); let mut evidence = Evidence { objects: f.evidence.objects.clone(), - trader_leaves: f.evidence.trader_leaves.clone(), vault_leaves: f.evidence.vault_leaves.clone(), setups: f.evidence.setups.clone(), token_policies: f.evidence.token_policies.clone(), - accepted_claims: f.evidence.accepted_claims.clone(), + setup_lineages: f.evidence.setup_lineages.clone(), }; let VaultLeafPre::State(state) = evidence.vault_leaves[&(vault_id, state_key)].clone() else { @@ -3080,11 +3326,10 @@ mod tests { let state_key = derive::vault_state_key(&vault_id); let mut evidence = Evidence { objects: f.evidence.objects.clone(), - trader_leaves: f.evidence.trader_leaves.clone(), vault_leaves: f.evidence.vault_leaves.clone(), setups: f.evidence.setups.clone(), token_policies: f.evidence.token_policies.clone(), - accepted_claims: f.evidence.accepted_claims.clone(), + setup_lineages: f.evidence.setup_lineages.clone(), }; let VaultLeafPre::State(state) = evidence.vault_leaves[&(vault_id, state_key)].clone() else { @@ -3238,11 +3483,6 @@ mod tests { } let evidence = Evidence { objects, - trader_leaves: BTreeMap::from([ - (a_key, TraderLeafPre::Absent), - (b_key, TraderLeafPre::Absent), - (rel_key, TraderLeafPre::Relationship(trader_rel)), - ]), vault_leaves: BTreeMap::from([ ((vault_id, state_key), VaultLeafPre::State(state)), ( @@ -3252,10 +3492,13 @@ mod tests { ]), setups: BTreeMap::from([(setup_ref_for(vault_id), setup_envelope_for(vault_id))]), token_policies: tokens()[..=1].iter().cloned().collect(), - accepted_claims: BTreeMap::from([(SETUP_POS, accepted_setup_claim())]), + setup_lineages: BTreeMap::from([( + SETUP_POS, + SetupLineage::Accepted(accepted_setup_claim()), + )]), }; let realize_root = { - let entries = trader_fold_entries(&trader_core, &e, &evidence).unwrap(); + let entries = trader_fold_entries(&trader_core, &e).unwrap(); batch_fold(&entries).unwrap().post_root }; let precommit = TraderPrecommitBody::new( @@ -3326,11 +3569,10 @@ mod tests { let state_key = derive::vault_state_key(&vault_id); let mut evidence = Evidence { objects: f.evidence.objects.clone(), - trader_leaves: f.evidence.trader_leaves.clone(), vault_leaves: f.evidence.vault_leaves.clone(), setups: f.evidence.setups.clone(), token_policies: f.evidence.token_policies.clone(), - accepted_claims: f.evidence.accepted_claims.clone(), + setup_lineages: f.evidence.setup_lineages.clone(), }; let VaultLeafPre::State(state) = evidence.vault_leaves[&(vault_id, state_key)].clone() else { @@ -3604,15 +3846,20 @@ mod tests { assert_eq!(post.len(), f.preimage.trader_core().entries().len()); let in_key = balance_key(f.precommit.genesis(), f.precommit.device_id(), &token_in); let out_key = balance_key(f.precommit.genesis(), f.precommit.device_id(), &token_out); - let TraderLeafPre::Balance(before) = f.evidence.trader_leaf(&in_key).unwrap() else { - panic!("the fixture holds the token spent") - }; + let before = + TraderPreBalance::decode(&f.evidence.objects[&named_pre_balances(&f.preimage)[0]]) + .unwrap(); + assert_eq!( + *before.policy_commit(), + token_in, + "the fixture holds the token spent" + ); let state_at = |key: &D32| post.iter().find(|(k, _)| k == key).unwrap().1.clone(); assert_eq!( state_at(&in_key), Some(EconomicLeafState::Balance(EconomicBalanceState { policy_commit: token_in, - amount: before.amount - amount_in, + amount: before.amount() - amount_in, })) ); assert_eq!( @@ -3757,11 +4004,10 @@ mod tests { let state_key = derive::vault_state_key(&vault_id); let mut evidence = Evidence { objects: f.evidence.objects.clone(), - trader_leaves: f.evidence.trader_leaves.clone(), vault_leaves: f.evidence.vault_leaves.clone(), setups: f.evidence.setups.clone(), token_policies: f.evidence.token_policies.clone(), - accepted_claims: f.evidence.accepted_claims.clone(), + setup_lineages: f.evidence.setup_lineages.clone(), }; let VaultLeafPre::State(state) = evidence.vault_leaves[&(vault_id, state_key)].clone() else { @@ -3790,11 +4036,10 @@ mod tests { let state_key = derive::vault_state_key(&vault_id); let mut evidence = Evidence { objects: f.evidence.objects.clone(), - trader_leaves: f.evidence.trader_leaves.clone(), vault_leaves: f.evidence.vault_leaves.clone(), setups: f.evidence.setups.clone(), token_policies: f.evidence.token_policies.clone(), - accepted_claims: f.evidence.accepted_claims.clone(), + setup_lineages: f.evidence.setup_lineages.clone(), }; let VaultLeafPre::State(state) = evidence.vault_leaves[&(vault_id, state_key)].clone() else { @@ -3839,11 +4084,10 @@ mod tests { let state_key = derive::vault_state_key(&vault_id); let mut evidence = Evidence { objects: f.evidence.objects.clone(), - trader_leaves: f.evidence.trader_leaves.clone(), vault_leaves: f.evidence.vault_leaves.clone(), setups: f.evidence.setups.clone(), token_policies: f.evidence.token_policies.clone(), - accepted_claims: f.evidence.accepted_claims.clone(), + setup_lineages: f.evidence.setup_lineages.clone(), }; let VaultLeafPre::State(state) = evidence.vault_leaves[&(vault_id, state_key)].clone() else { @@ -3876,11 +4120,10 @@ mod tests { let (market, _, _) = policies(0); let mut evidence = Evidence { objects: f.evidence.objects.clone(), - trader_leaves: f.evidence.trader_leaves.clone(), vault_leaves: f.evidence.vault_leaves.clone(), setups: f.evidence.setups.clone(), token_policies: f.evidence.token_policies.clone(), - accepted_claims: f.evidence.accepted_claims.clone(), + setup_lineages: f.evidence.setup_lineages.clone(), }; let VaultLeafPre::State(state) = evidence.vault_leaves[&(vault_id, state_key)].clone() else { @@ -3919,11 +4162,10 @@ mod tests { let (first, second) = (hops[0].vault_id, hops[1].vault_id); let mut evidence = Evidence { objects: f.evidence.objects.clone(), - trader_leaves: f.evidence.trader_leaves.clone(), vault_leaves: f.evidence.vault_leaves.clone(), setups: f.evidence.setups.clone(), token_policies: f.evidence.token_policies.clone(), - accepted_claims: f.evidence.accepted_claims.clone(), + setup_lineages: f.evidence.setup_lineages.clone(), }; // Hop 0's state is simply not held. evidence @@ -4084,11 +4326,10 @@ mod tests { let addr = policy_addr(crate::ccb::class::MARKET_POLICY, &market.encode()); let mut evidence = Evidence { objects: f.evidence.objects.clone(), - trader_leaves: f.evidence.trader_leaves.clone(), vault_leaves: f.evidence.vault_leaves.clone(), setups: f.evidence.setups.clone(), token_policies: f.evidence.token_policies.clone(), - accepted_claims: f.evidence.accepted_claims.clone(), + setup_lineages: f.evidence.setup_lineages.clone(), }; // Garbage, under the address of a policy that really exists. evidence @@ -4203,11 +4444,10 @@ mod tests { let rel_key = derive::relationship_key(&G, &DEV, &vault_id); let mut evidence = Evidence { objects: f.evidence.objects.clone(), - trader_leaves: f.evidence.trader_leaves.clone(), vault_leaves: f.evidence.vault_leaves.clone(), setups: f.evidence.setups.clone(), token_policies: f.evidence.token_policies.clone(), - accepted_claims: f.evidence.accepted_claims.clone(), + setup_lineages: f.evidence.setup_lineages.clone(), }; let VaultLeafPre::Relationship(leaf) = evidence.vault_leaves[&(vault_id, rel_key)].clone() else { @@ -4398,7 +4638,7 @@ mod tests { economic_position: SETUP_POS, })); let mut evidence = f.evidence.clone(); - evidence.accepted_claims.clear(); + evidence.setup_lineages.clear(); assert_eq!(validate(&f.precommit, &f.preimage, &evidence), missing); let foreign = crate::economic::lineage::AcceptedClaim::rehydrate_from_admitted_store( token(0x33), @@ -4410,10 +4650,86 @@ mod tests { }, ) .unwrap(); - evidence.accepted_claims.insert(SETUP_POS, foreign); + evidence + .setup_lineages + .insert(SETUP_POS, SetupLineage::Accepted(foreign)); assert_eq!(validate(&f.precommit, &f.preimage, &evidence), missing); } + /// SoFi Amendment S13: lineage validation that establishes the trader's + /// lineage Invalid — evidence that verified as wrong, or a divergent + /// register cell — makes the setup Invalid, and so the route. No accepted + /// claim is synthesized: the verdict itself is the negative fact. + #[test] + fn a_setup_on_a_lineage_known_invalid_is_invalid() { + use crate::economic::provenance::PeerLineageFailure as F; + let f = swap_fixture(); + for verdict in [ + F::Invalid("a step's witness does not fold".to_string()), + F::Quarantined("two claims hold the register cell".to_string()), + ] { + let mut evidence = f.evidence.clone(); + evidence.setup_lineages.clear(); + let lineage = setup_lineage(G, DEV, SETUP_POS, Err(verdict)).unwrap(); + evidence.setup_lineages.insert(SETUP_POS, lineage); + assert_eq!( + validate(&f.precommit, &f.preimage, &evidence), + Err(Refusal::Invalid(Invalid::SetupLineageIsInvalid)) + ); + assert_eq!( + route_validation(&f.precommit, &f.preimage, &evidence), + Ok(Validation::Invalid) + ); + } + } + + /// A lineage whose verdict is not established yet — evidence not in hand, + /// or a SoFi position the walk cannot pass yet — yields nothing, and the + /// setup waits: not known is never read as known invalid. + #[test] + fn a_lineage_not_established_leaves_the_setup_waiting() { + use crate::economic::provenance::PeerLineageFailure as F; + let f = swap_fixture(); + for pending in [ + F::Incomplete("the register cell is not decided yet".to_string()), + F::Unresolved("a conditional position has not resolved".to_string()), + ] { + assert_eq!(setup_lineage(G, DEV, SETUP_POS, Err(pending)), None); + } + let mut evidence = f.evidence.clone(); + evidence.setup_lineages.clear(); + assert_eq!( + validate(&f.precommit, &f.preimage, &evidence), + Err(Refusal::Incomplete(Missing::AcceptedClaim { + economic_position: SETUP_POS, + })) + ); + } + + /// A verdict about another trader, or about another position, says + /// nothing about this setup: it neither poisons it nor stands in for its + /// accepted claim. + #[test] + fn an_invalid_verdict_about_another_lineage_or_position_supplies_nothing() { + use crate::economic::provenance::PeerLineageFailure as F; + let f = swap_fixture(); + let missing = Err(Refusal::Incomplete(Missing::AcceptedClaim { + economic_position: SETUP_POS, + })); + for (genesis, position) in [(token(0x33), SETUP_POS), (G, SETUP_POS + 1)] { + let mut evidence = f.evidence.clone(); + let lineage = setup_lineage( + genesis, + DEV, + position, + Err(F::Invalid("another lineage".to_string())), + ) + .unwrap(); + evidence.setup_lineages.insert(SETUP_POS, lineage); + assert_eq!(validate(&f.precommit, &f.preimage, &evidence), missing); + } + } + // ── The transferable check on every leg (SoFi §49, MR-SOFI-0311) ──────── #[test] @@ -4526,4 +4842,299 @@ mod tests { assert!(!f.evidence.token_policies.contains_key(&era)); assert_eq!(validate(&f.precommit, &f.preimage, &f.evidence), Ok(())); } + + // ── SoFi Amendment S12: the trader's balances before the trade ────────── + + /// The balance the fixture's trader spends, as its exercise carries it. + fn carried_pre_balance(f: &Fixture) -> TraderPreBalance { + let named = named_pre_balances(&f.preimage); + assert_eq!(named.len(), 1, "the swap fixture spends one balance"); + TraderPreBalance::decode(&f.evidence.objects[&named[0]]).unwrap() + } + + /// `f`'s operation with `𝒞_E^pre` naming the parent `P` names and each + /// of `objects` as a `TraderPreBalance`, re-pointed so `E` and + /// `P.R_realize` follow the new closure: the only thing under test is + /// Amendment S12. The evidence holds exactly `objects` under their + /// addresses, beside everything else the fixture holds. + fn with_pre_balance_bytes( + f: &Fixture, + objects: &[Vec], + ) -> (TraderPrecommitBody, SettlementPreimage, Evidence) { + let mut closure = with_parent(PreEClosureIndex::new(Vec::new()).unwrap(), &f.parent_claim); + let mut evidence = f.evidence.clone(); + for addr in named_pre_balances(&f.preimage) { + evidence.objects.remove(&addr); + } + for bytes in objects { + let addr = + derive::closure_content_address(crate::ccb::class::SOFI_TRADER_PRE_BALANCE, bytes) + .unwrap(); + closure = closure + .with(ValidationRef::ContentAddr { + object_class: crate::ccb::class::SOFI_TRADER_PRE_BALANCE, + addr, + }) + .unwrap(); + evidence.objects.insert(addr, bytes.clone()); + } + let settlement = match f.preimage.settlement().clone() { + SettlementBody::Swap { + token_in, + amount_in, + token_out, + exact_out, + hops, + trader_core, + dlv_cores, + .. + } => SettlementBody::Swap { + token_in, + amount_in, + token_out, + exact_out, + hops, + trader_core, + dlv_cores, + closure, + }, + SettlementBody::Close { .. } => panic!("a swap fixture"), + }; + let preimage = SettlementPreimage::new( + settlement, + f.preimage.trader_core().clone(), + f.preimage.dlv_cores().to_vec(), + ) + .unwrap(); + let e = derive::recompute_e(&preimage).unwrap(); + let precommit = TraderPrecommitBody::new( + *f.precommit.genesis(), + *f.precommit.device_id(), + f.precommit.position(), + *f.precommit.parent_claim_ref(), + e, + f.precommit.legs().to_vec(), + realize_root(preimage.trader_core(), &e).unwrap(), + *f.precommit.void_root(), + *f.precommit.storage_set_id(), + f.precommit.signature_alg(), + f.precommit.claimant_public_key(), + ) + .unwrap(); + (precommit, preimage, evidence) + } + + fn with_pre_balances( + f: &Fixture, + balances: &[TraderPreBalance], + ) -> (TraderPrecommitBody, SettlementPreimage, Evidence) { + let objects: Vec> = balances.iter().map(TraderPreBalance::encode).collect(); + with_pre_balance_bytes(f, &objects) + } + + /// The helper itself changes nothing a verifier judges: the honest + /// balance, re-carried, is still Valid. Every refusal below is the + /// amendment's, not the rebuild's. + #[test] + fn a_rebuilt_closure_carrying_the_honest_balance_is_valid() { + let f = swap_fixture(); + let (precommit, preimage, evidence) = with_pre_balances(&f, &[carried_pre_balance(&f)]); + assert_eq!(validate(&precommit, &preimage, &evidence), Ok(())); + } + + /// A balance `T°` states as present before the trade must have its number + /// in the closure. A trade without it is Invalid, decided from its own + /// bytes: nothing is fetched to know it, and it is never left undecided, + /// which would let a trader freeze every vault it trades through. + #[test] + fn a_balance_the_closure_does_not_carry_is_invalid() { + let f = swap_fixture(); + let (precommit, preimage, evidence) = with_pre_balances(&f, &[]); + assert_eq!( + validate(&precommit, &preimage, &evidence), + Err(Refusal::Invalid(Invalid::TraderPreBalanceSetNotExact)) + ); + assert_eq!( + route_invalid_in_hand(&precommit, &preimage), + Some(Invalid::TraderPreBalanceSetNotExact) + ); + } + + /// A trader that states a larger balance than the leaf its core commits + /// is refused: the number counts only because it hashes to that leaf. + #[test] + fn a_balance_that_is_not_the_leaf_the_core_states_is_invalid() { + let f = swap_fixture(); + let honest = carried_pre_balance(&f); + let inflated = TraderPreBalance::new( + *honest.trader_genesis(), + *honest.trader_device_id(), + *honest.policy_commit(), + honest.amount() + 1_000_000, + ) + .unwrap(); + let (precommit, preimage, evidence) = with_pre_balances(&f, &[inflated]); + assert_eq!( + validate(&precommit, &preimage, &evidence), + Err(Refusal::Invalid(Invalid::LeafPreValueMismatch)) + ); + } + + /// A balance naming another trader is not this trader's number. + #[test] + fn a_balance_of_another_trader_is_invalid() { + let f = swap_fixture(); + let honest = carried_pre_balance(&f); + let theirs = TraderPreBalance::new( + token(0x55), + *honest.trader_device_id(), + *honest.policy_commit(), + honest.amount(), + ) + .unwrap(); + let (precommit, preimage, evidence) = with_pre_balances(&f, &[theirs]); + assert_eq!( + validate(&precommit, &preimage, &evidence), + Err(Refusal::Invalid(Invalid::TraderPreBalanceNotThisTrader)) + ); + } + + /// A number keyed to no balance the core states as present — the token + /// the trader receives, which it did not hold, or a token the trade never + /// touches — matches nothing, and the balance the core does state is then + /// left without its number. + #[test] + fn a_balance_keyed_to_no_stated_balance_is_invalid() { + let f = swap_fixture(); + let honest = carried_pre_balance(&f); + let SettlementBody::Swap { token_out, .. } = f.preimage.settlement() else { + panic!("a swap fixture") + }; + for token in [*token_out, token(0x57)] { + let stray = TraderPreBalance::new( + *honest.trader_genesis(), + *honest.trader_device_id(), + token, + 7, + ) + .unwrap(); + let (precommit, preimage, evidence) = with_pre_balances(&f, &[stray]); + assert_eq!( + validate(&precommit, &preimage, &evidence), + Err(Refusal::Invalid(Invalid::TraderPreBalanceSetNotExact)) + ); + } + } + + /// Two numbers for one balance, or one beside a balance the core does not + /// state, are refused: the closure carries exactly one per stated balance. + #[test] + fn extra_balances_in_the_closure_are_invalid() { + let f = swap_fixture(); + let honest = carried_pre_balance(&f); + let second = TraderPreBalance::new( + *honest.trader_genesis(), + *honest.trader_device_id(), + *honest.policy_commit(), + honest.amount() + 1, + ) + .unwrap(); + let (precommit, preimage, evidence) = with_pre_balances(&f, &[honest, second]); + assert_eq!( + validate(&precommit, &preimage, &evidence), + Err(Refusal::Invalid(Invalid::TraderPreBalanceSetNotExact)) + ); + assert_eq!( + route_invalid_in_hand(&precommit, &preimage), + Some(Invalid::TraderPreBalanceSetNotExact) + ); + } + + /// Bytes at an address the closure names that are not a + /// `TraderPreBalance` are an object `E` committed with no reading. + #[test] + fn bytes_at_a_named_address_that_are_not_a_balance_are_invalid() { + let f = swap_fixture(); + let not_a_balance = f.parent_claim.clone(); + let (precommit, preimage, evidence) = with_pre_balance_bytes(&f, &[not_a_balance]); + assert!(matches!( + validate(&precommit, &preimage, &evidence), + Err(Refusal::Invalid(Invalid::TraderPreBalanceDoesNotDecode(..))) + )); + } + + /// A number the closure names but the verifier does not hold yet leaves + /// the predicate unevaluated: the verifier waits and fetches, and the + /// absence is never read as a verdict. + #[test] + fn a_named_balance_not_in_hand_waits() { + let f = swap_fixture(); + let mut evidence = f.evidence.clone(); + let named = named_pre_balances(&f.preimage); + for addr in &named { + evidence.objects.remove(addr); + } + assert_eq!( + validate(&f.precommit, &f.preimage, &evidence), + Err(Refusal::Incomplete(Missing::TraderPreBalance { + addr: named[0] + })) + ); + } + + /// Bytes supplied under the named address that do not re-derive it prove + /// nothing either way (note 9) — even when they are a well-formed + /// balance of this trader. + #[test] + fn bytes_that_do_not_re_derive_the_named_address_prove_nothing() { + let f = swap_fixture(); + let honest = carried_pre_balance(&f); + let addr = named_pre_balances(&f.preimage)[0]; + let other = TraderPreBalance::new( + *honest.trader_genesis(), + *honest.trader_device_id(), + *honest.policy_commit(), + honest.amount() + 1, + ) + .unwrap(); + let mut evidence = f.evidence.clone(); + evidence.objects.insert(addr, other.encode()); + assert_eq!( + validate(&f.precommit, &f.preimage, &evidence), + Err(Refusal::Incomplete(Missing::NonVerifyingObject { addr })) + ); + } + + /// The attack the amendment closes, judged by a verifier that holds + /// nothing of the trader's: a trader that pays into the vault without + /// debiting its own balance — its core states the spent balance's post + /// as the pre — is refused from the exercise's own bytes. + #[test] + fn a_trade_that_does_not_debit_the_trader_is_invalid_from_the_exercise_alone() { + let f = swap_fixture(); + let honest = carried_pre_balance(&f); + let spent_key = balance_key(&G, &DEV, honest.policy_commit()); + let entries: Vec = f + .preimage + .trader_core() + .entries() + .iter() + .map(|entry| match entry { + CoreEntry::Mutation { key, pre, path, .. } if *key == spent_key => { + CoreEntry::Mutation { + key: *key, + pre: *pre, + post: *pre, + path: path.clone(), + } + } + other => other.clone(), + }) + .collect(); + let (precommit, preimage) = with_trader_core(&f, entries); + assert_eq!( + validate(&precommit, &preimage, &f.evidence), + Err(Refusal::Invalid(Invalid::LeafPostValueMismatch)) + ); + } } diff --git a/dsm_client/deterministic_state_machine/dsm/src/sofi/wire/mod.rs b/dsm_client/deterministic_state_machine/dsm/src/sofi/wire/mod.rs index 72cfe831d..c533b1a76 100644 --- a/dsm_client/deterministic_state_machine/dsm/src/sofi/wire/mod.rs +++ b/dsm_client/deterministic_state_machine/dsm/src/sofi/wire/mod.rs @@ -108,6 +108,12 @@ //! `0x004D TraderRelationshipLeaf` (the `R_econ` leaf state, at `k_{T,v}`): //! 1 `vault_id` digest32 · 2 `leaf` digest32 (`hʲ`). //! +//! `0x0061 TraderPreBalance` (SoFi Amendment S12, a trader's balance of one +//! token before a trade, named in `𝒞_E^pre` by its content address under +//! `DSM/sofi/trader-pre-balance-object/v1`): 1 `trader_genesis` digest32 · 2 +//! `trader_device_id` digest32 · 3 `policy_commit` digest32 · 4 `amount` u64, +//! strictly positive. +//! //! Core entries, each carrying a full authentication path against the core's //! `pre_root` (default-sibling compression is deferred): //! `0x004E CoreEntryMutation`: 1 `key` · 2 `pre` · 3 `post` · 4 `path` @@ -269,6 +275,9 @@ pub enum SofiWireError { /// Only `P` and `F` are signed objects; `G` has no issuer signature, and a /// setup signs `m_setup` through its own object. UnsupportedSignedBodyClass { body_class: u16 }, + /// A `TraderPreBalance` of zero. A zero balance is an absent leaf, which + /// needs no object, so an object holding zero has no canonical bytes. + ZeroPreBalance, } impl core::fmt::Display for SofiWireError { @@ -328,6 +337,10 @@ impl core::fmt::Display for SofiWireError { f, "authentication path is {got} siblings deep; the tree fixes {expected}" ), + Self::ZeroPreBalance => write!( + f, + "a balance of zero is an absent leaf and has no TraderPreBalance" + ), } } } diff --git a/dsm_client/deterministic_state_machine/dsm/src/sofi/wire/objects.rs b/dsm_client/deterministic_state_machine/dsm/src/sofi/wire/objects.rs index 8df24daac..aca75b6d7 100644 --- a/dsm_client/deterministic_state_machine/dsm/src/sofi/wire/objects.rs +++ b/dsm_client/deterministic_state_machine/dsm/src/sofi/wire/objects.rs @@ -1393,6 +1393,80 @@ impl TraderRelationshipLeaf { } } +// ── 0x0061 TraderPreBalance ──────────────────────────────────────────────── + +/// A trader's balance of one token before a trade (SoFi Amendment S12). +/// +/// `T°` states each balance leaf it writes only by the hash of its value, and +/// `TraderSideValid` recomputes the balance after the trade from the balance +/// before it. The exercise carries that value as this object, named in +/// `𝒞_E^pre` by its content address, so `E` commits it. It is evidence and +/// never authority: a verifier accepts it only because it hashes to the leaf +/// value the core states at `balance_key(trader_genesis, trader_device_id, +/// policy_commit)`, and the fold proves that leaf against the trader's root. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TraderPreBalance { + trader_genesis: D32, + trader_device_id: D32, + policy_commit: D32, + amount: u64, +} + +impl TraderPreBalance { + /// A zero balance is an absent leaf and needs no object, so a zero amount + /// is refused rather than encoded. + pub fn new( + trader_genesis: D32, + trader_device_id: D32, + policy_commit: D32, + amount: u64, + ) -> Result { + if amount == 0 { + return Err(SofiWireError::ZeroPreBalance); + } + Ok(Self { + trader_genesis, + trader_device_id, + policy_commit, + amount, + }) + } + + pub fn trader_genesis(&self) -> &D32 { + &self.trader_genesis + } + + pub fn trader_device_id(&self) -> &D32 { + &self.trader_device_id + } + + pub fn policy_commit(&self) -> &D32 { + &self.policy_commit + } + + pub fn amount(&self) -> u64 { + self.amount + } + + pub fn encode(&self) -> Vec { + let mut out = Vec::new(); + push_env(&mut out, class::SOFI_TRADER_PRE_BALANCE); + push_digest32(&mut out, &self.trader_genesis); + push_digest32(&mut out, &self.trader_device_id); + push_digest32(&mut out, &self.policy_commit); + push_u64(&mut out, self.amount); + out + } + + pub fn decode(bytes: &[u8]) -> Result { + let mut c = Cursor { b: bytes, i: 0 }; + c.envelope(class::SOFI_TRADER_PRE_BALANCE, SCHEMA_V1)?; + let v = Self::new(c.digest32()?, c.digest32()?, c.digest32()?, c.u64()?) + .map_err(wire_invalid)?; + finish(&c, v) + } +} + // ── 0x004E | 0x004F | 0x0050 CoreEntry ───────────────────────────────────── /// One per-key entry of a core, with its full authentication path against the diff --git a/dsm_client/deterministic_state_machine/dsm/tests/sofi_v8_vault_bytes.rs b/dsm_client/deterministic_state_machine/dsm/tests/sofi_v8_vault_bytes.rs index 5c7c09ec8..6d31aa488 100644 --- a/dsm_client/deterministic_state_machine/dsm/tests/sofi_v8_vault_bytes.rs +++ b/dsm_client/deterministic_state_machine/dsm/tests/sofi_v8_vault_bytes.rs @@ -1041,6 +1041,96 @@ fn decoders_refuse_wrong_classes_truncation_and_trailing_bytes() { )); } +/// SoFi Amendment S12: a trader's balance before a trade. Its bytes follow +/// the field table, its address is the immutable-store address under its own +/// namespace, and its balance hashes to the economic leaf value a trader core +/// states — each checked against the independent encoder and frozen here. +#[test] +fn a_trader_pre_balance_matches_the_independent_encoder_and_its_frozen_address() { + let balance = TraderPreBalance::new(b(0x91), b(0x92), b(0x93), 50_000).unwrap(); + let bytes = [ + indep::env(0x0061), + b(0x91).to_vec(), + b(0x92).to_vec(), + b(0x93).to_vec(), + indep::u64be(50_000), + ] + .concat(); + assert_eq!(balance.encode(), bytes); + assert_eq!(TraderPreBalance::decode(&bytes).unwrap(), balance); + + let namespace = "DSM/sofi/trader-pre-balance-object/v1"; + let inner = indep::h(namespace, &[&bytes]); + let addr = indep::h("DSM/storage-object", &[namespace.as_bytes(), &inner]); + assert_eq!(d::trader_pre_balance_addr(&balance), addr); + assert_eq!(d::closure_content_address(0x0061, &bytes), Some(addr)); + assert_eq!( + cf(&addr), + "EX68SMCY5KTWY45TRJEM50Z4PJN9PE5RW59QPHPK6QB08H5FF9FG" + ); + + // The leaf the core states: the economic balance state (class 0x001F) + // under the economic leaf-state tag. + let leaf = indep::h( + "DSM/economic-leaf-state/v1", + &[&[indep::env(0x001F), b(0x93).to_vec(), indep::u64be(50_000)].concat()], + ); + assert_eq!( + dsm::economic::state::EconomicLeafState::Balance( + dsm::economic::state::EconomicBalanceState { + policy_commit: b(0x93), + amount: 50_000, + } + ) + .leaf_value() + .unwrap(), + leaf + ); + assert_eq!( + cf(&leaf), + "2TEDV0VXSX6BB0XTEGJ9BA21N0PVZHCC0EC99Y34SQX62YWAJ7W0" + ); + + // Strict: a zero balance, the wrong class, truncation and trailing bytes + // have no reading. + assert!(matches!( + TraderPreBalance::new(b(0x91), b(0x92), b(0x93), 0), + Err(SofiWireError::ZeroPreBalance) + )); + let zero = [ + indep::env(0x0061), + b(0x91).to_vec(), + b(0x92).to_vec(), + b(0x93).to_vec(), + indep::u64be(0), + ] + .concat(); + assert!(matches!( + TraderPreBalance::decode(&zero), + Err(DecodeError::Invalid(..)) + )); + assert!(matches!( + TraderPreBalance::decode(&bytes[..bytes.len() - 1]), + Err(DecodeError::Truncated) + )); + let mut long = bytes.clone(); + long.push(0); + assert!(matches!( + TraderPreBalance::decode(&long), + Err(DecodeError::TrailingBytes { extra: 1 }) + )); + let relationship = VaultRelationshipLeaf { + trader_genesis: b(0x91), + trader_device_id: b(0x92), + leaf: b(0x93), + } + .encode(); + assert!(matches!( + TraderPreBalance::decode(&relationship), + Err(DecodeError::WrongClass { got: 0x004C }) + )); +} + #[test] fn the_new_classes_are_allocated_exactly_once() { let src = include_str!("../src/ccb/mod.rs"); diff --git a/dsm_client/deterministic_state_machine/dsm_sdk/src/handlers/node_e2e_tests.rs b/dsm_client/deterministic_state_machine/dsm_sdk/src/handlers/node_e2e_tests.rs index be54d4e23..7d38f45ba 100644 --- a/dsm_client/deterministic_state_machine/dsm_sdk/src/handlers/node_e2e_tests.rs +++ b/dsm_client/deterministic_state_machine/dsm_sdk/src/handlers/node_e2e_tests.rs @@ -9,7 +9,7 @@ use std::collections::BTreeMap; -use dsm::economic::lineage::{AdmittedEconomicPosition, ValidatedEconomicRoot}; +use dsm::economic::lineage::AdmittedEconomicPosition; use dsm::route_chain::{CellFact, ChainState}; use dsm::sofi::conformance::{ conformance_invalid_in_hand, derive_policy_fulfillments, FulfillmentConformanceError, @@ -18,7 +18,7 @@ use dsm::sofi::derive; use dsm::sofi::exercise::{recognize_exercise, RecognizedExercise}; use dsm::sofi::publication::Publication; use dsm::sofi::resolution::WalkOutcome; -use dsm::sofi::resolve::{LocalLeaves, WALK_BUDGET}; +use dsm::sofi::resolve::WALK_BUDGET; use dsm::sofi::wire::{ AttemptEntry, DlvPolicyFulfillmentBody, PrecommitLeg, SofiExercise, TraderFulfillmentBody, TraderPrecommitBody, @@ -32,7 +32,7 @@ use crate::bridge::{AppInvoke, AppQuery, AppResult, AppRouter as _}; use crate::economic_fixtures::NETWORK; use crate::sdk::sofi_advance::{complete_pending_fulfillment, Completion, NotTaken}; use crate::sdk::sofi_exercise::{attempt_cell, write_exercise}; -use crate::sdk::sofi_reads::{local_leaves_of_validated, VerifierContext}; +use crate::sdk::sofi_reads::VerifierContext; use crate::sdk::sofi_register::position_cells; use crate::sdk::storage_set::canonical_set; use crate::storage::client_db::economic_lineage; @@ -254,20 +254,13 @@ struct Market { tkn: [u8; 32], } -/// A creates the token and the vault (100 ERA against 1000 TKN at 30 bps); -/// B adopts the token and sets up. Adoption precedes receipt (owner ruling -/// 2026-09-13): the trader adds TKN before it can receive any. -async fn open_market(p: &Pair) -> Market { - let tkn = create_token(&p.a, "TKN", 10_000).await; - let era = era(); - let (token_a, token_b, reserve_a, reserve_b) = if era < tkn { - (era, tkn, 100, 1_000) - } else { - (tkn, era, 1_000, 100) - }; +/// `d`'s `sofi.createVault` on two tokens at their reserves, at 30 bps, the +/// pair in the order §28 requires (`token_a < token_b`). The vault id. +async fn create_vault(d: &TestDevice, x: ([u8; 32], u64), y: ([u8; 32], u64)) -> [u8; 32] { + let ((token_a, reserve_a), (token_b, reserve_b)) = if x.0 < y.0 { (x, y) } else { (y, x) }; let vault_id = match payload( &invoke( - &p.a, + d, "sofi.createVault", args(&generated::SofiCreateVaultRequest { token_a_policy_commit: token_a.to_vec(), @@ -282,29 +275,55 @@ async fn open_market(p: &Pair) -> Market { Payload::SofiVaultCreatedResponse(v) => v.vault_id, other => panic!("sofi.createVault answered {other:?}"), }; - let vault_id: [u8; 32] = vault_id + vault_id .as_slice() .try_into() - .expect("a vault id is 32 bytes"); - p.b.enter(); - let adopted = - p.b.router() - .query(crate::bridge::AppQuery { - path: "tokens.addByAnchor".to_string(), - params: crate::util::text_id::encode_base32_crockford(&tkn).into_bytes(), - }) - .await; - assert!(adopted.success, "B adopts TKN: {:?}", adopted.error_message); - payload( + .expect("a vault id is 32 bytes") +} + +/// `d` adds `token` by its anchor. Adoption precedes receipt (owner ruling +/// 2026-09-13): a trader adds a token before it can receive any. +async fn adopt(d: &TestDevice, token: &[u8; 32]) { + d.enter(); + let adopted = d + .router() + .query(crate::bridge::AppQuery { + path: "tokens.addByAnchor".to_string(), + params: crate::util::text_id::encode_base32_crockford(token).into_bytes(), + }) + .await; + assert!( + adopted.success, + "{} adopts the token: {:?}", + d.slot, adopted.error_message + ); +} + +/// `d`'s `sofi.setup` with `vault_id` (§29). The setup reference. +async fn set_up(d: &TestDevice, vault_id: &[u8; 32]) -> Vec { + match payload( &invoke( - &p.b, + d, "sofi.setup", args(&generated::SofiSetupRequest { vault_id: vault_id.to_vec(), }), ) .await, - ); + ) { + Payload::SofiSetupResponse(r) => r.setup_ref, + other => panic!("sofi.setup answered {other:?}"), + } +} + +/// A creates the token and the vault (100 ERA against 1000 TKN at 30 bps); +/// B adopts the token and sets up. +async fn open_market(p: &Pair) -> Market { + let tkn = create_token(&p.a, "TKN", 10_000).await; + let era = era(); + let vault_id = create_vault(&p.a, (era, 100), (tkn, 1_000)).await; + adopt(&p.b, &tkn).await; + set_up(&p.b, &vault_id).await; Market { vault_id, era, tkn } } @@ -338,40 +357,41 @@ async fn resolve(p: &Pair) -> (u64, i32) { ) } -/// B trades `amount_in` ERA in the market and the position resolves -/// Realized, through `sofi.resolve` if the trade's own rounds did not get -/// there. The position. -async fn realized_trade(p: &Pair, m: &Market, amount_in: u64) -> u64 { +/// `d` takes a position through `route` and it resolves Realized, through +/// `sofi.resolve` if the route's own rounds did not get there. The position. +async fn realized_through(d: &TestDevice, route: &str, request: Vec) -> u64 { let realized = generated::SofiPositionState::Realized as i32; - let (position, state) = position_of( - &invoke(&p.b, "sofi.trade", args(&trade_request(m, amount_in))).await, - "sofi.trade", - ); + let (position, state) = position_of(&invoke(d, route, request).await, route); if state == realized { return position; } - let (resolved, state) = resolve(p).await; - assert_eq!((resolved, state), (position, realized)); + let resolved = position_of( + &invoke(d, "sofi.resolve", args(&generated::SofiResolveRequest {})).await, + "sofi.resolve", + ); + assert_eq!(resolved, (position, realized), "{route} on {}", d.slot); position } +/// B trades `amount_in` ERA in the market and the position resolves +/// Realized. The position. +async fn realized_trade(p: &Pair, m: &Market, amount_in: u64) -> u64 { + realized_through(&p.b, "sofi.trade", args(&trade_request(m, amount_in))).await +} + /// What a device stands on for a resolution, as `sofi_advance` assembles it: -/// its own leaves at its validated predecessor, and the conditional position -/// it resolved, if that is what it stands on. -fn standing_of(d: &TestDevice) -> (LocalLeaves, Option) { +/// its identity, and the conditional position it resolved, if that is what +/// it stands on. +fn standing_of(d: &TestDevice) -> (([u8; 32], [u8; 32]), Option) { d.enter(); let admitted = economic_lineage::get_admitted() .expect("read admitted") .expect("an admitted position"); - let validated = ValidatedEconomicRoot::rehydrate_from_admitted_store(admitted) - .expect("a resolved predecessor"); - let local = - local_leaves_of_validated(&d.genesis, &d.device_id, &validated).expect("own leaves"); // The position this device resolved itself, for Core to read what it // selected when a P names it as its parent. let parent = matches!(admitted, AdmittedEconomicPosition::ResolvedSofi { .. }).then_some(admitted); - (local, parent) + ((d.genesis, d.device_id), parent) } fn pending_position(d: &TestDevice) -> Option { @@ -576,8 +596,8 @@ async fn a_key_held_by_an_exercise_its_own_bytes_refute_is_skipped_on_those_byte // The vault's chain as B established it: the genesis root and the // generation B's trade produced. - let (local, parents) = standing_of(&p.b); - let ctx = VerifierContext::new(&set, Some(&local), parents.as_ref()).expect("a verifier"); + let (own, parents) = standing_of(&p.b); + let ctx = VerifierContext::new(&set, Some(own), parents.as_ref()).expect("a verifier"); let verifier = ctx.verifier(); let chain = verifier.chain(&m.vault_id).expect("the vault's chain"); assert_eq!(chain.roots().len(), 2, "genesis and one consumption"); @@ -704,8 +724,8 @@ async fn an_unsigned_exercise_at_a_successor_key_takes_nothing() { let m = open_market(&p).await; realized_trade(&p, &m, 10).await; let set = canonical_set(NETWORK).expect("the pinned set"); - let (local, parents) = standing_of(&p.b); - let ctx = VerifierContext::new(&set, Some(&local), parents.as_ref()).expect("a verifier"); + let (own, parents) = standing_of(&p.b); + let ctx = VerifierContext::new(&set, Some(own), parents.as_ref()).expect("a verifier"); let verifier = ctx.verifier(); let chain = verifier.chain(&m.vault_id).expect("the vault's chain"); assert_eq!(chain.roots().len(), 2, "genesis and one consumption"); @@ -786,8 +806,8 @@ async fn a_trade_cut_short_by_a_refused_write_is_the_network_status_until_it_lan let pair = position_cells(&set, &p.b.genesis, &p.b.device_id, q, &root) .expect("B's next position pair"); let pair_leader = member_name(pair.fulfillment().route().leader()); - let (local, parents) = standing_of(&p.b); - let ctx = VerifierContext::new(&set, Some(&local), parents.as_ref()).expect("a verifier"); + let (own, parents) = standing_of(&p.b); + let ctx = VerifierContext::new(&set, Some(own), parents.as_ref()).expect("a verifier"); let verifier = ctx.verifier(); let chain = verifier.chain(&m.vault_id).expect("the vault's chain"); assert_eq!(chain.roots().len(), 1, "the vault is at its genesis"); @@ -904,3 +924,216 @@ async fn a_route_search_that_cannot_see_a_vault_is_an_error_not_an_empty_route() assert_eq!(route.len(), 1, "the hop is found again"); assert_eq!(route[0].amount_out, out); } + +/// CONFORMANCE §6.39, SoFi Amendment S12: once a trader has traded through a +/// vault, the owner's close still resolves. The owner judges the trader's +/// exercise from the exercise's own bytes: the trader's balance before the +/// trade travels in it, so the owner needs nothing of the trader's. +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +#[serial] +async fn a_vault_traded_through_closes_for_its_owner() { + let p = Pair::boot(500, 200).await; + let m = open_market(&p).await; + assert_eq!( + set_up(&p.a, &m.vault_id).await.len(), + 32, + "the owner's setup" + ); + let out = dsm::dlv::route_commit::constant_product_output(10, 100, 1_000, 30) + .expect("the vault prices the trade"); + realized_trade(&p, &m, 10).await; + assert_eq!(balance(&p.b, &m.tkn), out, "B holds what the trade priced"); + + let era_before = balance(&p.a, &m.era); + let tkn_before = balance(&p.a, &m.tkn); + realized_through( + &p.a, + "sofi.close", + args(&generated::SofiCloseRequest { + vault_id: m.vault_id.to_vec(), + }), + ) + .await; + assert_eq!( + balance(&p.a, &m.era), + era_before + 110, + "the vault's ERA: its reserve and all of B's input" + ); + assert_eq!( + balance(&p.a, &m.tkn), + tkn_before + 1_000 - out, + "and the TKN B did not take" + ); +} + +/// SoFi §27 (MR-SOFI-0255): the app reaches SoFi through exactly its eight +/// routes. Each, sent through the production router, reaches its producer +/// and answers with the result §27 names for it: +/// - `sofi.createVault`: A opens a vault on ERA/TKN and one on TKN/TKB. +/// - `sofi.setup`: B sets up with both, and A with the vault it closes: a +/// close is a one-hop route against the owner's own vault, carrying the +/// owner's setup (§32). +/// - `sofi.findRoute`: B is quoted ERA→TKN→TKB, two hops, each priced at its +/// vault's head. +/// - `sofi.route`: B takes those two hops and receives what they priced. +/// - `sofi.relay`: A carries B's route position with nothing from B: the +/// position pair's two cells and each hop's key. +/// - `sofi.trade`: B takes the one hop ERA→TKN at the price it was quoted. +/// - `sofi.close`: A closes its TKN/TKB vault and receives both reserves. +/// - `sofi.resolve`: resolves each position its route's own rounds left +/// pending, and with nothing pending its producer refuses. +/// +/// A ninth `sofi.` method reaches no SoFi route: the router refuses it. +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +#[serial] +async fn every_sofi_route_reaches_its_producer() { + let p = Pair::boot(500, 200).await; + let era = era(); + let tkn = create_token(&p.a, "TKN", 10_000).await; + let tkb = create_token(&p.a, "TKB", 10_000).await; + let first = create_vault(&p.a, (era, 100), (tkn, 1_000)).await; + let second = create_vault(&p.a, (tkn, 1_000), (tkb, 1_000)).await; + assert_eq!(balance(&p.a, &tkn), 8_000, "A funded both vaults' TKN"); + assert_eq!(balance(&p.a, &tkb), 9_000, "and the second's TKB"); + for token in [&tkn, &tkb] { + adopt(&p.b, token).await; + } + for vault in [&first, &second] { + assert_eq!(set_up(&p.b, vault).await.len(), 32, "a setup reference"); + } + assert_eq!(set_up(&p.a, &second).await.len(), 32, "the owner's setup"); + + let hops = |r: &AppResult| match payload(r) { + Payload::SofiFindRouteResponse(r) => r.hops, + other => panic!("sofi.findRoute answered {other:?}"), + }; + let search = |token_out: &[u8; 32]| { + args(&generated::SofiFindRouteRequest { + token_in_policy_commit: era.to_vec(), + token_out_policy_commit: token_out.to_vec(), + amount_in: 10, + }) + }; + let one = dsm::dlv::route_commit::constant_product_output(10, 100, 1_000, 30) + .expect("the first vault prices its hop"); + let two = dsm::dlv::route_commit::constant_product_output(one, 1_000, 1_000, 30) + .expect("the second vault prices its hop"); + let quoted: Vec<_> = hops(&invoke(&p.b, "sofi.findRoute", search(&tkb)).await) + .into_iter() + .map(|h| { + ( + h.vault_id, + h.token_in_policy_commit, + h.token_out_policy_commit, + h.amount_in, + h.amount_out, + ) + }) + .collect(); + assert_eq!( + quoted, + vec![ + (first.to_vec(), era.to_vec(), tkn.to_vec(), 10, one), + (second.to_vec(), tkn.to_vec(), tkb.to_vec(), one, two), + ], + "ERA→TKN→TKB through the two vaults" + ); + + let routed = realized_through( + &p.b, + "sofi.route", + args(&generated::SofiRouteRequest { + vault_ids: vec![first.to_vec(), second.to_vec()], + token_in_policy_commit: era.to_vec(), + amount_in: 10, + min_amount_out: two, + }), + ) + .await; + assert_eq!(balance(&p.b, &era), 190, "the route took 10 ERA"); + assert_eq!(balance(&p.b, &tkb), two, "and gave what its hops priced"); + assert_eq!(balance(&p.b, &tkn), 0, "and kept nothing on the way"); + + let relayed = match payload( + &invoke( + &p.a, + "sofi.relay", + args(&generated::SofiRelayRequest { + trader_genesis: p.b.genesis.to_vec(), + trader_device_id: p.b.device_id.to_vec(), + position: routed, + }), + ) + .await, + ) { + Payload::SofiRelayResponse(r) => r.cells_written, + other => panic!("sofi.relay answered {other:?}"), + }; + assert_eq!( + relayed, 4, + "the position pair's two cells and both hops' keys" + ); + + let quote = hops(&invoke(&p.b, "sofi.findRoute", search(&tkn)).await); + assert_eq!(quote.len(), 1, "one hop ERA→TKN"); + let bought = quote[0].amount_out; + realized_through( + &p.b, + "sofi.trade", + args(&generated::SofiTradeRequest { + vault_id: first.to_vec(), + token_in_policy_commit: era.to_vec(), + amount_in: 10, + min_amount_out: bought, + }), + ) + .await; + assert_eq!(balance(&p.b, &era), 180, "the trade took 10 ERA"); + assert_eq!(balance(&p.b, &tkn), bought, "and gave what it was quoted"); + + realized_through( + &p.a, + "sofi.close", + args(&generated::SofiCloseRequest { + vault_id: second.to_vec(), + }), + ) + .await; + assert_eq!( + balance(&p.a, &tkb), + 10_000 - two, + "the close released every TKB B does not hold" + ); + assert_eq!( + balance(&p.a, &tkn), + 8_000 + 1_000 + one, + "and the vault's TKN: its reserve and the first hop's output" + ); + + let nothing_pending = invoke( + &p.b, + "sofi.resolve", + args(&generated::SofiResolveRequest {}), + ) + .await; + let Some(refusal) = nothing_pending.error_message else { + panic!( + "a resolve with nothing pending answered {:?}", + nothing_pending.data + ) + }; + assert!( + refusal.starts_with("sofi.resolve: ") + && refusal.contains("no pending admission: nothing to resolve"), + "sofi.resolve's producer refuses: {refusal}" + ); + + let unknown = invoke(&p.b, "sofi.quote", args(&generated::SofiResolveRequest {})).await; + let Some(refusal) = unknown.error_message else { + panic!("sofi.quote answered {:?}", unknown.data) + }; + assert!( + refusal.starts_with("unknown invoke method: 'sofi.quote'"), + "the router refuses a method that is no SoFi route: {refusal}" + ); +} diff --git a/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/sofi_advance.rs b/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/sofi_advance.rs index 6c54c31c8..86b43757a 100644 --- a/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/sofi_advance.rs +++ b/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/sofi_advance.rs @@ -38,7 +38,7 @@ use dsm::sofi::lineage::{advance_resolved, descendant_fence, AdvanceError}; use dsm::sofi::publication::Publication; use dsm::sofi::registration::Registration; use dsm::sofi::resolution::{PositionEffect, Resolution}; -use dsm::sofi::resolve::{Acquired, LocalLeaves}; +use dsm::sofi::resolve::Acquired; use dsm::sofi::storage::Resolved; use dsm::sofi::validation::{trader_post_states, vault_post_states}; use dsm::sofi::wire::{ @@ -54,7 +54,7 @@ use crate::sdk::economic_admission_flow::validated_root_or_activate; use crate::sdk::route_seats::{read_cell, NodeSeats}; use crate::sdk::sofi_exercise::{build_exercise, write_exercise, LegWrite}; use crate::sdk::sofi_publish::{fetch_fulfillment, fetch_precommit, fetch_preimage}; -use crate::sdk::sofi_reads::{local_leaves_of_validated, verifier_error, VerifierContext}; +use crate::sdk::sofi_reads::{verifier_error, VerifierContext}; use crate::sdk::sofi_register::{ install_fulfillment, position_cells, InstallError, InstallRequest, Installed, }; @@ -270,28 +270,26 @@ fn install_request<'a>(request: &FulfillRequest<'a>) -> InstallRequest<'a> { } } -/// This device as the verifier of its own position: its leaves at its -/// validated root, and the position it resolved itself. +/// This device as the verifier of its own position: its identity, and the +/// position it resolved itself. struct OwnStanding { - local: LocalLeaves, + own: (D32, D32), admitted: AdmittedEconomicPosition, } impl OwnStanding { - fn of( - core: &CoreSDK, - admitted: AdmittedEconomicPosition, - validated: &ValidatedEconomicRoot, - ) -> Result { + fn of(core: &CoreSDK, admitted: AdmittedEconomicPosition) -> Result { let head = core .device_head() .ok_or_else(|| storage("device head", "none"))?; - let local = local_leaves_of_validated(&head.genesis_digest(), &head.devid(), validated)?; - Ok(Self { local, admitted }) + Ok(Self { + own: (head.genesis_digest(), head.devid()), + admitted, + }) } fn context<'a>(&'a self, set: &'a StorageSet) -> Result, DsmError> { - VerifierContext::new(set, Some(&self.local), Some(&self.admitted)) + VerifierContext::new(set, Some(self.own), Some(&self.admitted)) } } @@ -338,11 +336,6 @@ async fn exercise_legs( { Acquired::Complete(evidence) => evidence, Acquired::Exhausted(missing) => return Ok(Err(NotTaken::Evidence(missing))), - Acquired::NoSource(missing) => { - return Err(refuse(format!( - "exercise: conformance evidence has no source: {missing:?}" - ))) - } }; let exercise = build_exercise(&install, &evidence)?; let recognized = dsm::sofi::exercise::recognize_exercise(&exercise.encode()) @@ -559,9 +552,7 @@ pub async fn complete_pending_fulfillment( // or it is not finished. descendant_fence(admitted.predecessor_claim(), &pending.pre_economic_root) .map_err(|e| refuse(e.to_string()))?; - let validated = ValidatedEconomicRoot::rehydrate_from_admitted_store(admitted) - .map_err(|e| refuse(e.to_string()))?; - let standing = OwnStanding::of(core, admitted, &validated)?; + let standing = OwnStanding::of(core, admitted)?; let ctx = standing.context(set)?; let installed = match install_pair(&ctx, set, &request).await? { Ok(installed) => installed, @@ -719,7 +710,7 @@ pub async fn resolve_pending_position( // Registration, from the pair (R10). The F at q must be THIS one: the // device only ever writes its own claims at its own positions, so any // other outcome is a local incoherence, not a race to wait out. - let standing = OwnStanding::of(core, admitted, &validated)?; + let standing = OwnStanding::of(core, admitted)?; let ctx = standing.context(set)?; let verifier = ctx.verifier(); let registration = match verifier diff --git a/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/sofi_flow.rs b/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/sofi_flow.rs index 38bee2567..bde259811 100644 --- a/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/sofi_flow.rs +++ b/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/sofi_flow.rs @@ -355,8 +355,7 @@ pub async fn setup( refuse("no admitted position: a setup names the claim registered at its position") })?; let validated = validated_root_or_activate(core)?; - let local = local_leaves_of_validated(&genesis, &device_id, &validated)?; - let ctx = VerifierContext::new(set, Some(&local), Some(&admitted))?; + let ctx = VerifierContext::new(set, Some((genesis, device_id)), Some(&admitted))?; match ctx .verifier() .vault_genesis(&intent.vault_id) @@ -496,7 +495,6 @@ async fn vault_policies(set: &StorageSet, state: &VaultStateLeaf) -> Result(&'a self, set: &'a StorageSet) -> Result, DsmError> { - VerifierContext::new(set, Some(&self.local), Some(&self.admitted)) + VerifierContext::new( + set, + Some((self.genesis, self.device_id)), + Some(&self.admitted), + ) } } @@ -988,11 +990,11 @@ async fn exercise_draft( let ctx = standing.context(set)?; let verifier = ctx.verifier(); let evidence = match verifier - .acquire_evidence(draft.precommit(), draft.preimage()) + .acquire_evidence(draft.precommit(), draft.preimage(), &draft.carried()) .map_err(verifier_error)? { Acquired::Complete(evidence) => evidence, - Acquired::Exhausted(missing) | Acquired::NoSource(missing) => { + Acquired::Exhausted(missing) => { return Err(storage( "evidence", format!("not in hand after the acquisition rounds: {missing:?}; nothing published"), @@ -1033,7 +1035,8 @@ async fn exercise_draft( ToPublish::Setup(..) | ToPublish::Precommit(..) | ToPublish::Preimage(..) - | ToPublish::PolicyFulfillment(..) => None, + | ToPublish::PolicyFulfillment(..) + | ToPublish::PreBalance(..) => None, }) .ok_or_else(|| refuse("the fulfillment was not produced"))?; diff --git a/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/sofi_publish.rs b/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/sofi_publish.rs index 47b60238f..eb3f7e09a 100644 --- a/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/sofi_publish.rs +++ b/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/sofi_publish.rs @@ -105,6 +105,7 @@ pub async fn publish_produced( body, signature: operation_signature, }, + ToPublish::PreBalance(balance) => Publication::TraderPreBalance(balance), }; out.push(publish(set, &publication).await?); } diff --git a/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/sofi_reads.rs b/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/sofi_reads.rs index a7c62dd55..552e2ac7c 100644 --- a/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/sofi_reads.rs +++ b/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/sofi_reads.rs @@ -85,17 +85,29 @@ pub struct LiveSofiReads<'a> { set: &'a StorageSet, runtime: tokio::runtime::Handle, network: Vec, + /// This device's `(genesis, device_id)` when it verifies as a trader: + /// the one identity its own admitted store answers for. + own: Option<(D32, D32)>, } impl<'a> LiveSofiReads<'a> { - pub fn new(set: &'a StorageSet) -> Result { + pub fn new(set: &'a StorageSet, own: Option<(D32, D32)>) -> Result { Ok(Self { set, runtime: tokio::runtime::Handle::current(), network: committed_network_id()?, + own, }) } + fn peer_resolver(&self) -> LiveRegisterResolver<'_> { + LiveRegisterResolver { + set: self.set, + runtime: self.runtime.clone(), + expected_network_id: self.network.clone(), + } + } + fn block(&self, fut: impl Future) -> T { tokio::task::block_in_place(|| self.runtime.block_on(fut)) } @@ -169,12 +181,13 @@ impl SofiReads for LiveSofiReads<'_> { device_id: &D32, position: u64, ) -> Result { - let resolver = LiveRegisterResolver { - set: self.set, - runtime: self.runtime.clone(), - expected_network_id: self.network.clone(), - }; - resolve_peer_with_cache(&resolver, &self.network, genesis, device_id, position) + resolve_peer_with_cache( + &self.peer_resolver(), + &self.network, + genesis, + device_id, + position, + ) } fn vault_leaves_at( @@ -193,19 +206,27 @@ impl SofiReads for LiveSofiReads<'_> { genesis: &D32, device_id: &D32, position: u64, - ) -> Result, ReadFailure> { - let Some(admitted) = economic_lineage::get_admitted_at(position) - .map_err(|e| ReadFailure(format!("admitted history: {e}")))? - else { - return Ok(None); - }; - match AcceptedClaim::rehydrate_from_admitted_store(*genesis, *device_id, admitted) { - Ok(claim) => Ok(Some(claim)), - Err(unresolved) => { - log::info!("[sofi reads] no accepted claim at {position}: {unresolved:?}"); - Ok(None) - } + ) -> Result { + if self.own != Some((*genesis, *device_id)) { + // Another trader's position: the claim the peer walk accepted + // there, as `advance_validated` produced it on this device, or + // the walk's failure in the class it gave it. + return resolve_peer_with_cache( + &self.peer_resolver(), + &self.network, + genesis, + device_id, + position, + ) + .map(|transition| *transition.accepted_claim()); } + let admitted = economic_lineage::get_admitted_at(position) + .map_err(|e| PeerLineageFailure::Incomplete(format!("admitted history: {e}")))? + .ok_or_else(|| { + PeerLineageFailure::Incomplete(format!("no admitted position at {position}")) + })?; + AcceptedClaim::rehydrate_from_admitted_store(*genesis, *device_id, admitted) + .map_err(|unresolved| PeerLineageFailure::Unresolved(unresolved.to_string())) } fn recorded_generations( @@ -232,32 +253,30 @@ impl SofiReads for LiveSofiReads<'_> { /// Everything a [`Verifier`] borrows, held together: the reads over the /// pinned set, the set's members and id, the committed network, and — when -/// the verifier is a trader — its own leaves and the position it resolved +/// the verifier is a trader — its identity and the position it resolved /// itself. pub struct VerifierContext<'a> { reads: LiveSofiReads<'a>, members: StorageSetMembers, set_id: D32, network: Vec, - local: Option<&'a LocalLeaves>, parent: Option<&'a AdmittedEconomicPosition>, } impl<'a> VerifierContext<'a> { - /// A verifier over `set`. `local` and `parent` are this device's own - /// leaves and resolved predecessor when it verifies as a trader; a relay - /// or a reader of another trader's position brings neither. + /// A verifier over `set`. `own` and `parent` are this device's identity + /// and resolved predecessor when it verifies as a trader; a relay or a + /// reader of another trader's position brings neither. pub fn new( set: &'a StorageSet, - local: Option<&'a LocalLeaves>, + own: Option<(D32, D32)>, parent: Option<&'a AdmittedEconomicPosition>, ) -> Result { Ok(Self { - reads: LiveSofiReads::new(set)?, + reads: LiveSofiReads::new(set, own)?, members: as_ccb_members(set)?, set_id: set.id(), network: committed_network_id()?, - local, parent, }) } @@ -268,7 +287,6 @@ impl<'a> VerifierContext<'a> { members: &self.members, set_id: self.set_id, network_id: &self.network, - local: self.local, parent: self.parent, } } diff --git a/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/sofi_register.rs b/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/sofi_register.rs index 452f6fde4..8fdfdb5d0 100644 --- a/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/sofi_register.rs +++ b/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/sofi_register.rs @@ -170,9 +170,7 @@ pub async fn install_fulfillment( .map_err(|e| InstallError::Storage(e.to_string()))? { Acquired::Complete(evidence) => evidence, - Acquired::Exhausted(missing) | Acquired::NoSource(missing) => { - return Err(InstallError::Unavailable(missing)) - } + Acquired::Exhausted(missing) => return Err(InstallError::Unavailable(missing)), }; match fulfillment_conformance( request.fulfillment, diff --git a/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/sofi_sdk.rs b/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/sofi_sdk.rs index a05359f6d..5921c474b 100644 --- a/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/sofi_sdk.rs +++ b/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/sofi_sdk.rs @@ -40,6 +40,8 @@ //! //! `sdk::sofi_flow` runs them for the SoFi routes. +use std::collections::BTreeMap; + use dsm::sofi::admission::{preimage_admissible, NotAdmissible}; use dsm::sofi::conformance::{ check_fulfillment_against_precommit, derive_policy_fulfillments, FulfillmentConformanceError, @@ -54,8 +56,8 @@ use dsm::sofi::validation::{realize_root, validate, Evidence, Invalid, Missing, use dsm::sofi::wire::{ AttemptEntry, DlvCore, DlvPolicyFulfillmentBody, OwnerAuthority, ParentClaimRef, PreEClosureIndex, PrecommitLeg, SettlementBody, SettlementPreimage, SofiSetupBody, - SofiWireError, SwapHop, TraderCore, TraderFulfillmentBody, TraderPrecommitBody, VaultCreation, - VaultGenesisPreimage, + SofiWireError, SwapHop, TraderCore, TraderFulfillmentBody, TraderPreBalance, + TraderPrecommitBody, ValidationRef, VaultCreation, VaultGenesisPreimage, }; use dsm::types::operations::Operation; @@ -163,6 +165,9 @@ pub enum ToPublish { Preimage(SettlementPreimage), PolicyFulfillment(DlvPolicyFulfillmentBody), Fulfillment(TraderFulfillmentBody), + /// A trader's balance before the trade (SoFi Amendment S12), found by + /// the address `𝒞_E^pre` names. + PreBalance(TraderPreBalance), } /// A produced operation: the transition, what must be published for anyone to @@ -191,6 +196,7 @@ pub struct Produced { pub struct UncheckedDraft { precommit: TraderPrecommitBody, preimage: SettlementPreimage, + pre_balances: Vec, } impl UncheckedDraft { @@ -201,6 +207,28 @@ impl UncheckedDraft { pub fn preimage(&self) -> &SettlementPreimage { &self.preimage } + + /// The closure objects only this trader holds before they are published: + /// each `TraderPreBalance` under the reference `𝒞_E^pre` names it by. + pub fn carried(&self) -> BTreeMap> { + carried(&self.pre_balances) + } +} + +/// Each `TraderPreBalance` under the closure reference that names it. +fn carried(pre_balances: &[TraderPreBalance]) -> BTreeMap> { + pre_balances + .iter() + .map(|balance| (pre_balance_ref(balance), balance.encode())) + .collect() +} + +/// The `ContentAddr` `𝒞_E^pre` names a `TraderPreBalance` by. +fn pre_balance_ref(balance: &TraderPreBalance) -> ValidationRef { + ValidationRef::ContentAddr { + object_class: dsm::ccb::class::SOFI_TRADER_PRE_BALANCE, + addr: derive::trader_pre_balance_addr(balance), + } } /// The verifier's own verdict over a draft, over evidence acquired for it @@ -217,6 +245,7 @@ pub fn check_draft( Ok(()) => Ok(PrecommitDraft { precommit: draft.precommit, preimage: draft.preimage, + pre_balances: draft.pre_balances, }), Err(Refusal::Invalid(reason)) => Err(BuildError::StaticallyInvalid(reason)), Err(Refusal::Incomplete(missing)) => Err(BuildError::Incomplete(missing)), @@ -229,6 +258,7 @@ pub fn check_draft( pub struct PrecommitDraft { precommit: TraderPrecommitBody, preimage: SettlementPreimage, + pre_balances: Vec, } impl PrecommitDraft { @@ -250,6 +280,12 @@ impl PrecommitDraft { &self.preimage } + /// The closure objects only this trader holds: each `TraderPreBalance` + /// under the reference `𝒞_E^pre` names it by. + pub fn carried(&self) -> BTreeMap> { + carried(&self.pre_balances) + } + /// `E`, as the preimage derives it. pub fn external_commitment(&self) -> D32 { *self.precommit.external_commitment() @@ -412,38 +448,40 @@ pub struct TraderContext<'a> { /// `𝒞_E^pre` of a draft: the typed reference of the parent `P` names (P /// conformance rule 2), so `E` commits to the exact claim the operation -/// extends. Beta references no other pre-E object. -fn pre_e_closure(ctx: &TraderContext<'_>) -> Result { - Ok(PreEClosureIndex::new(vec![ctx - .parent_claim - .validation_ref( - &ctx.genesis, - &ctx.device_id, - ctx.position, - )])?) +/// extends; and one `TraderPreBalance` for each balance `T°` states as +/// present before the trade (SoFi Amendment S12), so a verifier that is not +/// this trader can judge it. The references are in canonical order. +fn pre_e_closure( + ctx: &TraderContext<'_>, + pre_balances: &[TraderPreBalance], +) -> Result { + let mut refs = + vec![ctx + .parent_claim + .validation_ref(&ctx.genesis, &ctx.device_id, ctx.position)]; + refs.extend(pre_balances.iter().map(pre_balance_ref)); + refs.sort_by_key(ValidationRef::encode); + Ok(PreEClosureIndex::new(refs)?) } /// Assemble `P(E)` and `P`, refusing anything beta will not run. /// /// Neither root is the caller's: `R_void` is `T°.pre_root` (P15-2), and -/// `R_realize` is `Fold(T°, E)` over the trader's own leaves, computed by the -/// Core function the verifier checks it with — so it can only be computed -/// once `E` exists, which is here. +/// `R_realize` is `Fold(T°, E)`, computed by the Core function the verifier +/// checks it with — so it can only be computed once `E` exists, which is +/// here. fn draft( settlement: SettlementBody, ctx: &TraderContext<'_>, cores: Vec, legs: Vec, - local: &LocalLeaves, + pre_balances: Vec, ) -> Result { let preimage = SettlementPreimage::new(settlement, ctx.trader_core.clone(), cores)?; // Beta will not execute this, so nothing here will build it. preimage_admissible(&preimage).map_err(BuildError::NotAdmissible)?; let e = derive::recompute_e(&preimage)?; - let trader_evidence = local - .trader_evidence(&ctx.trader_core) - .map_err(|why| BuildError::LocalLeaves(why.to_string()))?; - let realize = match realize_root(&ctx.trader_core, &e, &trader_evidence) { + let realize = match realize_root(&ctx.trader_core, &e) { Ok(root) => root, Err(Refusal::Invalid(reason)) => return Err(BuildError::StaticallyInvalid(reason)), Err(Refusal::Incomplete(missing)) => return Err(BuildError::Incomplete(missing)), @@ -466,6 +504,7 @@ fn draft( Ok(UncheckedDraft { precommit, preimage, + pre_balances, }) } @@ -514,6 +553,9 @@ pub fn draft_route( }) .collect(); legs.sort_by_key(|l| l.vault_id); + let pre_balances = local + .pre_balances(&ctx.trader_core) + .map_err(|why| BuildError::LocalLeaves(why.to_string()))?; let settlement = SettlementBody::Swap { token_in: first.token_in, amount_in: first.amount_in, @@ -522,9 +564,9 @@ pub fn draft_route( hops, trader_core: derive::trader_core_digest(&ctx.trader_core.encode()?), dlv_cores: core_digests, - closure: pre_e_closure(ctx)?, + closure: pre_e_closure(ctx, &pre_balances)?, }; - draft(settlement, ctx, sorted_cores, legs, local) + draft(settlement, ctx, sorted_cores, legs, pre_balances) } /// A full close of one vault by its origin owner. @@ -543,6 +585,9 @@ pub fn draft_close( ctx: &TraderContext<'_>, local: &LocalLeaves, ) -> Result { + let pre_balances = local + .pre_balances(&ctx.trader_core) + .map_err(|why| BuildError::LocalLeaves(why.to_string()))?; let settlement = SettlementBody::Close { vault_id, parent_root, @@ -552,7 +597,7 @@ pub fn draft_close( reserve_b, trader_core: derive::trader_core_digest(&ctx.trader_core.encode()?), dlv_core: derive::dlv_core_digest(&core.encode()?), - closure: pre_e_closure(ctx)?, + closure: pre_e_closure(ctx, &pre_balances)?, }; draft( settlement, @@ -563,7 +608,7 @@ pub fn draft_close( parent_root, setup_ref, }], - local, + pre_balances, ) } @@ -634,6 +679,13 @@ pub fn build_fulfillment( }), ToPublish::Preimage(draft.preimage.clone()), ]; + publish.extend( + draft + .pre_balances + .iter() + .cloned() + .map(ToPublish::PreBalance), + ); publish.extend(witnesses.into_iter().map(ToPublish::PolicyFulfillment)); publish.push(ToPublish::Fulfillment(fulfillment.clone())); Ok(Produced { diff --git a/specs/SoFi_Settlement_Specification.md b/specs/SoFi_Settlement_Specification.md index ad5394a91..f5c0ec64e 100644 --- a/specs/SoFi_Settlement_Specification.md +++ b/specs/SoFi_Settlement_Specification.md @@ -757,6 +757,7 @@ TAG_DSM_SOFI_VAULT_GENESIS_LOCATOR DSM/sofi/vault-genesis-locator/v1 TAG_DSM_SOFI_VAULT_CREATION_KEY DSM/sofi/vault-creation-key/v1 key of the creation leaf TAG_DSM_SOFI_VAULT_STATE_KEY DSM/sofi/vault-state-key/v1 key of the vault state leaf TAG_DSM_SOFI_VAULT_LEAF_STATE DSM/sofi/vault-leaf-state/v1 DLV leaf values +TAG_DSM_SOFI_TRADER_PRE_BALANCE_OBJECT DSM/sofi/trader-pre-balance-object/v1 address of a TraderPreBalance (Amendment S12) @@ -818,6 +819,7 @@ pre 0x0059 SOFI_SETTLEMENT_PREIMAGE P (E) 0x005A SOFI_VAULT_GENESIS_PREIMAGE vault genesis preimage 0x005B SOFI_VAULT_CREATION vault creation leaf +0x0061 SOFI_TRADER_PRE_BALANCE a trader's balance before the trade (Amendment S12) @@ -916,6 +918,14 @@ SetupValid(setup) is semantic and belongs to Core: canonical body encoding, ρ, ClaimRef, and the identity and vault relationship rules. > **Amendment S9 (owner, 2026-09-23) — ClaimRef is checked against the verifier's own lineage.** `SetupValid` compares the setup's `claim_ref` with the digest of the claim this verifier accepted at the setup's position `p` when it validated the trader's lineage: the registered root claim of an ordinary position, or `C_p` of a resolved SoFi position. RouteValidation's evidence carries that accepted claim as a value only lineage validation produces (or the device's own admitted store, for its own positions), so RouteValidation reads no storage for it. A setup naming any other claim is Invalid. Until the accepted claim is in hand, the setup is not evaluated. + +> **Amendment S13 (owner, 2026-09-29) — a lineage known invalid makes the setup Invalid.** This supersedes the last sentence of Amendment S9, which let two different states read alike: a lineage whose verdict is not known yet, and a lineage known to be invalid, which can never yield an accepted claim. Under S9's wording the second left its trade holding a vault key forever. `SetupValid` compares the setup's `claim_ref` with the claim accepted at position `p` by validation of the trader's lineage, and there are three cases: +> +> - **Not established.** Lineage validation has not reached a verdict, or evidence it needs is not in hand: `SetupValid` is not evaluated yet, and the verifier waits (Amendment S3). +> - **Valid.** The accepted claim is in hand, and the setup's `claim_ref` must equal its digest. A setup naming any other claim is Invalid. +> - **Invalid.** Lineage validation establishes the trader's lineage Invalid at or before `p`: `SetupValid` is Invalid, so RouteValidation is Invalid. A divergent write-once register cell of the trader, which the walk quarantines, is such a verdict. No accepted claim is required, and none is synthesized: the negative fact is the lineage verdict itself. +> +> No trade whose trader's lineage is known invalid can occupy a vault key indefinitely because that lineage can never yield an accepted claim. SetupRegistered(setup) is a durability fact that Core derives from storage reads. Accept(E) requires both. Storage establishes neither. @@ -1128,6 +1138,15 @@ It proves itself from its own bytes and state the reader already holds, so it ca realized, or impossible and skipped. A value that names nothing Core can classify cannot occupy a successor key. +> **Amendment S12 (owner, 2026-09-29) — the exercise carries the trader's balances before the trade.** `T°` states each balance leaf it writes only by the hash of its value, and `TraderSideValid` recomputes the balance after the trade from the balance before it. Only the trader held that value, so no other reader could judge the exercise: once one trader had traded through a vault, the next reader of that vault, its owner closing it included, could not classify the trade (CONFORMANCE §6.39). The exercise now carries those values. +> +> - **The object.** `TraderPreBalance`, class `0x0061`, schema 1: `trader_genesis` (digest32), `trader_device_id` (digest32), `policy_commit` (digest32), `amount` (u64, strictly positive). A zero balance is an absent leaf, and an absent balance needs no object. Its address is `immutable_addr(DSM/sofi/trader-pre-balance-object/v1, CCB bytes)`. `𝒞_E^pre` references it as `ContentAddr{0x0061, addr}`, so `E` commits it, and the exercise carries it with the other closure objects. +> - **Exactly one for each balance the core states.** For every entry of `T°` whose balance before the trade is present, `𝒞_E^pre` names exactly one such object. Its `trader_genesis` and `trader_device_id` are `P`'s trader; `balance_key(G, DevID, policy_commit)` is the entry's key; and `H(DSM/economic-leaf-state/v1; CCB(Balance{policy_commit, amount}))` is the value the entry states before the trade. `𝒞_E^pre` names no other object of this class. +> - **A missing number is Invalid.** An entry with no object in `𝒞_E^pre`, an object that disagrees with its entry, and an object that matches no entry are each Invalid, known from the committed bytes alone. An object `𝒞_E^pre` names whose bytes are not in hand, or whose bytes do not re-derive its address, is not evaluated yet (Amendment S3): bytes that do not authenticate prove nothing. +> - **One source for every verifier.** Every verifier, the trader included, reads the trader's balances before the trade from these objects and from nothing else. An entry that states its balance as absent before the trade is absent. A relationship entry's leaf before the trade is the one its `base` names, and the fold against `T°.pre_root` proves it. The rest of `TraderSideValid` is unchanged. +> - **Not authority.** A value counts only because it hashes to the leaf the core states, and the fold proves that leaf against the trader's root. Storage holds the bytes and decides nothing. +> - **What it shows.** Anyone who reads the exercise sees the trader's balance of each token the trade moves, as it stood before the trade. SoFi's objects are public by design; the trader's other balances and history are not in the exercise. + @@ -1808,9 +1827,10 @@ A producer that receives Unavailable stops. It never publishes, exercises or adv ### 36 The unlock preimage, traced Each row names one evidence item, where it comes from, the Core check that consumes it, the verdict that check feeds, -and where its effect lands in the output. Evidence (CORE/sofi/validation.rs:228) carries three of the items as -maps: objects (policy objects by address), trader_leaves (trader leaf pre values by key) and vault_leaves (vault -leaf pre values by vault and key). +and where its effect lands in the output. Evidence (CORE/sofi/validation.rs:228) carries two of the items as +maps: objects (policy objects and the trader's TraderPreBalance objects, by address) and vault_leaves (vault +leaf pre values by vault and key). The trader's leaf pre values are derived from T° and those objects +(Amendment S12). Evidence Comes from Consumed by Feeds Lands in @@ -1839,8 +1859,8 @@ Vj◦ inside P (E) verify_batch from Rj ; clo write set; hashes to c◦V,j cessor policy objects Evidence.objects, policy checks in validate PolicyFulfillmentValid pricing and release deci- address recomputed sions in Vj◦ -trader leaf pre Evidence.trader_ compared with the entries of T ◦ TraderSideValid pre root of T ◦ -values leaves +trader leaf pre TraderPreBalance ob- compared with the entries of T ◦ TraderSideValid pre root of T ◦ +values jects CE names (S12) vault leaf pre val- Evidence.vault_leaves vault state and relationship PolicyFulfillmentValid pre root of Vj◦ ues checks pre diff --git a/specs/requirements/CONFORMANCE_GAPS.md b/specs/requirements/CONFORMANCE_GAPS.md index 37956ea51..6ebce7d37 100644 --- a/specs/requirements/CONFORMANCE_GAPS.md +++ b/specs/requirements/CONFORMANCE_GAPS.md @@ -1134,8 +1134,7 @@ Owner ruling (plan of 2026-09-25, restated 2026-09-26 — "get the real wire in" | Location | Hole | |---|---| | `dsm/src/sofi/resolve.rs` · `Verifier::chain` | What a recorded generation memoizes — that the consumption it names happened — is not re-established from the network on a later walk; the memo is anchored at the accepted genesis and linked, and that is what can be checked without walking again. The same class as the peer walker's memo start. | -| `dsm/src/economic/provenance.rs` · P15-9 | The peer walk still refuses a resolved SoFi position. The verifier that could establish one is Core's now; wiring it into the peer walk (a `Verifier` over the peer's position, with the peer's own leaves as its `NoSource`) is that round's work. | -| `dsm/src/sofi/resolve.rs` · `acquire_evidence` | Another trader's route remains unresolvable by a verifier that is not the trader (`Acquired::NoSource`): `TraderSideValid` reads the trader's leaf pre values and no section names where a foreign verifier gets them (§6.5). | +| `dsm/src/economic/provenance.rs` · P15-9 | The peer walk still refuses a resolved SoFi position. The verifier that could establish one is Core's now, and since SoFi Amendment S12 it judges another trader's route from the exercise alone (§6.40); wiring it into the peer walk (a `Verifier` over the peer's position) is that round's work. | ### 6.31 The SoFi facade reports what happened (placeholder sweep C1, `fix/sofi-facade-outcomes-classified`, 2026-09-26) @@ -1531,23 +1530,57 @@ A finding stays until it is fixed or disproved, whatever a later change touches. - **The parked dBTC policy could abort the Android library's load.** *Resolved for the load (owner ruling, 2026-09-29); the check itself belongs to the dBTC redo.* `_dsm_builtins_guard` was a load-time constructor that asserted the built-in dBTC policy bytes match their commit, and a mismatch panicked while the library loaded. It is deleted with the `ctor` dependency, and no dBTC code changed: `policy::builtins::assert_builtins_sound` stays and runs in no shipped build (its manifest row carries the dBTC scope exception). Where that integrity check belongs is part of the owner's dBTC redo. - **The committed C header is stale.** *Confirmed, remediation deferred.* `dsm_sdk/include/dsm_sdk.h`, last changed 2026-04-08, declares nine functions, none of which has a Rust definition. No C export exists since `dsm_init_runtime` was deleted, so every declaration it holds is stale. - **Three §8 rows were stale the other way (Missing, though code exists).** *Resolved (owner ruling, 2026-09-29): each traced, and its row now reads what the trace and its tests establish.* - - **MR-SOFI-0255, now Partial.** *Confirmed, remediation deferred: the close defect below.* The app router's invoke sends exactly the eight `sofi.*` methods to `handle_sofi_invoke` (`sofi_routes.rs`), which gives each its own route, and each route hands its checked intent to its producer in `sdk::sofi_flow`. A test driving all eight through the router on nodes, each to the result §27 names, with a ninth `sofi.` method refused by the router as unknown, found two defects: the relay's (fixed here) and the close's (below). It lands with the close fix. + - **MR-SOFI-0255, now Partial; Met since §6.40.** *Resolved with the close defect below.* The app router's invoke sends exactly the eight `sofi.*` methods to `handle_sofi_invoke` (`sofi_routes.rs`), which gives each its own route, and each route hands its checked intent to its producer in `sdk::sofi_flow`. A test driving all eight through the router on nodes, each to the result §27 names, with a ninth `sofi.` method refused by the router as unknown, found two defects: the relay's (fixed here) and the close's (below). It lands with the close fix. - **`sofi.relay` never carried an exercise to a leg key.** *Resolved (owner ruling, 2026-09-29): Core's held reading carries the bytes that hold the cell.* `relay_fulfillment` took the id of the value holding a leg cell and looked its bytes up by entry digest (`value_of`). An attempt cell names its holder by `E`, which is no digest of any bytes, so the lookup never matched and every relay wrote the position pair alone. A throwaway probe on nodes showed it after a realized trade: the leg cell held Final under `E`, and every carried value's entry digest differed from `E`. `evaluate` had the leader link's bytes in hand and dropped them. `CellReading::Held` now carries them (`value`), `AttemptCellRead::value` exposes them, the relay carries exactly those bytes, and `sofi_advance` reads a final root claim's bytes the same way. `value_of`, which reconstructed a value from its id, is deleted. Regressions: `a_held_attempt_cell_carries_the_exact_bytes_that_hold_it` (`E`) and `a_held_root_cell_carries_the_exact_bytes_that_hold_it` (the entry digest). On nodes, the held route test's relay of a realized two-hop position, by a device that is not the trader, wrote 2 cells before the fix and 4 after (local runs, 2026-09-29). Nothing had tested the relay before: MR-SOFI-0266 cited a test that no longer exists. -- **`sofi.close` never resolves once its vault has been traded through.** *Confirmed, remediation deferred (owner, 2026-09-29).* Reproduced on nodes in isolation: the owner sets up with its vault, a trader takes a hop through it and realizes, and the owner's `sofi.close` completes its position (`Completion::Written`), but every resolution round answers `NotYet` with `RouteEvidenceHasNoSource` for three of the owner's trader leaves, so the route reports `RetriesExhausted`. The same close of a vault nobody has traded through realizes. The eight-route test lands with the fix. +- **`sofi.close` never resolves once its vault has been traded through.** *Resolved (owner ruling, 2026-09-29): SoFi Amendment S12, §6.40.* Reproduced on nodes in isolation: the owner sets up with its vault, a trader takes a hop through it and realizes, and the owner's `sofi.close` completes its position (`Completion::Written`), but every resolution round answers `NotYet` with `RouteEvidenceHasNoSource` for three of the owner's trader leaves, so the route reports `RetriesExhausted`. The same close of a vault nobody has traded through realizes. The eight-route test lands with the fix. - **MR-STOR-0146, now Met.** A node's spool holds only what its submit route receives (`spool_insert` has that one caller). The SDK submits only through `B0xSDK::deliver`, and each of its four callers sends bytes `seal_for` made: an outer envelope with no headers carrying a `SealedEnvelopeV1`. Production seals at six places: the transfer and its evidence where `wallet.send` freezes them, the relationship-finalized certificate, the acceptance reply, the checkpoint resync, and `submit_inner`. The node checks nothing (Amendment A3), so the property is the sending device's. `a_transfer_reaches_the_nodes_only_sealed_and_arrives` reads every envelope every node holds after a transfer. - **MR-DSM-0272, now Partial.** *Confirmed, remediation deferred.* Each payload is sealed end to end under an authenticated cipher (XChaCha20-Poly1305, the message id as associated data) with a key under its own domain tag, `K = H(DSM/spool-seal/v1 ‖ shared secret ‖ message id)`, one seal kept per message id. The shared secret is not the step's: `seal_for` makes a fresh ML-KEM encapsulation to the recipient's directory key for each payload, where Amendment A7 derives the message key from the step's own Kyber encapsulation, the one that yields the step's shared secret. `dsm::crypto::spool_seal`'s module doc said a transfer seals under its step's encapsulation; it now says what the code does. - **101 code references in Met and Partial rows name no definition the map holds.** *Confirmed, remediation deferred to Stage 3.* These are module paths, shorthand for a method (`dsm::types::device_state::advance` for `DeviceState::advance`), and code since deleted (`process_online_transfer_logic`, gone since #1048). Stage 3's canonicalization resolves them, and until then they are not in the intent manifest. +### 6.40 The exercise carries the trader's balances before the trade (SoFi Amendment S12, `fix/sofi-close-after-trade`, 2026-09-29) + +**The defect (§6.39).** `TraderSideValid` recomputes each balance the trade moves from the balance before it, and `T°` states that balance only by the hash of its leaf. Only the trader held the value, so a verifier that was not the trader had no source for it (`Acquired::NoSource`). Every other reader of a vault stopped at the first trade another device made through it: its owner's close never resolved, and no device could judge another trader's route. + +**Owner ruling (2026-09-29).** The exercise carries each such balance as a content-addressed `TraderPreBalance` that `𝒞_E^pre` names, so `E` commits it (SoFi Amendment S12; MR-SOFI-0338–0341). A value counts only because it hashes to the leaf the core states. A balance the closure does not name makes the trade Invalid ("If there's no number, it's wrong"); an object the closure names but the verifier does not hold yet is waited for. Walking the trader's published history instead was considered and dropped: it reads the plaintext transition witnesses G16 records as a violation of "only the parties to a relationship hold its bytes". + +**Changed** + +| Where | What | +|---|---| +| `dsm` · ccb, domain tags, sofi/wire | Class `0x0061` `TraderPreBalance` (trader genesis, device id, policy commit, strictly positive amount), with a strict decoder, and its address namespace `DSM/sofi/trader-pre-balance-object/v1`. `derive::closure_content_address` gives a closure `ContentAddr` of that class its address, beside the three policy classes. | +| `dsm` · sofi/validation.rs | `trader_pre_balances` reads the trader's balances before the trade from those objects, for every verifier, the trader included. The closure names exactly one per balance the core states as present (`named_pre_balances`), each of `P`'s trader. A relationship entry's leaf before the trade is the one its `base` names, and the fold proves it, so `realize_root` reads nothing but `T°` and `E`. `Evidence.trader_leaves`, `Missing::TraderLeaf` and `TraderLeafPre::Relationship` are deleted. | +| `dsm` · sofi/resolve.rs, sofi/facts.rs | `acquire_evidence` takes the closure objects the exercise or the draft carries and fetches the rest by address. `Acquired::NoSource`, `NotEstablished::RouteEvidenceHasNoSource`, `Verifier.local` and `LocalLeaves::{trader_evidence, owns, pre}` are deleted. `LocalLeaves::pre_balances` builds the objects from the trader's own leaves. | +| `dsm_sdk` · sofi_sdk.rs, sofi_flow.rs, sofi_publish.rs | The draft builds the objects, names them in `𝒞_E^pre` in canonical order and carries them; the producer publishes them (`Publication::TraderPreBalance`), and the exercise carries them with the other closure objects. | + +**Also found and fixed.** `LiveSofiReads::accepted_claim_at` answered for any trader from this device's own admitted store, under the other trader's identity: a fabricated accepted claim. Nothing reached it while another trader's route stopped at `NoSource`. It now answers from the admitted store only for this device's own identity, and from the peer walk for another trader. The walk already produced the claim in `advance_validated` and kept only the root; it now keeps the claim (`ValidatedPeerTransition::accepted_claim`). + +**Evidence** + +- Core: `dsm::sofi::validation::tests::a_balance_the_closure_does_not_carry_is_invalid`, `extra_balances_in_the_closure_are_invalid`, `a_balance_keyed_to_no_stated_balance_is_invalid`, `a_balance_of_another_trader_is_invalid`, `a_balance_that_is_not_the_leaf_the_core_states_is_invalid`, `bytes_at_a_named_address_that_are_not_a_balance_are_invalid`, `a_named_balance_not_in_hand_waits`, `bytes_that_do_not_re_derive_the_named_address_prove_nothing` and `a_trade_that_does_not_debit_the_trader_is_invalid_from_the_exercise_alone`. The wire: `dsm::sofi_v8_vault_bytes::a_trader_pre_balance_matches_the_independent_encoder_and_its_frozen_address`. +- Mutation controls (2026-09-29, each restored): the count check, the trader check, the one-object-per-balance check, the address re-derivation, the pre-value comparison, the post arithmetic from the carried balance and the post-value comparison each removed or bent → a named test red. The first attempt found two checks redundant, a duplicate guard and a second pre-value comparison; both are deleted. +- On nodes: `dsm_sdk::handlers::node_e2e_tests::a_vault_traded_through_closes_for_its_owner` (the §6.39 reproduction: the owner closes after a trade and receives both reserves) and `every_sofi_route_reaches_its_producer` (all eight routes through the router; MR-SOFI-0255 now Met). + +**A lineage known invalid (owner ruling, 2026-09-29; SoFi Amendment S13).** With another trader's routes now judgeable, a trader whose lineage validation finds Invalid never yielded an accepted claim, so under Amendment S9 its setup was never evaluated and its trade held a vault key forever. Owner ruling: history not established yet waits; history valid yields the accepted claim, checked as before; history established invalid makes the setup, and so the route, Invalid, and no accepted claim is synthesized (MR-SOFI-0331 rewritten, MR-SOFI-0342). +- `SofiReads::accepted_claim_at` returns the lineage walk's result in the class the walk gave it: the accepted claim, or `PeerLineageFailure`. Core's `setup_lineage` reads it: Invalid and Quarantined (a divergent register cell, as the vault-owner check already reads it) are the verdict `SetupLineage::Invalid`; Incomplete and Unresolved establish nothing. `Evidence.accepted_claims` is `Evidence.setup_lineages`. +- `setup_valid` refuses a setup whose trader's lineage is known invalid at its position (`Invalid::SetupLineageIsInvalid`). A verdict about another trader or position establishes nothing. +- Tests: `dsm::sofi::validation::tests::a_setup_on_a_lineage_known_invalid_is_invalid`, `a_lineage_not_established_leaves_the_setup_waiting` and `an_invalid_verdict_about_another_lineage_or_position_supplies_nothing`. Mutation controls, each restored: the verdict read as waiting; a verdict about another lineage or position poisoning; a quarantine read as not established; and not-established read as invalid. Each turned its named test red. + +**Open** + +| Location | Hole | +|---|---| +| `dsm/src/economic/provenance.rs` · P15-9 | Unchanged (§6.30): the peer walk refuses a resolved SoFi position. A trader whose setup follows a SoFi position of its own has no setup claim another verifier can accept, so its later routes stay unjudgeable by others. | + ## 7 Totals | Spec | Rows | Met | Partial | Missing | Violated | Not code | Deferred | |---|---|---|---|---|---|---|---| | DSM high-level (MR-DSM) | 272 | 69 | 114 | 38 | 4 | 29 | 18 | -| SoFi (MR-SOFI) | 337 | 209 | 85 | 18 | 8 | 17 | 0 | +| SoFi (MR-SOFI) | 342 | 215 | 84 | 18 | 8 | 17 | 0 | | dBTC (MR-DBTC) | 135 | 0 | 0 | 0 | 0 | 0 | 135 | | Storage node (MR-STOR) | 158 | 39 | 38 | 60 | 2 | 18 | 1 | | Storage §14 lines added after the pin (STOR-014) | 11 | 9 | 1 | 1 | 0 | 0 | 0 | -| **All** | **913** | **326** | **238** | **117** | **14** | **64** | **154** | +| **All** | **918** | **332** | **237** | **117** | **14** | **64** | **154** | ## 8 Per-requirement results @@ -2086,7 +2119,7 @@ A finding stays until it is fixed or disproved, whatever a later change touches. | MR-SOFI-0252 | Partial | dsm · sofi/resolution.rs (stateless verdict) | n/a | Challenge/drop-claim (storage §9.1) half absent — matches 0248/0329 | | MR-SOFI-0253 | Partial | dsm_sdk · sdk/sofi_sdk.rs / sofi_advance.rs (recompute-from-storage design) | no restart/crash test found | — | | MR-SOFI-0254 | Partial | `dsm_sdk::sdk::sofi_sdk::check_draft`; `dsm_sdk::sdk::sofi_flow::exercise_draft` | — | check_draft stops the producer on anything but Valid and sofi_flow advances only through Core, but the cited producer test was deleted in #977 and no SDK test fails if the stop is removed. | -| MR-SOFI-0255 | Partial | `dsm_sdk::handlers::sofi_routes`; `dsm_sdk::handlers::app_router_impl::AppRouterImpl`; `dsm_sdk::sdk::sofi_flow::create_vault`; `dsm_sdk::sdk::sofi_flow::setup`; `dsm_sdk::sdk::sofi_flow::find_route`; `dsm_sdk::sdk::sofi_flow::trade`; `dsm_sdk::sdk::sofi_flow::close`; `dsm_sdk::sdk::sofi_flow::relay`; `dsm_sdk::sdk::sofi_flow::resolve` | `dsm_sdk::handlers::node_e2e_tests::a_sofi_trade_executes_end_to_end`; `dsm_sdk::handlers::node_e2e_tests::a_route_search_that_cannot_see_a_vault_is_an_error_not_an_empty_route`; `dsm_sdk::handlers::node_e2e_tests::a_trade_cut_short_by_a_refused_write_is_the_network_status_until_it_lands` | Traced 2026-09-29 (§6.39). The router sends exactly the eight `sofi.*` methods to their routes, and each route hands its intent to its producer (`trade` serves `sofi.trade` and `sofi.route`). createVault, setup, findRoute, trade and resolve are tested end to end; the multi-hop route and the relay ran end to end in a route test held for the close fix; `sofi.close` of a vault that has been traded through never resolves (§6.39). | +| MR-SOFI-0255 | Met | `dsm_sdk::handlers::sofi_routes`; `dsm_sdk::handlers::app_router_impl::AppRouterImpl`; `dsm_sdk::sdk::sofi_flow::create_vault`; `dsm_sdk::sdk::sofi_flow::setup`; `dsm_sdk::sdk::sofi_flow::find_route`; `dsm_sdk::sdk::sofi_flow::trade`; `dsm_sdk::sdk::sofi_flow::close`; `dsm_sdk::sdk::sofi_flow::relay`; `dsm_sdk::sdk::sofi_flow::resolve` | `dsm_sdk::handlers::node_e2e_tests::every_sofi_route_reaches_its_producer`; `dsm_sdk::handlers::node_e2e_tests::a_vault_traded_through_closes_for_its_owner`; `dsm_sdk::handlers::node_e2e_tests::a_sofi_trade_executes_end_to_end`; `dsm_sdk::handlers::node_e2e_tests::a_route_search_that_cannot_see_a_vault_is_an_error_not_an_empty_route`; `dsm_sdk::handlers::node_e2e_tests::a_trade_cut_short_by_a_refused_write_is_the_network_status_until_it_lands` | Traced 2026-09-29 (§6.39). The router sends exactly the eight `sofi.*` methods to their routes, and each route hands its intent to its producer (`trade` serves `sofi.trade` and `sofi.route`). All eight run end to end on nodes, each to the result §27 names, and a ninth `sofi.` method is refused by the router; the close of a vault that has been traded through resolves since SoFi Amendment S12 (§6.40). | | MR-SOFI-0256 | Met | `dsm_sdk::sdk::sofi_flow::find_route`; `dsm_sdk::sdk::sofi_flow::quote` | `dsm_sdk::handlers::node_e2e_tests::a_route_search_that_cannot_see_a_vault_is_an_error_not_an_empty_route` | The search (`sofi.findRoute`, handlers/sofi_routes.rs) quotes hops at walked heads and carries nothing: a trade names its own hops and Core prices them again in `exercise_draft`. A vault whose head cannot be established is an error, not an empty route (§6.31). | | MR-SOFI-0257 | Partial | dsm_sdk · sdk/sofi_sdk.rs · `build_vault_create`; dsm · sofi/lineage.rs · `genesis_root`, `vault_leaves_at_genesis` | `a_vault_creation_signs_the_operations_own_bytes`; `the_genesis_root_holds_only_the_state_leaf` | The vault-create producer is tested but has no production entry (§3 G1). | | MR-SOFI-0258 | Partial | dsm_sdk · sdk/sofi_sdk.rs · `build_setup` | `a_setup_signs_its_object_and_not_the_operation` | Producer tested, no route | @@ -2162,13 +2195,18 @@ A finding stays until it is fixed or disproved, whatever a later change touches. | MR-SOFI-0328 | Missing | dsm · sofi/arith.rs · `resolve` (implements old leader/quorum model only, no route-chain / two-further-valid-links semantics) | leader_held_settles_the_race_before_finality (tests the OLD model) | S4 amendment (2026-09-22) mechanism not built | | MR-SOFI-0329 | Missing | dsm · sofi/resolution.rs · `resolve_position` (no rung 3a / drop-claim arm) | — | S5 amendment (2026-09-22) mechanism not built | | MR-SOFI-0330 | Partial | `dsm::sofi::registration::check_fulfillment_completion`; `dsm::sofi::exercise::check_attempt_completion`; `dsm_sdk::sdk::route_seats::keep_completion` | `dsm::sofi::registration::tests::a_final_fulfillment_has_a_completion_proof_that_checks`; `dsm::sofi::exercise::tests::a_final_exercise_has_a_completion_proof_that_checks` | Core builds and checks completion proofs and the SDK keeps the exercise's, but no unlock checks a proof: both checks are called only from tests (the G1 baseline lists them), and the proofs at K_ful and K_root are not kept. | -| MR-SOFI-0331 | Met | `dsm::sofi::validation::setup_valid`; `dsm::sofi::validation::Evidence` | `dsm::sofi::validation::tests::a_setup_naming_another_claim_than_the_accepted_one_is_invalid`; `dsm::sofi::validation::tests::a_setup_whose_accepted_claim_is_not_in_hand_is_missing` | Evidence carries the accepted claims lineage validation produced; a setup naming another claim is Invalid, and one whose accepted claim is not in hand is Missing. | +| MR-SOFI-0331 | Met | `dsm::sofi::validation::setup_valid`; `dsm::sofi::validation::setup_lineage`; `dsm::sofi::validation::Evidence`; `dsm::sofi::resolve::Verifier::acquire_evidence` | `dsm::sofi::validation::tests::a_setup_naming_another_claim_than_the_accepted_one_is_invalid`; `dsm::sofi::validation::tests::a_setup_whose_accepted_claim_is_not_in_hand_is_missing`; `dsm::sofi::validation::tests::a_setup_on_a_lineage_known_invalid_is_invalid`; `dsm::sofi::validation::tests::a_lineage_not_established_leaves_the_setup_waiting` | Amendments S9 and S13 (§6.40): evidence carries what lineage validation established at each setup's position, the accepted claim or the verdict that the lineage is invalid. A setup naming another claim is Invalid; one on a lineage known invalid (a quarantined cell included) is Invalid; one whose lineage is not established yet is Missing. | | MR-SOFI-0332 | Met | `dsm::economic::token_policy::TokenPolicy`; `dsm::economic::provenance::verify_genesis_release` | `dsm::economic_provenance_semantics::a_genesis_release_funds_its_creators_whole_supply`; `dsm::economic_provenance_semantics::a_genesis_release_of_another_creators_policy_is_refused` | The policy blob carries creator_genesis and creator_device_id; a genesis release of another creator's policy is refused. | | MR-SOFI-0333 | Met | `dsm::economic::keys::token_creation_key`; `dsm::economic::write_set::build_write_set` | `dsm::economic_write_set::a_token_is_created_once_on_its_creators_lineage`; `dsm::sofi::lineage::tests::the_creation_record_is_an_economic_leaf_with_its_own_key` | Creation inserts the 0x0060 record under its own key from zero; a second creation of the same commit cannot build its write set. | | MR-SOFI-0334 | Met | `dsm::economic::token_policy::parse_token_policy_blob`; `dsm::economic::token_policy::parse_token_policy`; `dsm_sdk::handlers::token_routes::build_policy_v3_bytes` | `dsm::economic::token_policy::tests::a_network_anchored_blob_names_no_creator_and_no_signer_set`; `dsm::economic::token_policy::tests::a_blob_whose_shape_does_not_match_its_release_rule_does_not_parse`; `dsm::economic::token_policy::tests::a_non_canonical_wrapper_does_not_parse`; `dsm_sdk::handlers::token_routes::tests::a_device_created_policy_keeps_its_layout_and_commitment` | The release rule decides the blob's shape; a device-created policy's bytes and commitment are unchanged (pinned before and after); one policy has one commitment (§6.33). | | MR-SOFI-0335 | Met | `dsm::core::token::era_policy::era_policy_commit`; `dsm::core::token::era_policy::era_policy`; `dsm::core::token::policy::TokenPolicySystem::policy_at` | `dsm::core::token::era_policy::tests::eras_commitment_is_derived_from_its_bytes_and_is_the_specifications`; `dsm::core::token::era_policy::tests::eras_policy_states_what_the_specification_fixes`; `dsm::core::token::policy::tests::eras_policy_is_answered_from_cores_bytes_never_cached_or_resolved`; `dsm_sdk::handlers::token_routes::tests::the_one_packer_reproduces_eras_policy_bytes` | ERA's commitment is derived from its 40 bytes in Core and equals the specification's check value; the enforcer answers it from those bytes, never from the cache or a store (§6.33). | | MR-SOFI-0336 | Met | `dsm::core::token::policy::TokenPolicySystem::register_policy`; `dsm_sdk::handlers::token_routes::adoptable_policy`; `dsm::economic::provenance::verify_genesis_release`; `dsm::economic::native_reserve::era_reserve_id` | `dsm::core::token::policy::tests::eras_own_bytes_are_never_registered`; `dsm_sdk::handlers::token_routes::tests::a_network_anchored_lookalike_is_neither_adopted_nor_published`; `dsm::economic_provenance_semantics::a_genesis_release_needs_the_all_at_creation_rule`; `dsm::economic_provenance_semantics::a_creation_naming_eras_commitment_is_refused_from_eras_own_policy` | No network-anchored policy is registered, adopted or published; none releases at creation; the only reserve is derived from ERA's commitment (§6.33). | | MR-SOFI-0337 | Missing | — | — | Amendment S11 (2026-09-26): the reserve's supply from ERA's policy and exhaustion refused before signing, built in this PR. | +| MR-SOFI-0338 | Met | `dsm::sofi::wire::objects::TraderPreBalance`; `dsm::sofi::derive::trader_pre_balance_addr`; `dsm::sofi::derive::closure_content_address` | `dsm::sofi_v8_vault_bytes::a_trader_pre_balance_matches_the_independent_encoder_and_its_frozen_address` | Amendment S12 (2026-09-29, §6.40): the bytes, the address and the leaf value agree with the independent encoder and are frozen; zero, the wrong class, truncation and trailing bytes have no reading. | +| MR-SOFI-0339 | Met | `dsm::sofi::validation::trader_pre_balances`; `dsm::sofi::validation::named_pre_balances`; `dsm::sofi::validation::check_trader_balances`; `dsm_sdk::sdk::sofi_sdk::draft_route`; `dsm_sdk::sdk::sofi_sdk::draft_close` | `dsm::sofi::validation::tests::a_balance_the_closure_does_not_carry_is_invalid`; `dsm::sofi::validation::tests::extra_balances_in_the_closure_are_invalid`; `dsm::sofi::validation::tests::a_balance_keyed_to_no_stated_balance_is_invalid`; `dsm::sofi::validation::tests::a_balance_of_another_trader_is_invalid`; `dsm::sofi::validation::tests::a_balance_that_is_not_the_leaf_the_core_states_is_invalid` | Amendment S12 (§6.40). The producer names one object per balance its core states as present, in canonical order. | +| MR-SOFI-0340 | Met | `dsm::sofi::validation::trader_pre_balances`; `dsm::sofi::validation::Evidence::pre_balance` | `dsm::sofi::validation::tests::a_balance_the_closure_does_not_carry_is_invalid`; `dsm::sofi::validation::tests::bytes_at_a_named_address_that_are_not_a_balance_are_invalid`; `dsm::sofi::validation::tests::a_named_balance_not_in_hand_waits`; `dsm::sofi::validation::tests::bytes_that_do_not_re_derive_the_named_address_prove_nothing` | Amendment S12 (§6.40): a missing number is Invalid in hand (`route_invalid_in_hand`); a named object not in hand, or not re-deriving its address, is Unavailable. | +| MR-SOFI-0341 | Met | `dsm::sofi::validation::trader_pre_balances`; `dsm::sofi::validation::realize_root`; `dsm::sofi::resolve::Verifier::acquire_evidence`; `dsm_sdk::sdk::sofi_reads::LiveSofiReads` | `dsm::sofi::validation::tests::a_trade_that_does_not_debit_the_trader_is_invalid_from_the_exercise_alone`; `dsm_sdk::handlers::node_e2e_tests::a_vault_traded_through_closes_for_its_owner`; `dsm_sdk::handlers::node_e2e_tests::every_sofi_route_reaches_its_producer` | Amendment S12 (§6.40): one path for every verifier; the trader's own route is judged from the same objects. | +| MR-SOFI-0342 | Met | `dsm::sofi::validation::setup_lineage`; `dsm::sofi::validation::InvalidLineage`; `dsm::sofi::validation::setup_valid` | `dsm::sofi::validation::tests::a_setup_on_a_lineage_known_invalid_is_invalid`; `dsm::sofi::validation::tests::an_invalid_verdict_about_another_lineage_or_position_supplies_nothing`; `dsm::sofi::validation::tests::a_lineage_not_established_leaves_the_setup_waiting` | Amendment S13 (§6.40): no accepted claim is synthesized; the verdict of lineage validation is the negative fact, and a verdict about another trader or position establishes nothing. | ### 8.3 dBTC native specification diff --git a/specs/requirements/INTENT_MANIFEST.tsv b/specs/requirements/INTENT_MANIFEST.tsv index cd8c0cbd1..0a7c9e24d 100644 --- a/specs/requirements/INTENT_MANIFEST.tsv +++ b/specs/requirements/INTENT_MANIFEST.tsv @@ -394,14 +394,14 @@ MR-SOFI-0250 dsm::sofi::resolution::effect_of android MUST_REACH active dsm_sdk: MR-SOFI-0251 dsm::sofi::lineage::advance_resolved android MUST_REACH active dsm_sdk::jni::unified_protobuf_bridge::Java_com_dsm_wallet_bridge_UnifiedNativeApi_dispatchIngress dsm::sofi::lineage::tests::advance_is_refused_on_each_missing_conjunct - CONFORMANCE §8 MR-SOFI-0251 (Met) MR-SOFI-0254 dsm_sdk::sdk::sofi_flow::exercise_draft android MUST_REACH active dsm_sdk::jni::unified_protobuf_bridge::Java_com_dsm_wallet_bridge_UnifiedNativeApi_dispatchIngress - - CONFORMANCE §8 MR-SOFI-0254 (Partial) MR-SOFI-0254 dsm_sdk::sdk::sofi_sdk::check_draft android MUST_REACH active dsm_sdk::jni::unified_protobuf_bridge::Java_com_dsm_wallet_bridge_UnifiedNativeApi_dispatchIngress - - CONFORMANCE §8 MR-SOFI-0254 (Partial) -MR-SOFI-0255 dsm_sdk::handlers::sofi_routes::AppRouterImpl::handle_sofi_invoke android MUST_REACH active dsm_sdk::jni::unified_protobuf_bridge::Java_com_dsm_wallet_bridge_UnifiedNativeApi_dispatchIngress dsm_sdk::handlers::node_e2e_tests::a_sofi_trade_executes_end_to_end;dsm_sdk::handlers::node_e2e_tests::a_route_search_that_cannot_see_a_vault_is_an_error_not_an_empty_route;dsm_sdk::handlers::node_e2e_tests::a_trade_cut_short_by_a_refused_write_is_the_network_status_until_it_lands - CONFORMANCE §8 MR-SOFI-0255 (Partial) -MR-SOFI-0255 dsm_sdk::sdk::sofi_flow::create_vault android MUST_REACH active dsm_sdk::jni::unified_protobuf_bridge::Java_com_dsm_wallet_bridge_UnifiedNativeApi_dispatchIngress dsm_sdk::handlers::node_e2e_tests::a_sofi_trade_executes_end_to_end;dsm_sdk::handlers::node_e2e_tests::a_route_search_that_cannot_see_a_vault_is_an_error_not_an_empty_route;dsm_sdk::handlers::node_e2e_tests::a_trade_cut_short_by_a_refused_write_is_the_network_status_until_it_lands - CONFORMANCE §8 MR-SOFI-0255 (Partial) -MR-SOFI-0255 dsm_sdk::sdk::sofi_flow::setup android MUST_REACH active dsm_sdk::jni::unified_protobuf_bridge::Java_com_dsm_wallet_bridge_UnifiedNativeApi_dispatchIngress dsm_sdk::handlers::node_e2e_tests::a_sofi_trade_executes_end_to_end;dsm_sdk::handlers::node_e2e_tests::a_route_search_that_cannot_see_a_vault_is_an_error_not_an_empty_route;dsm_sdk::handlers::node_e2e_tests::a_trade_cut_short_by_a_refused_write_is_the_network_status_until_it_lands - CONFORMANCE §8 MR-SOFI-0255 (Partial) -MR-SOFI-0255 dsm_sdk::sdk::sofi_flow::find_route android MUST_REACH active dsm_sdk::jni::unified_protobuf_bridge::Java_com_dsm_wallet_bridge_UnifiedNativeApi_dispatchIngress dsm_sdk::handlers::node_e2e_tests::a_sofi_trade_executes_end_to_end;dsm_sdk::handlers::node_e2e_tests::a_route_search_that_cannot_see_a_vault_is_an_error_not_an_empty_route;dsm_sdk::handlers::node_e2e_tests::a_trade_cut_short_by_a_refused_write_is_the_network_status_until_it_lands - CONFORMANCE §8 MR-SOFI-0255 (Partial) -MR-SOFI-0255 dsm_sdk::sdk::sofi_flow::trade android MUST_REACH active dsm_sdk::jni::unified_protobuf_bridge::Java_com_dsm_wallet_bridge_UnifiedNativeApi_dispatchIngress dsm_sdk::handlers::node_e2e_tests::a_sofi_trade_executes_end_to_end;dsm_sdk::handlers::node_e2e_tests::a_route_search_that_cannot_see_a_vault_is_an_error_not_an_empty_route;dsm_sdk::handlers::node_e2e_tests::a_trade_cut_short_by_a_refused_write_is_the_network_status_until_it_lands - CONFORMANCE §8 MR-SOFI-0255 (Partial) -MR-SOFI-0255 dsm_sdk::sdk::sofi_flow::close android MUST_REACH active dsm_sdk::jni::unified_protobuf_bridge::Java_com_dsm_wallet_bridge_UnifiedNativeApi_dispatchIngress dsm_sdk::handlers::node_e2e_tests::a_sofi_trade_executes_end_to_end;dsm_sdk::handlers::node_e2e_tests::a_route_search_that_cannot_see_a_vault_is_an_error_not_an_empty_route;dsm_sdk::handlers::node_e2e_tests::a_trade_cut_short_by_a_refused_write_is_the_network_status_until_it_lands - CONFORMANCE §8 MR-SOFI-0255 (Partial) -MR-SOFI-0255 dsm_sdk::sdk::sofi_flow::relay android MUST_REACH active dsm_sdk::jni::unified_protobuf_bridge::Java_com_dsm_wallet_bridge_UnifiedNativeApi_dispatchIngress dsm_sdk::handlers::node_e2e_tests::a_sofi_trade_executes_end_to_end;dsm_sdk::handlers::node_e2e_tests::a_route_search_that_cannot_see_a_vault_is_an_error_not_an_empty_route;dsm_sdk::handlers::node_e2e_tests::a_trade_cut_short_by_a_refused_write_is_the_network_status_until_it_lands - CONFORMANCE §8 MR-SOFI-0255 (Partial) -MR-SOFI-0255 dsm_sdk::sdk::sofi_flow::resolve android MUST_REACH active dsm_sdk::jni::unified_protobuf_bridge::Java_com_dsm_wallet_bridge_UnifiedNativeApi_dispatchIngress dsm_sdk::handlers::node_e2e_tests::a_sofi_trade_executes_end_to_end;dsm_sdk::handlers::node_e2e_tests::a_route_search_that_cannot_see_a_vault_is_an_error_not_an_empty_route;dsm_sdk::handlers::node_e2e_tests::a_trade_cut_short_by_a_refused_write_is_the_network_status_until_it_lands - CONFORMANCE §8 MR-SOFI-0255 (Partial) +MR-SOFI-0255 dsm_sdk::handlers::sofi_routes::AppRouterImpl::handle_sofi_invoke android MUST_REACH active dsm_sdk::jni::unified_protobuf_bridge::Java_com_dsm_wallet_bridge_UnifiedNativeApi_dispatchIngress dsm_sdk::handlers::node_e2e_tests::every_sofi_route_reaches_its_producer;dsm_sdk::handlers::node_e2e_tests::a_vault_traded_through_closes_for_its_owner;dsm_sdk::handlers::node_e2e_tests::a_sofi_trade_executes_end_to_end;dsm_sdk::handlers::node_e2e_tests::a_route_search_that_cannot_see_a_vault_is_an_error_not_an_empty_route;dsm_sdk::handlers::node_e2e_tests::a_trade_cut_short_by_a_refused_write_is_the_network_status_until_it_lands - CONFORMANCE §8 MR-SOFI-0255 (Met) +MR-SOFI-0255 dsm_sdk::sdk::sofi_flow::create_vault android MUST_REACH active dsm_sdk::jni::unified_protobuf_bridge::Java_com_dsm_wallet_bridge_UnifiedNativeApi_dispatchIngress dsm_sdk::handlers::node_e2e_tests::every_sofi_route_reaches_its_producer;dsm_sdk::handlers::node_e2e_tests::a_vault_traded_through_closes_for_its_owner;dsm_sdk::handlers::node_e2e_tests::a_sofi_trade_executes_end_to_end;dsm_sdk::handlers::node_e2e_tests::a_route_search_that_cannot_see_a_vault_is_an_error_not_an_empty_route;dsm_sdk::handlers::node_e2e_tests::a_trade_cut_short_by_a_refused_write_is_the_network_status_until_it_lands - CONFORMANCE §8 MR-SOFI-0255 (Met) +MR-SOFI-0255 dsm_sdk::sdk::sofi_flow::setup android MUST_REACH active dsm_sdk::jni::unified_protobuf_bridge::Java_com_dsm_wallet_bridge_UnifiedNativeApi_dispatchIngress dsm_sdk::handlers::node_e2e_tests::every_sofi_route_reaches_its_producer;dsm_sdk::handlers::node_e2e_tests::a_vault_traded_through_closes_for_its_owner;dsm_sdk::handlers::node_e2e_tests::a_sofi_trade_executes_end_to_end;dsm_sdk::handlers::node_e2e_tests::a_route_search_that_cannot_see_a_vault_is_an_error_not_an_empty_route;dsm_sdk::handlers::node_e2e_tests::a_trade_cut_short_by_a_refused_write_is_the_network_status_until_it_lands - CONFORMANCE §8 MR-SOFI-0255 (Met) +MR-SOFI-0255 dsm_sdk::sdk::sofi_flow::find_route android MUST_REACH active dsm_sdk::jni::unified_protobuf_bridge::Java_com_dsm_wallet_bridge_UnifiedNativeApi_dispatchIngress dsm_sdk::handlers::node_e2e_tests::every_sofi_route_reaches_its_producer;dsm_sdk::handlers::node_e2e_tests::a_vault_traded_through_closes_for_its_owner;dsm_sdk::handlers::node_e2e_tests::a_sofi_trade_executes_end_to_end;dsm_sdk::handlers::node_e2e_tests::a_route_search_that_cannot_see_a_vault_is_an_error_not_an_empty_route;dsm_sdk::handlers::node_e2e_tests::a_trade_cut_short_by_a_refused_write_is_the_network_status_until_it_lands - CONFORMANCE §8 MR-SOFI-0255 (Met) +MR-SOFI-0255 dsm_sdk::sdk::sofi_flow::trade android MUST_REACH active dsm_sdk::jni::unified_protobuf_bridge::Java_com_dsm_wallet_bridge_UnifiedNativeApi_dispatchIngress dsm_sdk::handlers::node_e2e_tests::every_sofi_route_reaches_its_producer;dsm_sdk::handlers::node_e2e_tests::a_vault_traded_through_closes_for_its_owner;dsm_sdk::handlers::node_e2e_tests::a_sofi_trade_executes_end_to_end;dsm_sdk::handlers::node_e2e_tests::a_route_search_that_cannot_see_a_vault_is_an_error_not_an_empty_route;dsm_sdk::handlers::node_e2e_tests::a_trade_cut_short_by_a_refused_write_is_the_network_status_until_it_lands - CONFORMANCE §8 MR-SOFI-0255 (Met) +MR-SOFI-0255 dsm_sdk::sdk::sofi_flow::close android MUST_REACH active dsm_sdk::jni::unified_protobuf_bridge::Java_com_dsm_wallet_bridge_UnifiedNativeApi_dispatchIngress dsm_sdk::handlers::node_e2e_tests::every_sofi_route_reaches_its_producer;dsm_sdk::handlers::node_e2e_tests::a_vault_traded_through_closes_for_its_owner;dsm_sdk::handlers::node_e2e_tests::a_sofi_trade_executes_end_to_end;dsm_sdk::handlers::node_e2e_tests::a_route_search_that_cannot_see_a_vault_is_an_error_not_an_empty_route;dsm_sdk::handlers::node_e2e_tests::a_trade_cut_short_by_a_refused_write_is_the_network_status_until_it_lands - CONFORMANCE §8 MR-SOFI-0255 (Met) +MR-SOFI-0255 dsm_sdk::sdk::sofi_flow::relay android MUST_REACH active dsm_sdk::jni::unified_protobuf_bridge::Java_com_dsm_wallet_bridge_UnifiedNativeApi_dispatchIngress dsm_sdk::handlers::node_e2e_tests::every_sofi_route_reaches_its_producer;dsm_sdk::handlers::node_e2e_tests::a_vault_traded_through_closes_for_its_owner;dsm_sdk::handlers::node_e2e_tests::a_sofi_trade_executes_end_to_end;dsm_sdk::handlers::node_e2e_tests::a_route_search_that_cannot_see_a_vault_is_an_error_not_an_empty_route;dsm_sdk::handlers::node_e2e_tests::a_trade_cut_short_by_a_refused_write_is_the_network_status_until_it_lands - CONFORMANCE §8 MR-SOFI-0255 (Met) +MR-SOFI-0255 dsm_sdk::sdk::sofi_flow::resolve android MUST_REACH active dsm_sdk::jni::unified_protobuf_bridge::Java_com_dsm_wallet_bridge_UnifiedNativeApi_dispatchIngress dsm_sdk::handlers::node_e2e_tests::every_sofi_route_reaches_its_producer;dsm_sdk::handlers::node_e2e_tests::a_vault_traded_through_closes_for_its_owner;dsm_sdk::handlers::node_e2e_tests::a_sofi_trade_executes_end_to_end;dsm_sdk::handlers::node_e2e_tests::a_route_search_that_cannot_see_a_vault_is_an_error_not_an_empty_route;dsm_sdk::handlers::node_e2e_tests::a_trade_cut_short_by_a_refused_write_is_the_network_status_until_it_lands - CONFORMANCE §8 MR-SOFI-0255 (Met) MR-SOFI-0256 dsm_sdk::sdk::sofi_flow::find_route android MUST_REACH active dsm_sdk::jni::unified_protobuf_bridge::Java_com_dsm_wallet_bridge_UnifiedNativeApi_dispatchIngress dsm_sdk::handlers::node_e2e_tests::a_route_search_that_cannot_see_a_vault_is_an_error_not_an_empty_route - CONFORMANCE §8 MR-SOFI-0256 (Met) MR-SOFI-0256 dsm_sdk::sdk::sofi_flow::quote android MUST_REACH active dsm_sdk::jni::unified_protobuf_bridge::Java_com_dsm_wallet_bridge_UnifiedNativeApi_dispatchIngress dsm_sdk::handlers::node_e2e_tests::a_route_search_that_cannot_see_a_vault_is_an_error_not_an_empty_route - CONFORMANCE §8 MR-SOFI-0256 (Met) MR-SOFI-0260 dsm_sdk::sdk::sofi_sdk::build_fulfillment android MUST_REACH active dsm_sdk::jni::unified_protobuf_bridge::Java_com_dsm_wallet_bridge_UnifiedNativeApi_dispatchIngress - - CONFORMANCE §8 MR-SOFI-0260 (Partial) @@ -472,8 +472,8 @@ MR-SOFI-0322 dsm::economic::provenance::verify_genesis_release android MUST_REAC MR-SOFI-0330 dsm::sofi::exercise::check_attempt_completion android MUST_REACH active - dsm::sofi::registration::tests::a_final_fulfillment_has_a_completion_proof_that_checks;dsm::sofi::exercise::tests::a_final_exercise_has_a_completion_proof_that_checks - CONFORMANCE §8 MR-SOFI-0330 (Partial) MR-SOFI-0330 dsm::sofi::registration::check_fulfillment_completion android MUST_REACH active - dsm::sofi::registration::tests::a_final_fulfillment_has_a_completion_proof_that_checks;dsm::sofi::exercise::tests::a_final_exercise_has_a_completion_proof_that_checks - CONFORMANCE §8 MR-SOFI-0330 (Partial) MR-SOFI-0330 dsm_sdk::sdk::route_seats::keep_completion android MUST_REACH active dsm_sdk::jni::unified_protobuf_bridge::Java_com_dsm_wallet_bridge_UnifiedNativeApi_dispatchIngress dsm::sofi::registration::tests::a_final_fulfillment_has_a_completion_proof_that_checks;dsm::sofi::exercise::tests::a_final_exercise_has_a_completion_proof_that_checks - CONFORMANCE §8 MR-SOFI-0330 (Partial) -MR-SOFI-0331 dsm::sofi::validation::Evidence android MUST_REACH active dsm_sdk::jni::unified_protobuf_bridge::Java_com_dsm_wallet_bridge_UnifiedNativeApi_dispatchIngress dsm::sofi::validation::tests::a_setup_naming_another_claim_than_the_accepted_one_is_invalid;dsm::sofi::validation::tests::a_setup_whose_accepted_claim_is_not_in_hand_is_missing - CONFORMANCE §8 MR-SOFI-0331 (Met) -MR-SOFI-0331 dsm::sofi::validation::setup_valid android MUST_REACH active dsm_sdk::jni::unified_protobuf_bridge::Java_com_dsm_wallet_bridge_UnifiedNativeApi_dispatchIngress dsm::sofi::validation::tests::a_setup_naming_another_claim_than_the_accepted_one_is_invalid;dsm::sofi::validation::tests::a_setup_whose_accepted_claim_is_not_in_hand_is_missing - CONFORMANCE §8 MR-SOFI-0331 (Met) +MR-SOFI-0331 dsm::sofi::validation::Evidence android MUST_REACH active dsm_sdk::jni::unified_protobuf_bridge::Java_com_dsm_wallet_bridge_UnifiedNativeApi_dispatchIngress dsm::sofi::validation::tests::a_setup_naming_another_claim_than_the_accepted_one_is_invalid;dsm::sofi::validation::tests::a_setup_whose_accepted_claim_is_not_in_hand_is_missing;dsm::sofi::validation::tests::a_setup_on_a_lineage_known_invalid_is_invalid;dsm::sofi::validation::tests::a_lineage_not_established_leaves_the_setup_waiting - CONFORMANCE §8 MR-SOFI-0331 (Met) +MR-SOFI-0331 dsm::sofi::validation::setup_valid android MUST_REACH active dsm_sdk::jni::unified_protobuf_bridge::Java_com_dsm_wallet_bridge_UnifiedNativeApi_dispatchIngress dsm::sofi::validation::tests::a_setup_naming_another_claim_than_the_accepted_one_is_invalid;dsm::sofi::validation::tests::a_setup_whose_accepted_claim_is_not_in_hand_is_missing;dsm::sofi::validation::tests::a_setup_on_a_lineage_known_invalid_is_invalid;dsm::sofi::validation::tests::a_lineage_not_established_leaves_the_setup_waiting - CONFORMANCE §8 MR-SOFI-0331 (Met) MR-SOFI-0332 dsm::economic::provenance::verify_genesis_release android MUST_REACH active dsm_sdk::jni::unified_protobuf_bridge::Java_com_dsm_wallet_bridge_UnifiedNativeApi_dispatchIngress dsm::economic_provenance_semantics::a_genesis_release_funds_its_creators_whole_supply;dsm::economic_provenance_semantics::a_genesis_release_of_another_creators_policy_is_refused - CONFORMANCE §8 MR-SOFI-0332 (Met) MR-SOFI-0332 dsm::economic::token_policy::TokenPolicy android MUST_REACH active dsm_sdk::jni::unified_protobuf_bridge::Java_com_dsm_wallet_bridge_UnifiedNativeApi_dispatchIngress dsm::economic_provenance_semantics::a_genesis_release_funds_its_creators_whole_supply;dsm::economic_provenance_semantics::a_genesis_release_of_another_creators_policy_is_refused - CONFORMANCE §8 MR-SOFI-0332 (Met) MR-SOFI-0333 dsm::economic::keys::token_creation_key android MUST_REACH active dsm_sdk::jni::unified_protobuf_bridge::Java_com_dsm_wallet_bridge_UnifiedNativeApi_dispatchIngress dsm::economic_write_set::a_token_is_created_once_on_its_creators_lineage;dsm::sofi::lineage::tests::the_creation_record_is_an_economic_leaf_with_its_own_key - CONFORMANCE §8 MR-SOFI-0333 (Met) @@ -488,6 +488,16 @@ MR-SOFI-0336 dsm::core::token::policy::TokenPolicySystem::register_policy androi MR-SOFI-0336 dsm::economic::native_reserve::era_reserve_id android MUST_REACH active dsm_sdk::jni::unified_protobuf_bridge::Java_com_dsm_wallet_bridge_UnifiedNativeApi_dispatchIngress dsm::core::token::policy::tests::eras_own_bytes_are_never_registered;dsm_sdk::handlers::token_routes::tests::a_network_anchored_lookalike_is_neither_adopted_nor_published;dsm::economic_provenance_semantics::a_genesis_release_needs_the_all_at_creation_rule;dsm::economic_provenance_semantics::a_creation_naming_eras_commitment_is_refused_from_eras_own_policy - CONFORMANCE §8 MR-SOFI-0336 (Met) MR-SOFI-0336 dsm::economic::provenance::verify_genesis_release android MUST_REACH active dsm_sdk::jni::unified_protobuf_bridge::Java_com_dsm_wallet_bridge_UnifiedNativeApi_dispatchIngress dsm::core::token::policy::tests::eras_own_bytes_are_never_registered;dsm_sdk::handlers::token_routes::tests::a_network_anchored_lookalike_is_neither_adopted_nor_published;dsm::economic_provenance_semantics::a_genesis_release_needs_the_all_at_creation_rule;dsm::economic_provenance_semantics::a_creation_naming_eras_commitment_is_refused_from_eras_own_policy - CONFORMANCE §8 MR-SOFI-0336 (Met) MR-SOFI-0336 dsm_sdk::handlers::token_routes::adoptable_policy android MUST_REACH active dsm_sdk::jni::unified_protobuf_bridge::Java_com_dsm_wallet_bridge_UnifiedNativeApi_dispatchIngress dsm::core::token::policy::tests::eras_own_bytes_are_never_registered;dsm_sdk::handlers::token_routes::tests::a_network_anchored_lookalike_is_neither_adopted_nor_published;dsm::economic_provenance_semantics::a_genesis_release_needs_the_all_at_creation_rule;dsm::economic_provenance_semantics::a_creation_naming_eras_commitment_is_refused_from_eras_own_policy - CONFORMANCE §8 MR-SOFI-0336 (Met) +MR-SOFI-0338 dsm::sofi::wire::objects::TraderPreBalance android MUST_REACH active dsm_sdk::jni::unified_protobuf_bridge::Java_com_dsm_wallet_bridge_UnifiedNativeApi_dispatchIngress dsm::sofi_v8_vault_bytes::a_trader_pre_balance_matches_the_independent_encoder_and_its_frozen_address - CONFORMANCE §8 MR-SOFI-0338 (Met) +MR-SOFI-0338 dsm::sofi::derive::closure_content_address android MUST_REACH active dsm_sdk::jni::unified_protobuf_bridge::Java_com_dsm_wallet_bridge_UnifiedNativeApi_dispatchIngress dsm::sofi_v8_vault_bytes::a_trader_pre_balance_matches_the_independent_encoder_and_its_frozen_address;dsm::sofi::validation::tests::bytes_that_do_not_re_derive_the_named_address_prove_nothing - CONFORMANCE §8 MR-SOFI-0338 (Met) +MR-SOFI-0339 dsm::sofi::validation::trader_pre_balances android MUST_REACH active dsm_sdk::jni::unified_protobuf_bridge::Java_com_dsm_wallet_bridge_UnifiedNativeApi_dispatchIngress dsm::sofi::validation::tests::a_balance_the_closure_does_not_carry_is_invalid;dsm::sofi::validation::tests::extra_balances_in_the_closure_are_invalid;dsm::sofi::validation::tests::a_balance_keyed_to_no_stated_balance_is_invalid;dsm::sofi::validation::tests::a_balance_of_another_trader_is_invalid;dsm::sofi::validation::tests::a_balance_that_is_not_the_leaf_the_core_states_is_invalid - CONFORMANCE §8 MR-SOFI-0339 (Met) +MR-SOFI-0339 dsm_sdk::sdk::sofi_sdk::draft_route android MUST_REACH active dsm_sdk::jni::unified_protobuf_bridge::Java_com_dsm_wallet_bridge_UnifiedNativeApi_dispatchIngress dsm_sdk::handlers::node_e2e_tests::every_sofi_route_reaches_its_producer;dsm_sdk::handlers::node_e2e_tests::a_vault_traded_through_closes_for_its_owner - CONFORMANCE §8 MR-SOFI-0339 (Met) +MR-SOFI-0339 dsm_sdk::sdk::sofi_sdk::draft_close android MUST_REACH active dsm_sdk::jni::unified_protobuf_bridge::Java_com_dsm_wallet_bridge_UnifiedNativeApi_dispatchIngress dsm_sdk::handlers::node_e2e_tests::every_sofi_route_reaches_its_producer;dsm_sdk::handlers::node_e2e_tests::a_vault_traded_through_closes_for_its_owner - CONFORMANCE §8 MR-SOFI-0339 (Met) +MR-SOFI-0340 dsm::sofi::validation::Evidence::pre_balance android MUST_REACH active dsm_sdk::jni::unified_protobuf_bridge::Java_com_dsm_wallet_bridge_UnifiedNativeApi_dispatchIngress dsm::sofi::validation::tests::a_balance_the_closure_does_not_carry_is_invalid;dsm::sofi::validation::tests::bytes_at_a_named_address_that_are_not_a_balance_are_invalid;dsm::sofi::validation::tests::a_named_balance_not_in_hand_waits;dsm::sofi::validation::tests::bytes_that_do_not_re_derive_the_named_address_prove_nothing - CONFORMANCE §8 MR-SOFI-0340 (Met) +MR-SOFI-0341 dsm::sofi::resolve::Verifier::acquire_evidence android MUST_REACH active dsm_sdk::jni::unified_protobuf_bridge::Java_com_dsm_wallet_bridge_UnifiedNativeApi_dispatchIngress dsm::sofi::validation::tests::a_trade_that_does_not_debit_the_trader_is_invalid_from_the_exercise_alone;dsm_sdk::handlers::node_e2e_tests::a_vault_traded_through_closes_for_its_owner;dsm_sdk::handlers::node_e2e_tests::every_sofi_route_reaches_its_producer - CONFORMANCE §8 MR-SOFI-0341 (Met) +MR-SOFI-0341 dsm::sofi::validation::trader_pre_balances android MUST_REACH active dsm_sdk::jni::unified_protobuf_bridge::Java_com_dsm_wallet_bridge_UnifiedNativeApi_dispatchIngress dsm::sofi::validation::tests::a_trade_that_does_not_debit_the_trader_is_invalid_from_the_exercise_alone;dsm_sdk::handlers::node_e2e_tests::a_vault_traded_through_closes_for_its_owner;dsm_sdk::handlers::node_e2e_tests::every_sofi_route_reaches_its_producer - CONFORMANCE §8 MR-SOFI-0341 (Met) +MR-SOFI-0342 dsm::sofi::validation::setup_lineage android MUST_REACH active dsm_sdk::jni::unified_protobuf_bridge::Java_com_dsm_wallet_bridge_UnifiedNativeApi_dispatchIngress dsm::sofi::validation::tests::a_setup_on_a_lineage_known_invalid_is_invalid;dsm::sofi::validation::tests::an_invalid_verdict_about_another_lineage_or_position_supplies_nothing;dsm::sofi::validation::tests::a_lineage_not_established_leaves_the_setup_waiting - CONFORMANCE §8 MR-SOFI-0342 (Met) +MR-SOFI-0342 dsm::sofi::validation::setup_valid android MUST_REACH active dsm_sdk::jni::unified_protobuf_bridge::Java_com_dsm_wallet_bridge_UnifiedNativeApi_dispatchIngress dsm::sofi::validation::tests::a_setup_on_a_lineage_known_invalid_is_invalid;dsm::sofi::validation::tests::an_invalid_verdict_about_another_lineage_or_position_supplies_nothing;dsm::sofi::validation::tests::a_lineage_not_established_leaves_the_setup_waiting - CONFORMANCE §8 MR-SOFI-0342 (Met) MR-STOR-0006 dsm_storage_node::api::objects::immutable::put_immutable node MUST_REACH active dsm_storage_node::main dsm_storage_node::api::transport::b0x::tests::bytes_the_node_cannot_read_are_kept_and_returned_unopened;dsm_storage_node::api::objects::immutable::tests::proto_decodable_bytes_are_just_bytes - CONFORMANCE §8 MR-STOR-0006 (Met) MR-STOR-0006 dsm_storage_node::api::transport::b0x::router node MUST_REACH active dsm_storage_node::main dsm_storage_node::api::transport::b0x::tests::bytes_the_node_cannot_read_are_kept_and_returned_unopened;dsm_storage_node::api::objects::immutable::tests::proto_decodable_bytes_are_just_bytes - CONFORMANCE §8 MR-STOR-0006 (Met) MR-STOR-0007 dsm_storage_node::api::objects::immutable::put_immutable node MUST_REACH active dsm_storage_node::main dsm_storage_node::api::transport::b0x::tests::bytes_the_node_cannot_read_are_kept_and_returned_unopened;dsm_storage_node::api::objects::immutable::tests::proto_decodable_bytes_are_just_bytes - CONFORMANCE §8 MR-STOR-0007 (Met) diff --git a/specs/requirements/MASTER_REQUIREMENTS.md b/specs/requirements/MASTER_REQUIREMENTS.md index bf7d3b754..6ac7723a8 100644 --- a/specs/requirements/MASTER_REQUIREMENTS.md +++ b/specs/requirements/MASTER_REQUIREMENTS.md @@ -15,11 +15,11 @@ Every extraction in this round is taken against exactly these bytes: | File | `git hash-object` | Lines | |---|---|---| | `specs/DSM_High_Level_Explainer.md` | `bc34c8f8a64f772471625d14d4d728e80e4cc02f` | 4305 | -| `specs/SoFi_Settlement_Specification.md` | `ad5394a91b0b32dda0863390f1d8d92a43dbbf3e` | 2620 | +| `specs/SoFi_Settlement_Specification.md` | `f5c0ec64e4490aa91059d54fcc7897f071b6a7ad` | 2640 | | `specs/dBTC_Native_Specification.md` | `233a3e72a5b16a023af830f4c8ffaad4ba9391a8` | 2160 | | `specs/DSM_Storage_Node_Specification.md` | `415a9b9c67c7a2b4b6df78b0af85fb3bc7282ae8` | 636 | -Pins updated 2026-09-26 after SoFi Amendment S11 (ERA's canonical policy); before that, 2026-09-24 after SoFi Amendments S8, S9 and S10 and the storage §9 rule on the leader first, one chain in route order and the completion proof (#977); before that, 2026-09-23 after storage §14 (#974), the set-identity amendment (SoFi Amendment S6, storage §10), the replication amendment (storage §12.5), the vault-consistency recommendation (SoFi §31), Amendment S7 (SoFi §24) and Amendment A7 (DSM §11, storage §8). The extractions of 2026-09-22 were taken against SoFi `4c62ee78…` (2597 lines) and storage `8d43ac02…` (571 lines); their line-based IDs refer to those bytes. +Pins updated 2026-09-29 after SoFi Amendments S12 (the trader's balances before the trade) and S13 (a lineage known invalid); before that, 2026-09-26 after SoFi Amendment S11 (ERA's canonical policy); before that, 2026-09-24 after SoFi Amendments S8, S9 and S10 and the storage §9 rule on the leader first, one chain in route order and the completion proof (#977); before that, 2026-09-23 after storage §14 (#974), the set-identity amendment (SoFi Amendment S6, storage §10), the replication amendment (storage §12.5), the vault-consistency recommendation (SoFi §31), Amendment S7 (SoFi §24) and Amendment A7 (DSM §11, storage §8). The extractions of 2026-09-22 were taken against SoFi `4c62ee78…` (2597 lines) and storage `8d43ac02…` (571 lines); their line-based IDs refer to those bytes. The DSM and SoFi specifications were amended on 2026-09-22 (marked "Amendment" in their text). The storage-node specification was added to the corpus on 2026-09-22, before any other extractor started. The owner accepted it in full the same day. Extract its items marked **Open** with Flags `ambiguous` and a Requirement text that says so, never as settled requirements. @@ -152,6 +152,8 @@ Each extraction also has a Findings table: - **Set identity amendment (2026-09-23).** Conformance finding on MR-STOR-0055 / MR-SOFI-0068: the code commits each member's register incarnation in `storage_set_id` (CCB storage-set schema 3) and the specs said member ids only. Owner decision: the specs follow the code (SoFi Amendment S6, storage §10). Both rows rewritten. - **Completion proofs, ClaimRef and the token creator (2026-09-23, landed in #977 without rows; added 2026-09-24).** Owner decisions: storage §9 gains the rule on the leader first, one chain in route order and the completion proof; SoFi Amendment S10 requires a completion proof for every Final a DLV unlock relies on, S9 checks ClaimRef against the verifier's own accepted claim, and S8 commits the creator in the policy blob and records a token's creation once. MR-SOFI-0330–0333 and MR-STOR-0148–0158 added with source `amendment`; §1 re-pinned. - **ERA's canonical policy (2026-09-26).** Owner decisions: ERA's former commitment was the hash of empty input and committed to no policy. ERA's policy is defined for the first time and its commitment derived from those bytes (SoFi Amendment S11). A network-anchored native policy names no creator and no signer set, and exactly one exists, ERA's. The genesis supply is 80,000,000,000 (the owner's number). The beta faucet's payout is not committed, while the reserve's accounting is. Join emission after beta is a different ERA identity (Open). MR-SOFI-0300, 0303, 0315 and 0332 are scoped to device-created policies; MR-SOFI-0334–0337 added with source `amendment`; §1 re-pinned. +- **The trader's balances before the trade (2026-09-29).** Conformance finding (CONFORMANCE §6.39): once one trader had traded through a vault, no other reader could classify the trade, because `TraderSideValid` needs the trader's balances before the trade and `T°` states them only as hashes; the owner's close of that vault never resolved. Owner decisions: the exercise carries each such balance as a content-addressed `TraderPreBalance` that `𝒞_E^pre` names, every verifier (the trader included) reads the balances from those objects, and a balance the closure does not name is Invalid, not undecided (SoFi Amendment S12). MR-SOFI-0241 is a different defect and is unchanged. MR-SOFI-0338–0341 added with source `amendment`; §1 re-pinned. +- **A lineage known invalid (2026-09-29).** Conformance finding (CONFORMANCE §6.40): under Amendment S9 a trader whose lineage validation finds Invalid never yields an accepted claim, so its setup was never evaluated and its trade held a vault key forever. Owner decision: not yet known waits; known invalid makes the setup, and so the route, Invalid; no accepted claim is synthesized (SoFi Amendment S13). MR-SOFI-0331 rewritten; MR-SOFI-0342 added with source `amendment`; §1 re-pinned. - **Post-reconciliation amendment (2026-09-22): route-chain finality.** For finding GPT-4, finality was redefined as a route chain (storage spec §9, §12.6, §14, §22; DSM Amendment A6; SoFi Amendment S4). §1 pins the amended files. Canonical rows restating the old rule were rewritten, and rows for the new rules were added at the end of §8.1, §8.2 and §8.4 with source `amendment`. The extraction files in `extractions/` remain as extracted against the earlier hashes. ## 8 Canonical requirements @@ -166,7 +168,7 @@ Reconciled on 2026-09-22 from two extractions: `claude-chat` (798 rows) and `cha | DSM_Storage_Node_Specification.md | 128 | 122 | 6 | | **Total** | **859** | **652** | **207** | -Added afterwards by amendment (§7.1): DSM 3, SoFi 10, storage 30, for 902 canonical requirements in all. +Added afterwards by amendment (§7.1): DSM 3, SoFi 15, storage 30, for 907 canonical requirements in all. Columns: **ID** is the canonical ID (`MR--nnnn`, in document order). **Sources** are the extraction IDs merged into the row (`cc:` claude-chat, `gpt:` chatgpt); the first source locates the quote. **Flags** carry the findings in §8.7 that bear on the row. @@ -781,13 +783,18 @@ Columns: **ID** is the canonical ID (`MR--nnnn`, in document order). **Sou | MR-SOFI-0328 | invariant | explicit | Final(K, x) and every use of it in SoFi are replaced by the storage-spec route chain: x is final with a valid leader link and two further valid links; LeaderHeld(K, y) means y has the valid leader link and still settles that no other value is final at K. | amendment: SoFi Amendment S4 (2026-09-22) | amendment | none | | MR-SOFI-0329 | transition | explicit | A position whose drop claim won under the challenge rule resolves Void (ladder step 3a) unless it is shown Invalid on evidence in hand; nothing executes, no balance moves, the lineage continues from the previous root, and it can never move to Invalid. | amendment: SoFi Amendment S5 (2026-09-22) | amendment | none | | MR-SOFI-0330 | evidence | explicit | Every Final a DLV unlock relies on is shown by a completion proof: FulfillmentRegistered(F) by the proofs at K_ful(q) and K_root(q), and each StorageFinalE(K(F.a_j), E) in ConsumedRoute by the proof at that successor key; the client keeps the proof of each Final it relies on, Core checks each proof against its own reads of the seats, and an unlock whose proofs do not check does not unlock. | amendment: SoFi Amendment S10 (2026-09-23) | owner | none | -| MR-SOFI-0331 | evidence | explicit | SetupValid compares the setup's claim_ref with the digest of the claim the verifier accepted at the setup's position p when it validated the trader's lineage (the registered root claim of an ordinary position, or C_p of a resolved SoFi position); RouteValidation's evidence carries that accepted claim as a value only lineage validation (or the device's own admitted store, for its own positions) produces, so RouteValidation reads no storage for it; a setup naming any other claim is Invalid, and until the accepted claim is in hand the setup is not evaluated. | amendment: SoFi Amendment S9 (2026-09-23) | owner | none | +| MR-SOFI-0331 | evidence | explicit | SetupValid compares the setup's claim_ref with the digest of the claim accepted at the setup's position p by validation of the trader's lineage (the registered root claim of an ordinary position, or C_p of a resolved SoFi position); RouteValidation's evidence carries that accepted claim as a value only lineage validation (or the device's own admitted store, for its own positions) produces, so RouteValidation reads no storage for it. Three cases: while lineage validation has not reached a verdict or its evidence is not in hand, the setup is not evaluated yet; when the lineage is valid, a setup naming any other claim than the accepted one is Invalid; when lineage validation establishes the lineage Invalid at or before p (a quarantined register cell included), the setup is Invalid. | amendment: SoFi Amendments S9 (2026-09-23) and S13 (2026-09-29) | owner | none | | MR-SOFI-0332 | authority | explicit | A device-created token's policy blob commits its creator, the genesis G and device id DevID of the creating device, after the release rule (a network-anchored policy names none, Amendment S11); a native token's genesis release is admissible only in a CreateToken of that device, so anyone else holding the same policy bytes releases nothing. | amendment: SoFi Amendment S8 (2026-09-23) | owner | none | | MR-SOFI-0333 | invariant | explicit | The creating transition inserts a creation record for the policy commit into the creator's economic tree from zero (class 0x0060, key H(DSM/economic-token-creation-key/v1; G ‖ DevID ‖ policy_commit)); its presence under a validated root proves the creation, a second creation of the same commit on that lineage cannot build its write set, and so the genesis supply is released exactly once. | amendment: SoFi Amendment S8 (2026-09-23) | owner | none | | MR-SOFI-0334 | invariant | explicit | A network-anchored native policy names no creator and no signer set: its blob omits both after the release rule, and its release rule alone governs every release; a device-created policy's layout is unchanged, and a blob whose shape does not match its release rule does not parse. | amendment: SoFi Amendment S11 (2026-09-26) | owner | none | | MR-SOFI-0335 | invariant | explicit | ERA's policy is fixed in Core (version 3, fungible, native, transferable and burnable, no recipient allowlist, the beta faucet release rule, ticker and alias ERA, decimals 0, genesis supply 80,000,000,000, no description or icon), and ERA's policy commitment is BLAKE3(DSM/policy ‖ 0x00 ‖ its TokenPolicyV3 bytes); every device holds it by construction, never from storage or a registry. | amendment: SoFi Amendment S11 (2026-09-26) | owner | none | | MR-SOFI-0336 | prohibition | explicit | Exactly one network-anchored policy exists, ERA's; any other network-anchored blob has no reserve, releases nothing, and is never registered, adopted or published. | amendment: SoFi Amendment S11 (2026-09-26) | owner | none | | MR-SOFI-0337 | invariant | explicit | ERA's reserve starts at ERA's committed genesis supply and every release is counted against it: remaining plus released equals the genesis supply at every state, the reserve is exhausted exactly when all of it has been released, and at exhaustion a claim is refused before anything is signed or written. | amendment: SoFi Amendment S11 (2026-09-26) | owner | none | +| MR-SOFI-0338 | invariant | explicit | TraderPreBalance is class 0x0061, schema 1: trader_genesis, trader_device_id and policy_commit (digest32 each) and amount (u64, strictly positive; a zero balance is an absent leaf and needs no object). Its address is immutable_addr(DSM/sofi/trader-pre-balance-object/v1, CCB bytes), and 𝒞_E^pre references it as ContentAddr{0x0061, addr}, so E commits it and the exercise carries it with the other closure objects. | amendment: SoFi Amendment S12 (2026-09-29) | owner | none | +| MR-SOFI-0339 | evidence | explicit | For every entry of T° whose balance before the trade is present, 𝒞_E^pre names exactly one TraderPreBalance whose trader_genesis and trader_device_id are P's trader, whose balance_key(G, DevID, policy_commit) is the entry's key, and whose H(DSM/economic-leaf-state/v1; CCB(Balance{policy_commit, amount})) is the value the entry states before the trade; 𝒞_E^pre names no other object of that class. | amendment: SoFi Amendment S12 (2026-09-29) | owner | none | +| MR-SOFI-0340 | prohibition | explicit | A present balance of T° with no TraderPreBalance in 𝒞_E^pre, an object that disagrees with its entry, and an object that matches no entry are each Invalid, known from the committed bytes alone; an object 𝒞_E^pre names whose bytes are not in hand, or do not re-derive its address, is not evaluated yet and proves nothing. | amendment: SoFi Amendment S12 (2026-09-29) | owner | none | +| MR-SOFI-0341 | evidence | explicit | Every verifier, the trader included, reads the trader's balances before the trade only from the TraderPreBalance objects 𝒞_E^pre names; an entry that states its balance as absent before the trade is absent, and a relationship entry's leaf before the trade is the one its base names, proven by the fold against T°.pre_root. A value counts only because it hashes to the leaf the core states; storage decides nothing. | amendment: SoFi Amendment S12 (2026-09-29) | owner | none | +| MR-SOFI-0342 | prohibition | explicit | No accepted claim is synthesized for a lineage known invalid: the negative fact is the lineage verdict itself, and a verdict about another trader or another position establishes nothing about a setup. No trade whose trader's lineage is known invalid holds a vault key indefinitely because that lineage can never yield an accepted claim. | amendment: SoFi Amendment S13 (2026-09-29) | owner | none | ### 8.3 dBTC native specification diff --git a/specs/requirements/VERIFICATION_MATRIX.md b/specs/requirements/VERIFICATION_MATRIX.md index 5bd7f2b08..0da4fa92a 100644 --- a/specs/requirements/VERIFICATION_MATRIX.md +++ b/specs/requirements/VERIFICATION_MATRIX.md @@ -131,3 +131,7 @@ Tests named `dsm_sdk::…` run on devices created as wallet creation creates the | SoFi §30, MR-SOFI-0259: a vault's chain is established from the genesis this verifier accepts, one Core-recomputed consumption at a time; what this device recorded is stood on only anchored at that genesis and linked row to row, and a recorded generation is written once | `dsm` · sofi/resolution.rs · `VaultChain::from_recorded`, `extend`; sofi/resolve.rs · `Verifier::chain`; `dsm_sdk` · storage/client_db/sofi_vault_head.rs · `write_root` | `dsm::sofi::resolution::tests::the_memo_becomes_a_chain_only_anchored_at_the_genesis_and_linked_row_to_row`; `dsm_sdk::storage::client_db::sofi_vault_head::tests::an_established_generation_is_never_rewritten`; `dsm_sdk::storage::client_db::sofi_vault_head::tests::the_recorded_generations_are_read_contiguously_with_their_links` | Run 2026-09-26: `from_recorded` taking the rows as recorded (neither anchored nor linked) → `the_memo_becomes_a_chain_only_anchored_at_the_genesis_and_linked_row_to_row` red; `write_root` updating the root on conflict → `an_established_generation_is_never_rewritten` red; restored | — | | SoFi §47, MR-SOFI-0304, MR-SOFI-0315: a token's policy is the `TokenPolicyV3` bytes at its commitment; the enforcer registers a policy only from bytes it hashed itself and takes durable bytes only when they re-hash to the commitment asked for | `dsm` · core/token/policy/mod.rs · `TokenPolicySystem::register_policy`, `policy_at` | `dsm::core::token::policy::tests::bytes_at_another_commitment_are_not_the_policy_asked_for`; `dsm::core::token::policy::tests::a_policy_is_registered_from_its_committed_bytes_alone` | `policy_at` taking the durable bytes without re-hashing them to the commitment asked for → `bytes_at_another_commitment_are_not_the_policy_asked_for` red (2026-09-26, restored). | — | | SoFi §51, MR-SOFI-0318: an operation is evaluated against the policy committed at the commitment it names and nothing else; where none is committed (ERA today) it is denied, never allowed by a default | `dsm` · core/token/policy/mod.rs · `TokenPolicySystem::enforce_policy`; `dsm_sdk` · sdk/core_sdk.rs · `build_token_policy_context` (the operation's `policy_commit`) | `dsm::core::token::policy::tests::an_operation_naming_a_commitment_without_a_policy_is_denied` | `enforce_policy` allowing where no policy is committed → red (2026-09-26, restored). | — | +| MR-SOFI-0331, MR-SOFI-0342, SoFi Amendment S13: a setup on a lineage known invalid is Invalid; not established waits; no accepted claim is synthesized, and a verdict about another lineage or position establishes nothing | `dsm` · sofi/validation.rs · `setup_lineage` (the walk's classes read), `setup_valid` (the verdict's identity and position) | `dsm::sofi::validation::tests::a_setup_on_a_lineage_known_invalid_is_invalid`; `dsm::sofi::validation::tests::a_lineage_not_established_leaves_the_setup_waiting`; `dsm::sofi::validation::tests::an_invalid_verdict_about_another_lineage_or_position_supplies_nothing` | Verdict read as waiting → `a_setup_on_a_lineage_known_invalid_is_invalid` red; a quarantine read as not established → the same test red; the verdict's identity check removed → `an_invalid_verdict_about_another_lineage_or_position_supplies_nothing` red; Incomplete and Unresolved read as invalid → `a_lineage_not_established_leaves_the_setup_waiting` red (2026-09-29, each restored). | — | +| MR-SOFI-0338, SoFi Amendment S12: a `TraderPreBalance` has one canonical encoding, a strictly positive amount and its own address namespace | `dsm` · sofi/wire/objects.rs · `TraderPreBalance::new`, `TraderPreBalance::decode`; sofi/derive.rs · `trader_pre_balance_addr`, `closure_content_address` | `dsm::sofi_v8_vault_bytes::a_trader_pre_balance_matches_the_independent_encoder_and_its_frozen_address` (the independent encoder and frozen address; zero, the wrong class, truncation and trailing bytes refused) | Zero-amount check removed → `a_trader_pre_balance_matches_the_independent_encoder_and_its_frozen_address` red (2026-09-29, restored). | — | +| MR-SOFI-0339, MR-SOFI-0340, SoFi Amendment S12: `𝒞_E^pre` names exactly one `TraderPreBalance` of `P`'s trader for each balance `T°` states as present; a missing one is Invalid in hand, and a named one not in hand waits | `dsm` · sofi/validation.rs · `trader_pre_balances` (the count, the trader, one object per stated balance), `Evidence::pre_balance` (the address re-derived) | `dsm::sofi::validation::tests::a_balance_the_closure_does_not_carry_is_invalid`; `dsm::sofi::validation::tests::extra_balances_in_the_closure_are_invalid`; `dsm::sofi::validation::tests::a_balance_keyed_to_no_stated_balance_is_invalid`; `dsm::sofi::validation::tests::a_balance_of_another_trader_is_invalid`; `dsm::sofi::validation::tests::bytes_that_do_not_re_derive_the_named_address_prove_nothing`; `dsm::sofi::validation::tests::bytes_at_a_named_address_that_are_not_a_balance_are_invalid`; `dsm::sofi::validation::tests::a_named_balance_not_in_hand_waits` | Count check removed → `extra_balances_in_the_closure_are_invalid` red; trader check removed → `a_balance_of_another_trader_is_invalid` red; a stated balance without its number read as absent → `a_balance_keyed_to_no_stated_balance_is_invalid` red; address check weakened to "any address" → `bytes_that_do_not_re_derive_the_named_address_prove_nothing` red (2026-09-29, each restored). | — | +| MR-SOFI-0341, SoFi Amendment S12: the carried balance counts only because it hashes to the leaf the core states, and every verifier judges the trader's side from it | `dsm` · sofi/validation.rs · `check_trader_balances` (pre and post values against `T°`), `trader_posts` | `dsm::sofi::validation::tests::a_balance_that_is_not_the_leaf_the_core_states_is_invalid`; `dsm::sofi::validation::tests::a_trade_that_does_not_debit_the_trader_is_invalid_from_the_exercise_alone`; `dsm::sofi::validation::tests::trader_post_states_are_recomputed_and_bound_to_the_stated_values`; `dsm_sdk::handlers::node_e2e_tests::a_vault_traded_through_closes_for_its_owner` | Pre-value comparison removed → `a_balance_that_is_not_the_leaf_the_core_states_is_invalid` red; post-value comparison removed → `a_trade_that_does_not_debit_the_trader_is_invalid_from_the_exercise_alone` red; post arithmetic off the carried balance → `trader_post_states_are_recomputed_and_bound_to_the_stated_values` red (2026-09-29, each restored). | — |