diff --git a/docs/papers/amendment-2c-d-bundle-acceptance-and-realization.md b/docs/papers/amendment-2c-d-bundle-acceptance-and-realization.md index a641e62f..2977f556 100644 --- a/docs/papers/amendment-2c-d-bundle-acceptance-and-realization.md +++ b/docs/papers/amendment-2c-d-bundle-acceptance-and-realization.md @@ -385,6 +385,51 @@ check is gone with both operands. `X` is inside `b`, `b` is inside the leaf, and authenticated under the root — the equality is established by the chain rather than asserted by the wrapper, which is Blocker 1's whole complaint. +### Producer adoption — `TA_B` becomes reachable (owner ruling, 2026-09-10) + +The owner ruling on producer reachability decomposes the remaining work into three changes, and this +is the second: + +```text +PR A bundle-aware settle write set; the 0x0032 leaf becomes mandatory + -> the acceptance leaf is guaranteed to EXIST +PR B construct and publish TA_B from the resulting economic post-state + -> TA_B is guaranteed to be CONSTRUCTIBLE and REACHABLE +PR C the realization / fence-release cutover + -> live realization is ALLOWED to consume it +``` + +**Where the path comes from, and where it must not.** `TA_B` proves the acceptance leaf's inclusion +under `R_T^+`, so its path is the one that holds in the FINAL economic post-state. A mutation's own +captured siblings are not that path: the write set captures mutation `i`'s siblings with mutations +`0..i` applied, and the acceptance leaf's key is a hash, so nothing places it last. The producer +therefore reads the finished tree — the same single snapshot whose root was registered, with no +second read and no window in which the tree could move — and refuses when that tree is not the one +the register committed. + +```text +economic post-state containing 0x0032 obtained + -> ordered inclusion path for that leaf under R_T^+ + -> TA_B built from the already-authenticated settlement facts + -> published through the durable publication path +``` + +**The identities are consumed, never chosen.** `b` and `economic_operation_id` arrive inside the +emitted leaf, lifted out of the witness; there is no parameter through which a caller could supply +either. `trader_genesis` is the authenticated local identity and `economic_position` the position +the register just committed. A producer that reconstructed any of these would be asserting a second +time what the transition already fixed. + +**What producing `TA_B` does not do.** It publishes an artifact. It does not call +`CompleteValidity::from_market_witness`, release the trader fence, mark the settlement realized, +advance the realized frontier, or publish a final realized receipt — and it constructs no +`BundleAcceptanceWitness`, whose only constructor is §7's verifier and which additionally requires +the composed bundle and an independently established trader AK. Those remain the dedicated +behaviour-changing cutover, for the reason §11's boundary note gives: one change, so that *"nothing +released before it"* stays checkable rather than argued. + +--- + --- ## §7 — Verification obligations, in order, all conjunctive diff --git a/dsm_client/deterministic_state_machine/dsm/src/economic/acceptance_produce.rs b/dsm_client/deterministic_state_machine/dsm/src/economic/acceptance_produce.rs new file mode 100644 index 00000000..a9d3b582 --- /dev/null +++ b/dsm_client/deterministic_state_machine/dsm/src/economic/acceptance_produce.rs @@ -0,0 +1,471 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! THE `TA_B` PRODUCER — amendment 2c-D §6, producer adoption. +//! +//! [`crate::economic::acceptance_verify`] is the other half: it checks a +//! `TA_B` against a bundle under §7's seven conjuncts. This module is what +//! makes one exist at all. Before it, `TraderAcceptance::new` had no caller +//! outside its own tests — §7 was constructible in the verifier and +//! unreachable in the live settle path. +//! +//! ## Everything is read from the transition that already happened +//! +//! A `TA_B` names four things, and not one of them is chosen here: +//! +//! ```text +//! trader_genesis the authenticated local identity, passed in +//! economic_position the position the register just committed +//! acceptance_leaf THE emitted 0x0032, lifted out of the witness +//! acceptance_path that leaf's siblings, taken from the post-state tree +//! ``` +//! +//! `b` and `economic_operation_id` therefore arrive inside the leaf the write +//! set emitted, never as parameters a caller could pick — the owner ruling's +//! *"no reconstructed or caller-selected identities"*. There is deliberately +//! no argument through which either could be supplied. +//! +//! ## What "under `R_T^+`" costs +//! +//! §7 step 5 folds the path and requires the result to equal the root the +//! validity walk derived. The path must therefore be the one that holds in the +//! FINAL post-state, and a mutation's own siblings are not that: they are +//! captured with mutations `0..i` applied, so any later mutation in key order +//! invalidates them. The acceptance leaf is not last by construction — its key +//! is a hash — so this module takes the path from the finished tree, and +//! refuses when that tree is not the one whose root was validated. +//! +//! ## What producing a `TA_B` does NOT do +//! +//! Nothing. It publishes an artifact. It does not realize the settlement, +//! release the trader fence, advance the realized frontier, or promote any +//! market fold out of `PartialPendingRealization` — those remain the dedicated +//! cutover, and 2c-D §11's boundary note is why they are one change and not +//! several. A `TA_B` existing is a precondition of realization, never its +//! trigger. + +use crate::economic::state::{EconomicBundleAcceptanceState, EconomicLeafState}; +use crate::economic::trader_acceptance::{AcceptanceMalformed, TraderAcceptance}; +use crate::economic::tree::{leaf_node, root_from_path, EconomicSmt}; +use crate::economic::witness::EconomicTransitionWitness; + +/// Why a transition that wrote an acceptance leaf still cannot yield a `TA_B`. +/// +/// Every arm is a disagreement between two facts that must be the same +/// transition. None of them is recoverable by retry: a producer holding a +/// tree, a witness and a validated root that do not describe one another has +/// a bug, not a transient. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum AcceptanceNotProducible { + /// The tree the path would come from is not the post-state whose root was + /// validated. A path taken from any other tree folds to any other root. + TreeIsNotTheValidatedPostState { tree: [u8; 32], validated: [u8; 32] }, + /// The witness the leaf would come from does not describe the transition + /// that produced the validated root — so its acceptance leaf, whatever it + /// says, belongs to some other transition. + WitnessIsNotTheValidatedPostState { + witness: [u8; 32], + validated: [u8; 32], + }, + /// More than one `0x0032` in one witness. The write-set boundary already + /// refuses this; reaching it here means a witness arrived from somewhere + /// else, and picking one of them would be a silent choice about which + /// bundle this transition accepted. + MoreThanOneAcceptanceLeaf { count: usize }, + /// The emitted leaf and the witness name different economic operations. + /// This is §7 step 5's middle term, checked at production so an artifact + /// that could never satisfy it is never published. + OperationIdentityDisagrees { leaf: [u8; 32], witness: [u8; 32] }, + /// The path this module just took does not fold the leaf back to the + /// validated root. + PathDoesNotFoldToTheValidatedRoot { + folded: [u8; 32], + validated: [u8; 32], + }, + /// `TraderAcceptance`'s own frozen rejections (§6). + Malformed(AcceptanceMalformed), + /// The leaf has no canonical bytes, so it has no leaf value to fold. + LeafNotEncodable, +} + +impl core::fmt::Display for AcceptanceNotProducible { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::TreeIsNotTheValidatedPostState { tree, validated } => write!( + f, + "the producer tree is at {tree:02x?}, not at the validated economic root \ + {validated:02x?}, so any path taken from it proves inclusion in a different tree" + ), + Self::WitnessIsNotTheValidatedPostState { witness, validated } => write!( + f, + "the witness ends at {witness:02x?}, not at the validated economic root \ + {validated:02x?}, so its acceptance leaf is not this transition's" + ), + Self::MoreThanOneAcceptanceLeaf { count } => write!( + f, + "{count} bundle-acceptance leaves in one economic operation; exactly one is the \ + rule, and choosing among them would decide which bundle was accepted" + ), + Self::OperationIdentityDisagrees { leaf, witness } => write!( + f, + "the acceptance leaf names operation {leaf:02x?} while its own witness names \ + {witness:02x?}" + ), + Self::PathDoesNotFoldToTheValidatedRoot { folded, validated } => write!( + f, + "the acceptance path folds to {folded:02x?}, not to the validated economic root \ + {validated:02x?}" + ), + Self::Malformed(e) => write!(f, "{e}"), + Self::LeafNotEncodable => { + write!(f, "the acceptance leaf has no canonical encoding") + } + } + } +} + +/// Build the canonical `TA_B` for a transition that accepted a settlement +/// bundle, or `None` for one that accepted none. +/// +/// `None` is the ordinary answer: every non-settlement writes no `0x0032`, and +/// that is an absence rather than a failure. A market settle always writes +/// exactly one (2c-D §8's producer-adoption cardinality), so `None` from a +/// settle would mean the write set did not emit its mandatory leaf — which +/// `build_write_set` refuses before this point. +/// +/// `validated_root` and `economic_position` come from the register commitment, +/// never from the tree: the point of passing them is that they can DISAGREE +/// with the tree, and the disagreement is the thing worth refusing. +pub fn produce_trader_acceptance( + tree: &EconomicSmt, + witness: &EconomicTransitionWitness, + genesis: &[u8; 32], + device_id: &[u8; 32], + validated_root: [u8; 32], + economic_position: u64, +) -> Result, AcceptanceNotProducible> { + // Both origins pinned to the validated root BEFORE anything is read out of + // either. The tree supplies the path, the witness supplies the leaf; if + // they are not the same finished transition, the artifact would pair one + // transition's leaf with another's proof. + let tree_root = tree.root(); + if tree_root != validated_root { + return Err(AcceptanceNotProducible::TreeIsNotTheValidatedPostState { + tree: tree_root, + validated: validated_root, + }); + } + if witness.post_economic_root != validated_root { + return Err(AcceptanceNotProducible::WitnessIsNotTheValidatedPostState { + witness: witness.post_economic_root, + validated: validated_root, + }); + } + + let mut accepted: Vec<&EconomicBundleAcceptanceState> = Vec::new(); + for m in &witness.mutations { + if let Some(EconomicLeafState::BundleAcceptance(a)) = &m.post_state { + accepted.push(a); + } + } + let leaf = match accepted.as_slice() { + [] => return Ok(None), + [one] => *one, + many => { + return Err(AcceptanceNotProducible::MoreThanOneAcceptanceLeaf { count: many.len() }) + } + }; + if leaf.economic_operation_id != witness.economic_operation_id { + return Err(AcceptanceNotProducible::OperationIdentityDisagrees { + leaf: leaf.economic_operation_id, + witness: witness.economic_operation_id, + }); + } + + // The key is derived through the leaf-state family, the one dispatch point + // a leaf written to the tree and a leaf nested in `TA_B` share. + let state = EconomicLeafState::BundleAcceptance(leaf.clone()); + let key = state.leaf_key(genesis, device_id); + let value = state + .leaf_value() + .map_err(|_| AcceptanceNotProducible::LeafNotEncodable)?; + let path = tree.siblings(&key); + + // §7 step 5's fold, run here against the tree the path came from. It + // cannot disagree while `siblings` and `root_from_path` agree — which is + // exactly why running it is worth its 256 hashes: an artifact that would + // fail step 5 is refused at birth rather than published and rejected by + // every verifier that ever reads it. + let folded = root_from_path(&key, &leaf_node(&key, Some(&value)), &path); + if folded != validated_root { + return Err(AcceptanceNotProducible::PathDoesNotFoldToTheValidatedRoot { + folded, + validated: validated_root, + }); + } + + TraderAcceptance::new(*genesis, economic_position, leaf.clone(), path.to_vec()) + .map(Some) + .map_err(AcceptanceNotProducible::Malformed) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::economic::keys::bundle_acceptance_key; + use crate::economic::mutation::EconomicLeafMutation; + use crate::economic::state::EconomicBalanceState; + use crate::economic::trader_acceptance::TRADER_ACCEPTANCE_LEN; + + const G: [u8; 32] = [0x11; 32]; + const DEV: [u8; 32] = [0x22; 32]; + const B: [u8; 32] = [0xB0; 32]; + const EOID: [u8; 32] = [0x50; 32]; + const OTHER_EOID: [u8; 32] = [0x51; 32]; + const OPD: [u8; 32] = [0x0D; 32]; + const POSITION: u64 = 3; + + fn pc() -> [u8; 32] { + [0x10; 32] + } + + fn acceptance(bundle: [u8; 32], eoid: [u8; 32]) -> EconomicLeafState { + EconomicLeafState::BundleAcceptance(EconomicBundleAcceptanceState { + bundle, + economic_operation_id: eoid, + }) + } + + fn balance(amount: u64) -> EconomicLeafState { + EconomicLeafState::Balance(EconomicBalanceState::new(pc(), amount).expect("balance")) + } + + /// A transition that debits a funded balance and writes `extra`, built the + /// way the write set builds one: key order, siblings captured with every + /// earlier mutation applied. Returns the witness and the FINISHED tree. + /// + /// The debit leg is not decoration — it is what makes the acceptance leaf + /// stop being the only mutation, so the difference between a mutation's own + /// captured siblings and the path under the final root is real here. + fn transition(extra: &[EconomicLeafState]) -> (EconomicTransitionWitness, EconomicSmt) { + transition_id(EOID, extra) + } + + /// As above, with the witness's own operation id chosen by the caller — + /// needed wherever the acceptance leaf's KEY, which is a function of that + /// id, has to land in a particular place in the mutation order. + fn transition_id( + eoid: [u8; 32], + extra: &[EconomicLeafState], + ) -> (EconomicTransitionWitness, EconomicSmt) { + let mut tree = EconomicSmt::new(); + let funded = balance(5_000); + tree.insert( + funded.leaf_key(&G, &DEV), + funded.leaf_value().expect("value"), + ); + let pre_root = tree.root(); + + let mut planned: Vec<(Option, EconomicLeafState)> = + vec![(Some(funded), balance(4_000))]; + planned.extend(extra.iter().map(|s| (None, s.clone()))); + planned.sort_by_key(|(_, post)| post.leaf_key(&G, &DEV)); + + let mut mutations = Vec::new(); + for (pre, post) in planned { + let key = post.leaf_key(&G, &DEV); + let siblings = tree.siblings(&key).to_vec(); + tree.insert(key, post.leaf_value().expect("value")); + mutations.push(EconomicLeafMutation::new(pre, Some(post), siblings).expect("mutation")); + } + let witness = + EconomicTransitionWitness::new(pre_root, tree.root(), eoid, OPD, mutations, Vec::new()) + .expect("witness"); + (witness, tree) + } + + fn produce( + witness: &EconomicTransitionWitness, + tree: &EconomicSmt, + ) -> Result, AcceptanceNotProducible> { + produce_trader_acceptance(tree, witness, &G, &DEV, tree.root(), POSITION) + } + + /// The ordinary answer for everything that is not a settle: no acceptance + /// leaf, so no `TA_B`, and that is an absence rather than a failure. + #[test] + fn a_transition_that_accepted_no_bundle_produces_nothing() { + let (witness, tree) = transition(&[]); + assert_eq!(produce(&witness, &tree), Ok(None)); + } + + /// THE PRODUCER'S WHOLE CLAIM: the path it emits folds the emitted leaf + /// back to `R_T^+`, and the identities it carries are the leaf's own. + /// + /// The fold is recomputed here from the artifact's OWN fields — never from + /// the tree — because that is what a verifier will have. + #[test] + fn the_emitted_path_folds_the_emitted_leaf_back_to_the_validated_root() { + let (witness, tree) = transition(&[acceptance(B, EOID)]); + let root = tree.root(); + let ta = produce(&witness, &tree) + .expect("producible") + .expect("a settle produces one"); + + assert_eq!(ta.trader_genesis(), G); + assert_eq!(ta.economic_position(), POSITION); + assert_eq!(ta.acceptance_leaf().bundle, B, "the emitted b, verbatim"); + assert_eq!( + ta.acceptance_leaf().economic_operation_id, + EOID, + "the emitted operation id, verbatim" + ); + assert_eq!(ta.encode().expect("encodable").len(), TRADER_ACCEPTANCE_LEN); + + let key = bundle_acceptance_key(&G, &DEV, &EOID); + let value = EconomicLeafState::BundleAcceptance(ta.acceptance_leaf().clone()) + .leaf_value() + .expect("value"); + let siblings: &[[u8; 32]; crate::economic::tree::ECONOMIC_SMT_HEIGHT] = + ta.acceptance_path().try_into().expect("exactly 256"); + assert_eq!( + root_from_path(&key, &leaf_node(&key, Some(&value)), siblings), + root, + "the artifact's own fields must reconstruct R_T^+" + ); + } + + /// A mutation's captured siblings are NOT the path under `R_T^+` unless it + /// happens to sort last. This is the reason the producer reads the finished + /// tree, stated as a test rather than as a comment: if it ever became true + /// that the two agreed, taking the cheaper one would look safe. + /// + /// The operation id is CHOSEN so the acceptance leaf sorts before the + /// debit. Left to the fixture's own id it sorts last, the two paths agree, + /// and the test would pass while proving nothing — which is how this one + /// was written the first time. + #[test] + fn the_leafs_own_captured_siblings_are_not_the_path_under_the_final_root() { + let debit_key = balance(4_000).leaf_key(&G, &DEV); + let eoid = (0u8..=255) + .map(|i| [i; 32]) + .find(|e| acceptance(B, *e).leaf_key(&G, &DEV) < debit_key) + .expect("some operation id keys the acceptance before the debit"); + let (witness, tree) = transition_id(eoid, &[acceptance(B, eoid)]); + let key = bundle_acceptance_key(&G, &DEV, &eoid); + let captured = witness + .mutations + .iter() + .find(|m| m.leaf_key(&G, &DEV).expect("keyed") == key) + .expect("the acceptance mutation") + .siblings + .clone(); + let root = tree.root(); + let ta = produce_trader_acceptance(&tree, &witness, &G, &DEV, root, POSITION) + .expect("producible") + .expect("one"); + assert_ne!( + captured.as_slice(), + ta.acceptance_path(), + "a later mutation must invalidate the siblings captured at the acceptance" + ); + // AND THE CAPTURED ONE WOULD BE WRONG, not merely different. + let value = EconomicLeafState::BundleAcceptance(ta.acceptance_leaf().clone()) + .leaf_value() + .expect("value"); + let stale: &[[u8; 32]; crate::economic::tree::ECONOMIC_SMT_HEIGHT] = + captured.as_slice().try_into().expect("exactly 256"); + assert_ne!( + root_from_path(&key, &leaf_node(&key, Some(&value)), stale), + root, + "the mutation's own siblings must NOT fold to R_T^+" + ); + } + + /// A path taken from any other tree proves inclusion in that other tree. + #[test] + fn a_tree_that_is_not_the_validated_post_state_is_refused() { + let (witness, tree) = transition(&[acceptance(B, EOID)]); + let elsewhere = [0x77; 32]; + assert_eq!( + produce_trader_acceptance(&tree, &witness, &G, &DEV, elsewhere, POSITION), + Err(AcceptanceNotProducible::TreeIsNotTheValidatedPostState { + tree: tree.root(), + validated: elsewhere, + }) + ); + } + + /// The leaf and the path must come from ONE transition. A witness ending + /// somewhere else describes another. + #[test] + fn a_witness_that_is_not_the_validated_post_state_is_refused() { + let (_, tree) = transition(&[acceptance(B, EOID)]); + let (other, _) = transition(&[acceptance(B, OTHER_EOID)]); + let root = tree.root(); + assert_eq!( + produce_trader_acceptance(&tree, &other, &G, &DEV, root, POSITION), + Err(AcceptanceNotProducible::WitnessIsNotTheValidatedPostState { + witness: other.post_economic_root, + validated: root, + }) + ); + } + + /// Two acceptances, two bundles, one transition: which one did this + /// transition accept? The producer declines to answer rather than taking + /// the first. The write-set boundary refuses this upstream; a witness that + /// arrived from anywhere else has not been through it. + #[test] + fn two_acceptance_leaves_refuse_rather_than_choose_a_bundle() { + let (witness, tree) = + transition(&[acceptance(B, EOID), acceptance([0xB1; 32], OTHER_EOID)]); + assert_eq!( + produce(&witness, &tree), + Err(AcceptanceNotProducible::MoreThanOneAcceptanceLeaf { count: 2 }) + ); + } + + /// §7 step 5's middle term, refused at production: a leaf whose operation + /// id is not its own witness's could never fold under an id the verifier + /// recomputes. + #[test] + fn a_leaf_naming_another_operation_than_its_witness_is_refused() { + let (witness, tree) = transition(&[acceptance(B, OTHER_EOID)]); + assert_eq!( + produce(&witness, &tree), + Err(AcceptanceNotProducible::OperationIdentityDisagrees { + leaf: OTHER_EOID, + witness: EOID, + }) + ); + } + + /// `TraderAcceptance`'s own frozen rejection reaches the producer rather + /// than being re-implemented by it. + #[test] + fn a_zero_genesis_produces_no_acceptance_at_all() { + let zero = [0u8; 32]; + let mut tree = EconomicSmt::new(); + let state = acceptance(B, EOID); + let key = state.leaf_key(&zero, &DEV); + let pre_root = tree.root(); + let siblings = tree.siblings(&key).to_vec(); + tree.insert(key, state.leaf_value().expect("value")); + let witness = EconomicTransitionWitness::new( + pre_root, + tree.root(), + EOID, + OPD, + vec![EconomicLeafMutation::new(None, Some(state), siblings).expect("mutation")], + Vec::new(), + ) + .expect("witness"); + let root = tree.root(); + assert_eq!( + produce_trader_acceptance(&tree, &witness, &zero, &DEV, root, POSITION), + Err(AcceptanceNotProducible::Malformed( + AcceptanceMalformed::GenesisIsZero + )) + ); + } +} diff --git a/dsm_client/deterministic_state_machine/dsm/src/economic/mod.rs b/dsm_client/deterministic_state_machine/dsm/src/economic/mod.rs index 2cb2fea0..1b2314cc 100644 --- a/dsm_client/deterministic_state_machine/dsm/src/economic/mod.rs +++ b/dsm_client/deterministic_state_machine/dsm/src/economic/mod.rs @@ -51,6 +51,7 @@ //! predicate the `0x0023` arm resolves — the producer is `token.mint`'s //! economic admission. +pub mod acceptance_produce; pub mod acceptance_verify; pub mod admission; pub mod authority_evidence; diff --git a/dsm_client/deterministic_state_machine/dsm/tests/trader_acceptance_producer.rs b/dsm_client/deterministic_state_machine/dsm/tests/trader_acceptance_producer.rs new file mode 100644 index 00000000..b4350e8d --- /dev/null +++ b/dsm_client/deterministic_state_machine/dsm/tests/trader_acceptance_producer.rs @@ -0,0 +1,411 @@ +// SPDX-License-Identifier: Apache-2.0 +#![allow(clippy::disallowed_methods)] // test asserts; a failure here is the signal + +//! THE PRODUCER MEETS §7 — amendment 2c-D, producer adoption. +//! +//! What becomes true here: a `TA_B` built by +//! [`dsm::economic::acceptance_produce::produce_trader_acceptance`] from the +//! post-state a REAL settle write set left behind satisfies 2c-D §7's +//! verifier. Before this file the two halves had never met — §7 was exercised +//! against hand-assembled acceptances, and the producer did not exist. +//! +//! Why that gap mattered: an artifact that a verifier can check and a producer +//! can never emit is a verifier with no subject. `verify_trader_acceptance` +//! could have been wrong in any way that a hand-built fixture also happened to +//! be wrong, and nothing would have said so. +//! +//! **What this file does NOT establish.** A `TA_B` that verifies is not a +//! realized settlement. §7 itself says so: acceptance is a precondition of +//! realization, never its trigger, and realization additionally needs `B` +//! binding-final and C4's correspondence. Nothing here releases a fence, +//! advances a frontier, or publishes a receipt — and +//! `a_settled_market_publishes_a_trader_acceptance_and_realizes_nothing` +//! in `dlv_routes` is the control that says so over the LIVE path. + +use std::collections::BTreeMap; + +use dsm::ccb::{ + Allocation, DsmSuccessorEvidence, EncumbranceSet, FeePolicy, MarketPolicy, MarketTerms, + ReleasePolicy, Route, RouteLeg, StorageSetMembers, TradeIntent, VaultStateV2, +}; +use dsm::dlv::successor_validity::{ + check_correspondence, check_market_correspondence, AcceptedTransition, BundleCoordinates, + MarketCorrespondence, +}; +use dsm::economic::acceptance_produce::produce_trader_acceptance; +use dsm::economic::acceptance_verify::{verify_trader_acceptance, AcceptanceInvalid}; +use dsm::economic::lineage::ValidatedEconomicRoot; +use dsm::economic::state::{EconomicBalanceState, EconomicLeafState}; +use dsm::economic::trader_acceptance::TraderAcceptance; +use dsm::economic::tree::EconomicSmt; +use dsm::economic::witness::EconomicTransitionWitness; +use dsm::economic::write_set::{ + build_write_set, CreditSourceFacts, EconomicPreState, EconomicWriteContext, +}; +use dsm::types::operations::{Operation, TransactionMode}; + +const G: [u8; 32] = [0x11; 32]; +const DEV: [u8; 32] = [0x22; 32]; +const VAULT: [u8; 32] = [0x03; 32]; +const C_DSM_PLUS: [u8; 32] = [0xC5; 32]; +const X: [u8; 32] = [0xA0; 32]; +const B: [u8; 32] = [0xB0; 32]; +const POSITION: u64 = 3; + +fn pc_a() -> [u8; 32] { + [0x10; 32] +} +fn pc_b() -> [u8; 32] { + [0x20; 32] +} + +fn econ_op_id() -> [u8; 32] { + dsm::economic::faucet::dsm_economic_operation_id(&G, &DEV, &C_DSM_PLUS) +} + +fn settle() -> Operation { + Operation::DlvSettle { + vault_id: VAULT.to_vec(), + owner_public_key: vec![0x01; 64], + owner_devid: [0x41; 32], + owner_genesis: [0x42; 32], + input_policy_commit: pc_a(), + output_policy_commit: pc_b(), + parent_sequence: 7, + parent_binding: [0xC0; 32], + route_commit_bytes: vec![0x09; 8], + external_commitment_x: X, + input_amount: 1_000, + output_amount: 900, + fee_bps: 30, + sigma: [0x66; 32], + settler_public_key: vec![0x02; 64], + settler_devid: DEV, + settlement_receipt_id: dsm::dlv::settlement_receipt_leaf::derive_receipt_id(&VAULT, &X), + signature: vec![0x77; 48], + mode: TransactionMode::Unilateral, + } +} + +/// The REAL settle write set, and the post-state tree it left behind. +/// +/// Everything downstream reads out of these two values, which is the point: +/// the acceptance leaf and its path are the ones production emits, not ones +/// this file assembled to be checkable. +fn settled() -> (EconomicTransitionWitness, EconomicSmt) { + let op = settle(); + let mut tree = EconomicSmt::new(); + let funded = + EconomicLeafState::Balance(EconomicBalanceState::new(pc_a(), 5_000).expect("balance")); + tree.insert( + funded.leaf_key(&G, &DEV), + funded.leaf_value().expect("value"), + ); + let mut balances = BTreeMap::new(); + balances.insert(pc_a(), 5_000u64); + let pre_root = tree.root(); + + let built = build_write_set( + &op, + &G, + &DEV, + &econ_op_id(), + &EconomicPreState::balances_only(&balances), + &mut tree, + &CreditSourceFacts::DlvReserveConsumption { + owner_economic_position: 3, + reserve_consumption_evidence_addr: [0xEE; 32], + }, + &EconomicWriteContext::DlvSettle { bundle_id: B }, + ) + .expect("the settle write set builds"); + + let witness = EconomicTransitionWitness::new( + pre_root, + built.post_root, + econ_op_id(), + dsm::economic::faucet::dsm_operation_digest(&op.to_bytes()), + built.mutations, + built.credit_sources, + ) + .expect("witness"); + (witness, tree) +} + +/// Market terms whose `sigma_dsm` genuinely signs the digest §7 step 2 +/// reconstructs, over the SAME settle operation the write set consumed. +fn terms_signed_by(sk: &[u8]) -> MarketTerms { + let op_bytes = settle().to_bytes(); + let digest = dsm::economic::successor_evidence::substrate_signing_digest( + &G, + &DEV, + &C_DSM_PLUS, + &dsm::economic::faucet::dsm_operation_digest(&op_bytes), + ); + let sigma = dsm::crypto::sphincs::sphincs_sign(sk, &digest).expect("sign"); + MarketTerms { + intent: TradeIntent { + token_in: pc_a(), + amount_in: 1_000, + token_out: pc_b(), + exact_out: 900, + fee_bps: 30, + nonce: [0x5A; 32], + }, + route_set_commitment: X, + selected_route: Route::new(vec![RouteLeg::Single(Allocation { + parent_binding: [0xC0; 32], + delta_in: 1_000, + delta_out: 900, + encumbrance_claim: [0xE1; 32], + fee_policy: FeePolicy::new(30).expect("fee below denominator"), + })]) + .expect("one leg"), + trader_parent: [0xC1; 32], + trader_successor: C_DSM_PLUS, + recovery_material: DsmSuccessorEvidence::new( + [0x77; 32], [0xC1; 32], DEV, op_bytes, [0xE0; 32], sigma, + ) + .expect("evidence"), + } +} + +/// Steps 4 and 7 arrive as C4's own fact, exactly as they do in production. +fn correspondence() -> MarketCorrespondence { + let v = VaultStateV2 { + owner_genesis_id: [1; 32], + owner_device_id: [2; 32], + vault_id: [3; 32], + generation: 7, + reserve_a: 10_000, + reserve_b: 5_000, + market_policy: MarketPolicy::beta_constant_product(pc_a(), pc_b()).expect("ordered pair"), + release_policy: ReleasePolicy::beta_owner_local_full_close(), + fee_policy: FeePolicy::new(30).expect("fee below denominator"), + encumbrances: EncumbranceSet::empty(), + iteration_budget: None, + parent_state_commitment: [4; 32], + owner_authority_transition_digest: [5; 32], + storage_set: StorageSetMembers::new(&[(b"dsm-node-1".as_slice(), [9; 32])]) + .expect("one member"), + quorum: 1, + }; + let supplied = v.encode().expect("encode"); + let witness = check_correspondence(&v, [0xC0; 32], &supplied).expect("10.a"); + check_market_correspondence( + &AcceptedTransition { + embedded_parent: [0xC1; 32], + c_dsm_plus: C_DSM_PLUS, + external_commitment_x: X, + parent_binding: [0xC0; 32], + effects_digest: [0xEF; 32], + }, + &BundleCoordinates { + trader_parent: [0xC1; 32], + trader_successor: C_DSM_PLUS, + route_set_commitment: X, + route_effects_digest: [0xEF; 32], + }, + [0xC0; 32], + witness, + ) + .expect("CORR.1-5") +} + +struct Fixture { + acceptance: TraderAcceptance, + terms: MarketTerms, + validated: ValidatedEconomicRoot, + ak: Vec, +} + +fn fixture() -> Fixture { + let (witness, tree) = settled(); + let root = tree.root(); + let acceptance = produce_trader_acceptance(&tree, &witness, &G, &DEV, root, POSITION) + .expect("producible") + .expect("a market settle always writes its acceptance leaf"); + let (pk, sk) = dsm::crypto::sphincs::generate_sphincs_keypair().expect("keypair"); + Fixture { + acceptance, + terms: terms_signed_by(&sk), + validated: ValidatedEconomicRoot::rehydrate_from_admitted_store(POSITION, root), + ak: pk, + } +} + +/// THE REACHABILITY CLAIM, in one assertion: what the settle path emits is +/// what §7 accepts. +#[test] +fn a_produced_acceptance_verifies_through_the_seven_conjuncts() { + let f = fixture(); + let witness = verify_trader_acceptance( + &f.acceptance, + &f.terms, + B, + &correspondence(), + &f.validated, + &f.ak, + ) + .expect("the produced acceptance satisfies §7"); + assert_eq!(witness.bundle(), B, "the witness names the emitted bundle"); + assert_eq!( + witness.economic_operation_id(), + econ_op_id(), + "and the operation the write set was built for" + ); + assert_eq!(witness.economic_root(), f.validated.economic_root()); +} + +/// The producer takes `b` and the operation id from the emitted leaf, so what +/// §7 authenticates is exactly what the write set committed. +#[test] +fn the_produced_acceptance_carries_the_emitted_leaf_verbatim() { + let f = fixture(); + assert_eq!(f.acceptance.acceptance_leaf().bundle, B); + assert_eq!( + f.acceptance.acceptance_leaf().economic_operation_id, + econ_op_id() + ); + assert_eq!(f.acceptance.trader_genesis(), G); + assert_eq!(f.acceptance.economic_position(), POSITION); + assert_eq!(f.acceptance.acceptance_path().len(), 256); +} + +/// STEP 6. The artifact is untouched; the bundle being composed is a different +/// one. This is the conjunct that keeps a genuine acceptance of one bundle +/// from realizing another. +#[test] +fn a_genuine_acceptance_does_not_realize_some_other_bundle() { + let f = fixture(); + let other = [0xB1; 32]; + assert_eq!( + verify_trader_acceptance( + &f.acceptance, + &f.terms, + other, + &correspondence(), + &f.validated, + &f.ak, + ), + Err(AcceptanceInvalid::LeafNamesAnotherBundle { + leaf: B, + composing: other, + }) + ); +} + +/// ALTERED `b`. Rewriting the bundle inside the leaf changes the leaf VALUE, +/// so the path no longer folds — step 5 catches it before step 6 ever reads +/// the bundle. That ordering is 2c-D §6's point stated as a test: `b` is +/// established by the chain, not asserted by the wrapper, so there is no way +/// to substitute one without breaking the inclusion proof. +#[test] +fn an_acceptance_whose_bundle_was_rewritten_no_longer_folds() { + let f = fixture(); + let mut leaf = f.acceptance.acceptance_leaf().clone(); + leaf.bundle = [0xB1; 32]; + let forged = TraderAcceptance::new( + f.acceptance.trader_genesis(), + f.acceptance.economic_position(), + leaf, + f.acceptance.acceptance_path().to_vec(), + ) + .expect("well formed, and untrue"); + assert!(matches!( + verify_trader_acceptance( + &forged, + &f.terms, + [0xB1; 32], + &correspondence(), + &f.validated, + &f.ak, + ), + Err(AcceptanceInvalid::PathDoesNotFoldToTheValidatedRoot { .. }) + )); +} + +/// ALTERED OPERATION ID. Step 5's three-way equality fires first: the carried +/// id is not what the authenticated transition recomputes to, and the leaf key +/// is never derived from it. +#[test] +fn an_acceptance_whose_operation_id_was_rewritten_is_refused_before_the_fold() { + let f = fixture(); + let mut leaf = f.acceptance.acceptance_leaf().clone(); + leaf.economic_operation_id = [0x51; 32]; + let forged = TraderAcceptance::new( + f.acceptance.trader_genesis(), + f.acceptance.economic_position(), + leaf, + f.acceptance.acceptance_path().to_vec(), + ) + .expect("well formed, and untrue"); + assert_eq!( + verify_trader_acceptance(&forged, &f.terms, B, &correspondence(), &f.validated, &f.ak,), + Err(AcceptanceInvalid::OperationIdentityDisagrees { + leaf: [0x51; 32], + recomputed: econ_op_id(), + }) + ); +} + +/// ALTERED PATH. One swapped sibling — the smallest change that keeps the +/// artifact well-formed — and the fold lands somewhere else. +#[test] +fn an_acceptance_whose_path_was_reordered_no_longer_folds() { + let f = fixture(); + let mut path = f.acceptance.acceptance_path().to_vec(); + path.swap(0, 255); + let forged = TraderAcceptance::new( + f.acceptance.trader_genesis(), + f.acceptance.economic_position(), + f.acceptance.acceptance_leaf().clone(), + path, + ) + .expect("well formed, and untrue"); + assert!(matches!( + verify_trader_acceptance(&forged, &f.terms, B, &correspondence(), &f.validated, &f.ak,), + Err(AcceptanceInvalid::PathDoesNotFoldToTheValidatedRoot { .. }) + )); +} + +/// A PRODUCED ACCEPTANCE STILL PROVES NOTHING WITHOUT THE AUTHORITY. §7 step 2 +/// authenticates `G` against `sigma_dsm` under an INDEPENDENTLY established +/// trader AK; producing the artifact does not supply one, and a different key +/// refuses. +#[test] +fn producing_an_acceptance_does_not_supply_the_authority_that_authenticates_it() { + let f = fixture(); + let (stranger, _sk) = dsm::crypto::sphincs::generate_sphincs_keypair().expect("keypair"); + assert_eq!( + verify_trader_acceptance( + &f.acceptance, + &f.terms, + B, + &correspondence(), + &f.validated, + &stranger, + ), + Err(AcceptanceInvalid::GenesisNotAuthenticated) + ); +} + +/// THE PUBLICATION ADDRESS IS THE CANONICAL IDENTITY. `TA_B` is published +/// under `DSM/trader-settlement-acceptance/v2`, and a storage object's inner +/// digest is `H_dom(namespace, payload)` — the same computation as `ta_B`. So +/// the object a Def 14.2 receipt binds by `ta_B` and the bytes the fleet holds +/// are addressed by one value, and there is no separate locator to keep in +/// step with it. +#[test] +fn the_publication_address_is_the_canonical_identity() { + let f = fixture(); + let bytes = f.acceptance.encode().expect("encodable"); + assert_eq!( + dsm::storage_object::immutable_inner( + dsm::common::domain_tags::TAG_DSM_TRADER_SETTLEMENT_ACCEPTANCE, + &bytes, + ), + f.acceptance.ta_b().expect("identity"), + "the namespace's inner digest must be ta_B itself" + ); +} diff --git a/dsm_client/deterministic_state_machine/dsm_sdk/src/handlers/dlv_routes.rs b/dsm_client/deterministic_state_machine/dsm_sdk/src/handlers/dlv_routes.rs index 4a827243..e418df5b 100644 --- a/dsm_client/deterministic_state_machine/dsm_sdk/src/handlers/dlv_routes.rs +++ b/dsm_client/deterministic_state_machine/dsm_sdk/src/handlers/dlv_routes.rs @@ -3674,9 +3674,19 @@ impl AppRouterImpl { // so the value it holds is foreign-verifiable. What has NOT // happened: the market fold stays PartialPendingRealization, // the vault's reserves have not moved, no Def 14.2 receipt is - // published, no bundle-acceptance witness exists to construct, - // and THE FENCE IS NOT RELEASED — Ruling V3 gates release on a - // certifying verdict, and ordinary DSM advancement is not one. + // published, and THE FENCE IS NOT RELEASED — Ruling V3 gates + // release on a certifying verdict, and ordinary DSM + // advancement is not one. + // + // The admission above now also PUBLISHES the canonical `TA_B` + // for `b` (2c-D §6, producer adoption), so the sentence that + // used to stand here — "no bundle-acceptance witness exists to + // construct" — would be read as still true and is not. Stated + // exactly: the ARTIFACT exists and is fetchable; the WITNESS + // does not, because `BundleAcceptanceWitness` has one + // constructor and it is 2c-D §7's verifier, which nothing on + // this path calls. Constructing one additionally needs the + // composed bundle and an independently established trader AK. pack_envelope_ok(generated::envelope::Payload::AppStateResponse( generated::AppStateResponse { key: "dlv.unlockRouted".to_string(), @@ -8402,6 +8412,164 @@ mod funded_creation_tests { ); } + /// 2c-D PRODUCER ADOPTION, OVER THE LIVE ROUTE: a settled market publishes + /// the canonical `TA_B` for the bundle it accepted — and realizes nothing. + /// + /// The two halves are asserted together on purpose. Producing `TA_B` is the + /// step that makes realization *possible*, so the risk it introduces is + /// that something starts treating the artifact's existence as permission. + /// A test that only proved publication would not notice; a test that only + /// proved nothing released would pass just as well before the producer + /// existed. Paired, the failure mode is visible. + /// + /// `TA_B` is read back as BYTES. The type has no decoder — deliberately, + /// because a decoder without §7 behind it invites reading a decoded + /// acceptance as an accepted one — so the field offsets of registry §5.40 + /// are what this test reads, and `b` is taken from the fence the binding + /// froze rather than from the artifact being checked. + #[test] + #[serial] + fn a_settled_market_publishes_a_trader_acceptance_and_realizes_nothing() { + install_identity(); + let (vault_id, (pc_a, pc_b), _owner_dev, traders) = + market_with_traders("sofi/spec/ta-b", &[("trader0", 0x51)]); + + let trader_dev = &traders[0]; + trader_dev.enter(); + let trader = trader_dev.router(); + // The parent the fence will be keyed by, captured BEFORE the advance: + // a successful settle moves the trader's chain tip, so reading the + // fence afterwards from the CURRENT tip finds nothing and would make + // the release assertion below vacuous. + let before = trader.core_sdk.device_head().expect("trader head"); + let rel_key = dsm::core::bilateral_transaction_manager::compute_smt_key( + &trader_dev.device_id, + &trader_dev.device_id, + ); + let fenced_parent = before.chain_tip(&rel_key).unwrap_or_else(|| { + dsm::core::bilateral_transaction_manager::initial_chain_tip_from_device_ids( + &trader_dev.device_id, + &trader_dev.device_id, + ) + }); + + let (res, x) = trader_settles( + trader, + &trader_dev.ak_pk.clone(), + &trader_dev.device_id, + &vault_id, + &pc_a, + &pc_b, + 0, + (10_000, 5_000), + 1_000, + crate::sdk::routing_path_sdk::constant_product_output(1_000, 10_000, 5_000, 30) + .expect("curve output"), + 0x61, + ); + assert!(res.success, "the settle binds: {:?}", res.error_message); + + // `b`, from the fence the binding transaction froze — tx_id is the + // bundle digest. An independent source from the artifact under test. + let fence = + crate::storage::client_db::trader_parent_fence::active_fence(&rel_key, &fenced_parent) + .expect("fence read") + .expect("the settle fenced the trader's own parent"); + let b = fence.tx_id; + + // ── THE ARTIFACT REACHED THE FLEET ────────────────────────────── + let keys: std::collections::BTreeSet = + crate::sdk::storage_io::fake_fleet::put_log() + .into_iter() + .map(|(_, key, _)| key) + .filter(|k| k.starts_with("immutable::DSM/trader-settlement-acceptance/v2::")) + .collect(); + assert_eq!( + keys.len(), + 1, + "a settled market publishes exactly one trader acceptance: {keys:?}" + ); + let key = keys.iter().next().expect("one key").clone(); + let bytes = &crate::sdk::storage_io::fake_fleet::any_member_holding(&key) + .expect("the acceptance is held by a member"); + assert_eq!( + bytes.len(), + dsm::economic::trader_acceptance::TRADER_ACCEPTANCE_LEN, + "registry §5.40 pins 8,308 bytes" + ); + assert_eq!(&bytes[0..4], &[0x00, 0x11, 0x00, 0x01], "0x0011 schema 1"); + assert_eq!( + &bytes[4..36], + before.genesis_digest().as_slice(), + "the trader's own genesis, not a carried stranger's" + ); + assert_eq!( + &bytes[44..48], + &[0x00, 0x32, 0x00, 0x01], + "field 3 is the complete nested 0x0032 CCB, envelope included" + ); + assert_eq!( + &bytes[48..80], + b.as_slice(), + "the acceptance leaf commits the EXACT bundle the binding froze" + ); + assert_eq!( + &bytes[112..116], + &[0x00, 0x00, 0x01, 0x00], + "u32_be(256) precedes the siblings" + ); + // CONTENT-ADDRESSED, and by its own canonical identity: the key is + // derived from these exact bytes under this namespace, whose inner + // digest IS `ta_B` (pinned by + // `the_publication_address_is_the_canonical_identity`). So a Def 14.2 + // receipt binding `ta_B` names the object at this key. + assert_eq!( + key, + format!( + "immutable::DSM/trader-settlement-acceptance/v2::{}", + crate::util::text_id::encode_base32_crockford( + &dsm::storage_object::immutable_addr( + dsm::common::domain_tags::TAG_DSM_TRADER_SETTLEMENT_ACCEPTANCE, + bytes, + ) + ) + ), + "the published key must be the content address of the published bytes" + ); + + // ── AND NOTHING WAS REALIZED ──────────────────────────────────── + assert!( + matches!( + fence.state, + dsm::dlv::trader_fence::FenceState::CommittedAwaitingAcceptance { .. } + ), + "the fence is committed and awaiting acceptance, not Released ({:?})", + fence.state + ); + assert!( + matches!( + crate::runtime::get_runtime().block_on( + crate::sdk::settlement_receipt_codec::fetch_verified_receipt(&vault_id, &x) + ), + crate::sdk::settlement_receipt_codec::ReceiptFetch::Absent + ), + "no Def 14.2 receipt was published merely because TA_B exists" + ); + let after = composed_frontier(&vault_id, &pc_a, &pc_b); + match after.frontier_binding { + crate::sdk::vault_state_composition::FrontierBinding::BoundUnrealized { + route_set_commitment, + .. + } => assert_eq!(route_set_commitment, x, "still bound, still unrealized"), + other => panic!("expected BoundUnrealized, got {other:?}"), + } + assert_eq!( + (after.sequence, after.reserves_a, after.reserves_b), + (0, 10_000, 5_000), + "the frontier stops AT the bound parent: reserves move on realization" + ); + } + /// below still go through the route. #[test] #[serial] diff --git a/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/economic_admission_flow.rs b/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/economic_admission_flow.rs index 68396756..09c011cf 100644 --- a/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/economic_admission_flow.rs +++ b/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/economic_admission_flow.rs @@ -1046,6 +1046,55 @@ fn economic_proof_artifact_for( ))) } +/// The canonical `TA_B` for a transition that accepted a settlement bundle, as +/// a publishable artifact — or `None` when it accepted none. +/// +/// **Built here for the same reason the inclusion proof is.** `TA_B` carries +/// the acceptance leaf's path under `R_T^+`, and a mutation's own captured +/// siblings are not that path: they hold with the earlier mutations applied, +/// and the acceptance leaf is not last in key order by any rule. So the path +/// must come from `tree` — the finished post-transition tree whose root was +/// just registered — in the same single snapshot, with no second read and no +/// window in which the tree could move. +/// +/// **What publishing it does not do.** A `TA_B` on the fleet is an artifact a +/// verifier can fetch. It realizes nothing: no fence is released, no frontier +/// advances, no market fold leaves `PartialPendingRealization`, and nothing +/// here constructs a `BundleAcceptanceWitness` — that type's only constructor +/// is 2c-D §7's verifier, which needs the composed bundle and an +/// independently established trader AK that this path does not have. +fn trader_acceptance_artifact_for( + tree: &EconomicSmt, + witness: &EconomicTransitionWitness, + genesis: &[u8; 32], + devid: &[u8; 32], + validated: &ValidatedEconomicRoot, +) -> Result, &'static str)>, DsmError> { + let Some(acceptance) = dsm::economic::acceptance_produce::produce_trader_acceptance( + tree, + witness, + genesis, + devid, + validated.economic_root(), + validated.economic_position(), + ) + .map_err(|e| DsmError::invalid_operation(format!("trader acceptance: {e}")))? + else { + return Ok(None); + }; + let bytes = acceptance + .encode() + .map_err(|e| storage_err("trader acceptance encode", e))?; + Ok(Some(( + crate::sdk::economic_registers::immutable_object_key( + dsm::common::domain_tags::TAG_DSM_TRADER_SETTLEMENT_ACCEPTANCE, + &bytes, + ), + bytes, + "trader-settlement-acceptance", + ))) +} + /// Everything after local acceptance. Separated so recovery re-enters here. #[allow(clippy::too_many_arguments)] pub(crate) async fn finish_admission( @@ -1245,6 +1294,37 @@ pub(crate) async fn finish_admission( None => None, }; + // ── THE TRADER ACCEPTANCE for the bundle this transition accepted ──── + // + // Same snapshot, same reason: `TA_B`'s path is the acceptance leaf's + // under the root just registered, and only `tree` holds it. Built after + // the inclusion proof so both come from one tree that has been shown to + // be the registered one. + // + // `None` here is the ordinary answer — every non-settlement accepts no + // bundle. A market settle always writes exactly one acceptance leaf + // (2c-D §8's producer-adoption cardinality, enforced by the write set), + // so a settle reaching `None` would mean the leaf was never emitted, and + // `build_write_set` refuses that long before this line. + // + // A CRASH-RESUMED admission reaches here too, and produces the SAME + // artifact: `resume_pending_admission` replays the frozen witness onto + // the validated pre-tree, so the tree, the witness and the root are the + // ones the first attempt had. The object is content-addressed, so the + // republish is idempotent — and if a replay ever diverged, the root guard + // inside the producer would refuse rather than publish a path that folds + // to a root nothing registered. + // No locator is returned. `ta_B` IS the object's inner address under this + // namespace, and a Def 14.2 receipt binds `ta_B` — so a consumer already + // has the address from the artifact that names it, and a second copy + // threaded through the admission outcome would be one more place for it + // to disagree. PR C adds a fetch path together with the code that reads it. + if let Some((key, bytes, purpose)) = + trader_acceptance_artifact_for(&tree, &witness, &genesis, &devid, &new_validated)? + { + post_admit_artifacts.push((key, bytes, purpose)); + } + let had_post_admit = !post_admit_artifacts.is_empty(); core.admit_economic_position( new_validated.economic_position(),