From 912c1aee3f1a3bccd97427efd0c36d65ad6ed9f1 Mon Sep 17 00:00:00 2001 From: Cryptskii <47649969+cryptskii@users.noreply.github.com> Date: Thu, 10 Sep 2026 22:42:36 -0400 Subject: [PATCH] =?UTF-8?q?fix(economic):=20decode=20the=200x0032=20bundle?= =?UTF-8?q?-acceptance=20leaf=20=E2=80=94=20a=20trader=20who=20settled=20c?= =?UTF-8?q?ould=20admit=20nothing=20again?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #852 made the bundle-acceptance leaf (0x0032) a mandatory part of every market settle's economic write set. It added the fifth EconomicLeafState arm's encoder, key, value, and even its arm in the pre-state match — but not its DECODER arm. read_leaf_state fell through to WrongClass for 0x0032. So since #852 every structure that decodes a settle's leaves failed: producer_tree_and_pre_state every admission rebuilds its pre-state from the leaf cache, which after a settle holds the acceptance leaf — so a trader who settled once could admit NOTHING again: no send, no second trade resume_pending_admission a crash-resumed settle could not decode its own frozen witness peer lineage walk any peer walking a trader THROUGH a settle read the undecodable witness as INVALID economic proof artifacts a settle's inclusion proof (the acceptance leaf is citable) did not decode The first is established by execution, not by reading. With the arm removed, the new end-to-end test fails on a genuine settle with: decode cached leaf state: unexpected object class 0x0032 Found while implementing the realization cutover (C2), whose walk and TA_B decoder both go through this function. Landed separately and first: it is a live defect on main and a separate topic. THE FIX mirrors the encoder exactly — envelope and two digest32, no rejection — because the encoder refuses nothing, and a decoder stricter than its encoder makes valid leaves unreadable. THE GUARD. every_leaf_class_round_trips_through_the_decoder matches every EconomicLeafState arm with no wildcard: a sixth arm fails to COMPILE there until a sample is added, and the sample fails at runtime until the decoder handles it. #852 slipped because no test ever decoded the leaf it added. Controls, each executed red with the arm removed, green with it: every_leaf_class_round_trips_through_the_decoder wire a_settle_witness_carrying_the_acceptance_leaf_decodes_to_itself REAL settle witness an_artifact_proving_a_bundle_acceptance_leaf_decodes_and_verifies proof artifact a_trader_that_settled_can_still_build_its_next_pre_state live route, end to end Dependent sweep: nothing asserted the old failure. Verification: economic_provenance_wire 18/18 trader_acceptance_producer 9/9 economic::proof_artifact 7/7 dlv_routes new end-to-end control 1/1 make lint exit 0 ci/production_safety_checks.sh exit 0 --- .../dsm/src/economic/decode.rs | 19 +++++++ .../dsm/src/economic/proof_artifact.rs | 20 +++++++ .../dsm/tests/economic_provenance_wire.rs | 53 ++++++++++++++++++- .../dsm/tests/trader_acceptance_producer.rs | 21 ++++++++ .../dsm_sdk/src/handlers/dlv_routes.rs | 45 ++++++++++++++++ 5 files changed, 156 insertions(+), 2 deletions(-) diff --git a/dsm_client/deterministic_state_machine/dsm/src/economic/decode.rs b/dsm_client/deterministic_state_machine/dsm/src/economic/decode.rs index 5bd81cca..134700ae 100644 --- a/dsm_client/deterministic_state_machine/dsm/src/economic/decode.rs +++ b/dsm_client/deterministic_state_machine/dsm/src/economic/decode.rs @@ -335,6 +335,25 @@ fn read_leaf_state(c: &mut Cursor<'_>) -> Result }, )) } + // The fifth arm (2c-D, `0x0032`). Missing from #852, which added the + // encoder, the key, the value and the pre-state match arm but not + // this one — so every market settle's witness, inclusion proof and + // cached leaf became undecodable. Mirrors the encoder exactly: two + // `digest32` and no rejection, because the encoder refuses nothing + // and a decoder stricter than its encoder makes valid leaves + // unreadable. + class::ECONOMIC_BUNDLE_ACCEPTANCE_STATE => { + c.envelope( + crate::economic::state::EconomicBundleAcceptanceState::CLASS, + crate::economic::state::EconomicBundleAcceptanceState::SCHEMA, + )?; + Ok(EconomicLeafState::BundleAcceptance( + crate::economic::state::EconomicBundleAcceptanceState { + bundle: c.digest32()?, + economic_operation_id: c.digest32()?, + }, + )) + } got => Err(DecodeError::WrongClass { got }), } } diff --git a/dsm_client/deterministic_state_machine/dsm/src/economic/proof_artifact.rs b/dsm_client/deterministic_state_machine/dsm/src/economic/proof_artifact.rs index 08582713..b487790b 100644 --- a/dsm_client/deterministic_state_machine/dsm/src/economic/proof_artifact.rs +++ b/dsm_client/deterministic_state_machine/dsm/src/economic/proof_artifact.rs @@ -442,4 +442,24 @@ mod tests { decode_economic_proof_artifact(&wrong.encode_to_vec()).expect("shape is still valid"); assert!(decoded.verify_against(&G, &D, 7, &tree.root()).is_err()); } + + /// #852 REGRESSION. A settle's inclusion proof carries the bundle-acceptance + /// leaf (it is citable: `TA_B` asks a stranger to verify it), so the + /// artifact must decode and verify with that leaf in it. + #[test] + fn an_artifact_proving_a_bundle_acceptance_leaf_decodes_and_verifies() { + let acceptance = EconomicLeafState::BundleAcceptance( + crate::economic::state::EconomicBundleAcceptanceState { + bundle: [0xB0; 32], + economic_operation_id: [0x50; 32], + }, + ); + let (tree, artifact) = published(&[reserve(0xA1, 10_000, 0), acceptance]); + let decoded = decode_economic_proof_artifact(&artifact.encode()) + .expect("a settle's proof artifact decodes"); + decoded + .verify_against(&G, &D, 7, &tree.root()) + .expect("and verifies against the root the reader names"); + assert_eq!(decoded.states().count(), 2); + } } diff --git a/dsm_client/deterministic_state_machine/dsm/tests/economic_provenance_wire.rs b/dsm_client/deterministic_state_machine/dsm/tests/economic_provenance_wire.rs index a3075b78..0296c0d4 100644 --- a/dsm_client/deterministic_state_machine/dsm/tests/economic_provenance_wire.rs +++ b/dsm_client/deterministic_state_machine/dsm/tests/economic_provenance_wire.rs @@ -23,8 +23,8 @@ use dsm::economic::credit::{ use dsm::economic::decode::{decode_credit_source, decode_leaf_state, decode_transition_witness}; use dsm::economic::mutation::EconomicLeafMutation; use dsm::economic::state::{ - EconomicBalanceState, EconomicConsumedSourceState, EconomicLeafState, - EconomicSettlementReceiptState, EconomicVaultReserveState, + EconomicBalanceState, EconomicBundleAcceptanceState, EconomicConsumedSourceState, + EconomicLeafState, EconomicSettlementReceiptState, EconomicVaultReserveState, }; use dsm::economic::tree::ECONOMIC_SMT_HEIGHT; use dsm::economic::witness::EconomicTransitionWitness; @@ -349,6 +349,55 @@ fn a_receipt_cannot_assert_a_receipt_id_its_contents_do_not_produce() { } } +/// EVERY leaf class the tree can hold decodes back to itself. +/// +/// The `match` has no wildcard ON PURPOSE: a sixth `EconomicLeafState` arm +/// fails to COMPILE here until a sample of it is added — and the sample then +/// fails at runtime until the decoder has an arm for it. #852 added the fifth +/// arm's encoder, key, value and pre-state handling but not its decoder, and +/// nothing noticed, because no test decoded one. This is that test. +#[test] +fn every_leaf_class_round_trips_through_the_decoder() { + let samples = [ + EconomicLeafState::Balance(EconomicBalanceState::new(ERA, 10).expect("balance")), + EconomicLeafState::VaultReserve(EconomicVaultReserveState { + vault_id: VAULT, + policy_commit: ERA, + amount: 5, + vault_sequence: 2, + }), + EconomicLeafState::SettlementReceipt( + EconomicSettlementReceiptState::new(VAULT, [7; 32], 4, 5, ERA, 10, SOFI, 9) + .expect("valid"), + ), + EconomicLeafState::ConsumedSource(EconomicConsumedSourceState { + source_id: [1; 32], + consumer_economic_operation_id: [2; 32], + }), + EconomicLeafState::BundleAcceptance(EconomicBundleAcceptanceState { + bundle: [0xB0; 32], + economic_operation_id: [0x50; 32], + }), + ]; + let mut covered = [false; 5]; + for state in &samples { + let slot = match state { + EconomicLeafState::Balance(_) => 0, + EconomicLeafState::VaultReserve(_) => 1, + EconomicLeafState::SettlementReceipt(_) => 2, + EconomicLeafState::ConsumedSource(_) => 3, + EconomicLeafState::BundleAcceptance(_) => 4, + }; + covered[slot] = true; + let bytes = state.encode().expect("encodable"); + assert_eq!( + &decode_leaf_state(&bytes).expect("every leaf the encoder emits must decode"), + state + ); + } + assert!(covered.iter().all(|c| *c), "one sample per leaf class"); +} + // ── The manifest's derived provenance index ──────────────────────────────── fn manifest(addrs: Vec<[u8; 32]>) -> EconomicAdmissionManifest { 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 index b4350e8d..e4b247bf 100644 --- a/dsm_client/deterministic_state_machine/dsm/tests/trader_acceptance_producer.rs +++ b/dsm_client/deterministic_state_machine/dsm/tests/trader_acceptance_producer.rs @@ -409,3 +409,24 @@ fn the_publication_address_is_the_canonical_identity() { "the namespace's inner digest must be ta_B itself" ); } + +/// #852 REGRESSION. The settle's own witness — what the resume path decodes +/// from frozen state and what every foreign lineage walk decodes from the +/// fleet — carries the bundle-acceptance leaf, and must decode to itself. +#[test] +fn a_settle_witness_carrying_the_acceptance_leaf_decodes_to_itself() { + let (witness, _) = settled(); + assert!( + witness + .mutations + .iter() + .any(|m| matches!(m.post_state, Some(EconomicLeafState::BundleAcceptance(_)))), + "the fixture must actually carry the leaf, or this test proves nothing" + ); + let bytes = witness.encode().expect("encodable"); + assert_eq!( + dsm::economic::decode::decode_transition_witness(&bytes) + .expect("a market settle's witness decodes"), + witness + ); +} 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 e418df5b..84ee2f52 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 @@ -8412,6 +8412,51 @@ mod funded_creation_tests { ); } + /// #852 REGRESSION, END TO END: a trader that settled must still be able to + /// start its NEXT economic admission. + /// + /// Every admission begins by rebuilding its pre-state from this device's + /// leaf cache, and after a market settle that cache holds the + /// bundle-acceptance leaf. With no decoder arm for it the rebuild failed + /// at the first cached leaf, so a trader who settled once could admit + /// nothing — no send, no second trade — ever again. + #[test] + #[serial] + fn a_trader_that_settled_can_still_build_its_next_pre_state() { + install_identity(); + let (vault_id, (pc_a, pc_b), _owner_dev, traders) = + market_with_traders("sofi/spec/next-pre-state", &[("trader0", 0x51)]); + let trader_dev = &traders[0]; + trader_dev.enter(); + let trader = trader_dev.router(); + 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"), + 0x71, + ); + assert!(res.success, "the settle binds: {:?}", res.error_message); + + let validated = + crate::sdk::economic_admission_flow::validated_root_or_activate(&trader.core_sdk) + .expect("the settle admitted a root"); + let (_tree, pre) = + crate::sdk::economic_admission_flow::producer_tree_and_pre_state(&validated) + .expect("the next admission can rebuild its pre-state from the leaf cache"); + assert!( + !pre.balances.is_empty(), + "and the rebuilt pre-state carries the trader's balances" + ); + } + /// 2c-D PRODUCER ADOPTION, OVER THE LIVE ROUTE: a settled market publishes /// the canonical `TA_B` for the bundle it accepted — and realizes nothing. ///