diff --git a/dsm_client/deterministic_state_machine/dsm/src/ccb/settlement.rs b/dsm_client/deterministic_state_machine/dsm/src/ccb/settlement.rs index b59de87c..44b75e07 100644 --- a/dsm_client/deterministic_state_machine/dsm/src/ccb/settlement.rs +++ b/dsm_client/deterministic_state_machine/dsm/src/ccb/settlement.rs @@ -698,12 +698,133 @@ pub mod fixtures { } } - /// Syntactically valid market terms whose route consumes `parent` and - /// whose route-set commitment is `x` — what ties a bound bundle to a - /// trade in every consumer. + /// The trader's fixture identity. A market settle advances the trader's + /// SELF-LOOP, so the counterparty devid IS the trader's own. + pub const FIXTURE_TRADER_GENESIS: [u8; 32] = [0x41; 32]; + pub const FIXTURE_TRADER_DEVID: [u8; 32] = [0x42; 32]; + pub const FIXTURE_REL_KEY: [u8; 32] = [0x51; 32]; + pub const FIXTURE_TRADER_PARENT: [u8; 32] = [0x52; 32]; + pub const FIXTURE_ENTROPY: [u8; 32] = [0x55; 32]; + + /// The trader's deterministic fixture keypair. Signing SPX256f is not free, + /// so it is derived once. + fn fixture_keypair() -> &'static crate::crypto::sphincs::SphincsKeyPair { + static KP: std::sync::OnceLock = + std::sync::OnceLock::new(); + KP.get_or_init(|| { + crate::crypto::sphincs::generate_keypair_from_seed( + crate::crypto::sphincs::SphincsVariant::SPX256f, + &[0x57; 32], + ) + .expect("fixture keypair") + }) + } + + /// The settle this trade actually is, signed. Every field is coherent with + /// the route leg and the successor the bundle carries: same pair, same + /// amounts, same fee, same parent. + fn fixture_signed_settle(parent: [u8; 32], x: [u8; 32]) -> crate::types::operations::Operation { + use crate::types::operations::{Operation, TransactionMode}; + let kp = fixture_keypair(); + let unsigned = Operation::DlvSettle { + vault_id: [0x03; 32].to_vec(), + owner_public_key: vec![0x02; 64], + owner_devid: [0x02; 32], + owner_genesis: [0x01; 32], + input_policy_commit: [0x10; 32], + output_policy_commit: [0x20; 32], + parent_sequence: 7, + parent_binding: parent, + route_commit_bytes: vec![0x09; 5], + external_commitment_x: x, + input_amount: 10_000, + output_amount: 4_935, + fee_bps: 30, + sigma: [0x00; 32], + settler_public_key: kp.public_key.clone(), + settler_devid: FIXTURE_TRADER_DEVID, + settlement_receipt_id: [0x11; 32], + signature: Vec::new(), + mode: TransactionMode::Unilateral, + }; + // The settler signs the canonical bytes with field 18 cleared, then + // writes the signature back — which is why a settle cannot be signed + // after the advance. + let sig = crate::crypto::sphincs::sphincs_sign(&kp.secret_key, &unsigned.to_bytes()) + .expect("fixture settle signature"); + match unsigned { + Operation::DlvSettle { + vault_id, + owner_public_key, + owner_devid, + owner_genesis, + input_policy_commit, + output_policy_commit, + parent_sequence, + parent_binding, + route_commit_bytes, + external_commitment_x, + input_amount, + output_amount, + fee_bps, + sigma, + settler_public_key, + settler_devid, + settlement_receipt_id, + mode, + .. + } => Operation::DlvSettle { + vault_id, + owner_public_key, + owner_devid, + owner_genesis, + input_policy_commit, + output_policy_commit, + parent_sequence, + parent_binding, + route_commit_bytes, + external_commitment_x, + input_amount, + output_amount, + fee_bps, + sigma, + settler_public_key, + settler_devid, + settlement_receipt_id, + signature: sig, + mode, + }, + _ => unreachable!("constructed as DlvSettle"), + } + } + + /// Market terms whose route consumes `parent` and whose route-set + /// commitment is `x`. + /// + /// NOTHING HERE IS INVENTED ANY MORE (5c-2 Step 2). `operation_bytes` is a + /// real signed `DlvSettleOperationPreimageV1`, `trader_successor` is + /// `relationship_chain_tip_v2` recomputed over exactly those bytes, and + /// `sigma_dsm` is a real SPHINCS+ signature over the substrate digest — + /// all produced by [`crate::dlv::market_producer`], which is the only way + /// to obtain them. The previous fixture asserted a 300-byte filler and a + /// byte-pattern signature, which could not have satisfied 2c-B's `G1`-`G4`. pub fn market_terms(parent: [u8; 32], x: [u8; 32]) -> MarketTerms { - MarketTerms { - intent: TradeIntent { + let settle = fixture_signed_settle(parent, x); + let prepared = crate::dlv::market_producer::prepare_market_successor( + FIXTURE_REL_KEY, + FIXTURE_TRADER_PARENT, + FIXTURE_TRADER_DEVID, + &settle, + FIXTURE_ENTROPY, + &crate::dlv::market_producer::TraderIdentity { + genesis: FIXTURE_TRADER_GENESIS, + device_id: FIXTURE_TRADER_DEVID, + }, + &fixture_keypair().secret_key, + ) + .expect("fixture market successor"); + crate::dlv::market_producer::market_terms( + TradeIntent { token_in: [0x10; 32], amount_in: 10_000, token_out: [0x20; 32], @@ -711,8 +832,8 @@ pub mod fixtures { fee_bps: 30, nonce: [0x5E; 32], }, - route_set_commitment: x, - selected_route: Route::new(vec![RouteLeg::Single(Allocation { + x, + Route::new(vec![RouteLeg::Single(Allocation { parent_binding: parent, delta_in: 10_000, delta_out: 4_935, @@ -720,18 +841,9 @@ pub mod fixtures { fee_policy: FeePolicy::new(30).unwrap(), })]) .unwrap(), - trader_parent: [0x52; 32], - trader_successor: [0x59; 32], - recovery_material: DsmSuccessorEvidence::new( - [0x51; 32], - [0x52; 32], - [0x53; 32], - vec![0x1A; 300], - [0x55; 32], - signature_bytes(0x57), - ) - .unwrap(), - } + &prepared, + ) + .expect("fixture market terms") } /// A canonical market bundle consuming `parent` into `successor` under @@ -809,6 +921,9 @@ mod tests { vec![0xA5; SPX256F_SIGNATURE_LEN] } + /// Synthetic on purpose, and legitimately so: these two call sites encode + /// it to check the `0x0031` ENVELOPE AND FIELD ORDER, not to stand in for a + /// produced bundle. Nothing derived from it is offered as a market bundle. fn evidence() -> DsmSuccessorEvidence { DsmSuccessorEvidence::new( [0x51; 32], @@ -821,17 +936,6 @@ mod tests { .unwrap() } - fn intent() -> TradeIntent { - TradeIntent { - token_in: [0x10; 32], - amount_in: 10_000, - token_out: [0x20; 32], - exact_out: 4_935, - fee_bps: 30, - nonce: [0x5E; 32], - } - } - fn allocation(parent: [u8; 32]) -> Allocation { Allocation { parent_binding: parent, @@ -842,15 +946,11 @@ mod tests { } } + /// Producer-derived, like every other market bundle in the tree since + /// 5c-2 Step 2. The tests below that need a MISMATCH build it by mutating + /// this, never by inventing a second set of operands. fn terms(parent: [u8; 32]) -> MarketTerms { - MarketTerms { - intent: intent(), - route_set_commitment: [0x58; 32], - selected_route: Route::new(vec![RouteLeg::Single(allocation(parent))]).unwrap(), - trader_parent: [0x52; 32], - trader_successor: [0x59; 32], - recovery_material: evidence(), - } + fixtures::market_terms(parent, [0x58; 32]) } // ── the worked owner-close layout, byte for byte where 2c-A pins it ── diff --git a/dsm_client/deterministic_state_machine/dsm/src/dlv/market_producer.rs b/dsm_client/deterministic_state_machine/dsm/src/dlv/market_producer.rs new file mode 100644 index 00000000..729ebee2 --- /dev/null +++ b/dsm_client/deterministic_state_machine/dsm/src/dlv/market_producer.rs @@ -0,0 +1,356 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! THE GENUINE MARKET BUNDLE PRODUCER — 5c-2 Step 2. +//! +//! Every operand of a market `MarketTerms` is DERIVED here from a real signed +//! operation. Nothing is invented, and there is no parameter through which a +//! caller could invent one: +//! +//! ```text +//! operation_bytes = the caller's SIGNED Operation::DlvSettle, canonically +//! encoded. Refused unless it is a settle carrying a +//! signature — an unsigned operation cannot be signed +//! afterwards, because the signature is inside the bytes +//! the chain tip hashes. +//! trader_successor = relationship_chain_tip_v2(...) over THOSE bytes. +//! RECOMPUTED, never accepted from a caller. +//! trader_parent = the same embedded parent the tip was computed from, so +//! 2c-B's second chain-tip equality holds by construction +//! rather than by a check that could be forgotten. +//! sigma_dsm = SPHINCS+ over +//! H_dom(DSM/economic-substrate-sign, +//! G ‖ DevID ‖ C_dsm+ ‖ operation_digest) +//! — the same digest chain the economic substrate's own +//! producer signs, so one accepted successor has one +//! signature and not two disagreeing ones. +//! ``` +//! +//! WHY THIS IS NOT `economic::successor_evidence`. That module signs the same +//! digest and then encodes **protobuf**. Registry §2.10 forbids protobuf +//! transport bytes from being hashed or signed as a CCB blob, and 2c-B records +//! that `DsmSuccessorEvidenceV1` "violates it twice" — a content address over +//! prost bytes, and prost determinism used AS the canonical form. The new class +//! "must not be the existing prost bytes canonized". So the signing chain is +//! shared and the encoding is not. +//! +//! WHAT THIS MODULE DOES NOT DO. It does not bind, fence, advance, admit or +//! publish anything, and it holds no I/O and no runtime state — `dlv/`'s +//! charter. It does not lift the market emission refusal: wiring the live path +//! is 5c-2 Step 3, and realization stays unreachable until 2c-D supplies the +//! bundle-acceptance witness. Producing a bundle is not settling a trade. + +use crate::ccb::settlement::{DsmSuccessorEvidence, MarketTerms, Route, TradeIntent}; +use crate::ccb::CcbError; +use crate::types::device_state::relationship_chain_tip_v2; +use crate::types::operations::{Operation, TransactionMode}; + +/// Why a market bundle could not be produced. Each arm names a fact about the +/// caller's inputs, never a repair. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ProducerError { + /// The operation is not a `DlvSettle`. Discriminator 26 is what 2c-B's + /// `G3` requires, and it is a property of the operation, not of the bytes. + NotASettle, + /// The settle carries no signature. The settler signs with field 18 + /// cleared and writes the signature back, so an operation committed + /// unsigned can never be signed afterwards — the signature is inside + /// `operation_bytes` and therefore inside `C_dsm+`. + Unsigned, + /// `G3` also fixes the mode. A bilateral settle is not this shape. + NotUnilateral, + /// Signing failed. + Signature(String), + /// The evidence or the terms would not encode. + Encoding(CcbError), +} + +impl core::fmt::Display for ProducerError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::NotASettle => write!( + f, + "market production needs an Operation::DlvSettle; discriminator 26 is 2c-B's G3" + ), + Self::Unsigned => write!( + f, + "the settle carries no signature, and it cannot acquire one later: the signature \ + is inside the bytes the chain tip hashes" + ), + Self::NotUnilateral => write!(f, "2c-B's G3 fixes the settle's mode to Unilateral"), + Self::Signature(e) => write!(f, "sigma_dsm could not be produced: {e}"), + Self::Encoding(e) => write!(f, "the produced object does not encode: {e:?}"), + } + } +} + +impl std::error::Error for ProducerError {} + +/// The trader's economic coordinates. Both are the trader's OWN; a market +/// settle advances the trader's self-loop and the vault owner never signs it. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct TraderIdentity { + pub genesis: [u8; 32], + pub device_id: [u8; 32], +} + +/// A prepared successor: the evidence, and the two coordinates `MarketTerms` +/// must carry. Constructing this is the only way to obtain them, so they +/// cannot come apart. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PreparedSuccessor { + evidence: DsmSuccessorEvidence, + trader_parent: [u8; 32], + trader_successor: [u8; 32], +} + +impl PreparedSuccessor { + pub fn evidence(&self) -> &DsmSuccessorEvidence { + &self.evidence + } + /// The exact `C_dsm+`, recomputed from the signed operation bytes. + pub fn trader_successor(&self) -> [u8; 32] { + self.trader_successor + } + pub fn trader_parent(&self) -> [u8; 32] { + self.trader_parent + } +} + +/// Prepare the successor for a market settle. +/// +/// `signed_settle` must already carry its signature: the settler signs the +/// canonical bytes with field 18 cleared and writes the signature back, and +/// only then are the bytes the chain tip hashes final. +#[allow(clippy::too_many_arguments)] +pub fn prepare_market_successor( + rel_key: [u8; 32], + embedded_parent: [u8; 32], + counterparty_devid: [u8; 32], + signed_settle: &Operation, + entropy: [u8; 32], + identity: &TraderIdentity, + ak_secret_key: &[u8], +) -> Result { + match signed_settle { + Operation::DlvSettle { + signature, mode, .. + } => { + if signature.is_empty() { + return Err(ProducerError::Unsigned); + } + if *mode != TransactionMode::Unilateral { + return Err(ProducerError::NotUnilateral); + } + } + _ => return Err(ProducerError::NotASettle), + } + + let operation_bytes = signed_settle.to_bytes(); + + // RECOMPUTED, never supplied. `encapsulated_entropy` is absent in this + // profile and the 0x0031 encoder emits its absence marker. + let trader_successor = relationship_chain_tip_v2( + &rel_key, + &embedded_parent, + &counterparty_devid, + &operation_bytes, + &entropy, + None, + ); + + let operation_digest = crate::economic::faucet::dsm_operation_digest(&operation_bytes); + let digest = crate::economic::successor_evidence::substrate_signing_digest( + &identity.genesis, + &identity.device_id, + &trader_successor, + &operation_digest, + ); + let sigma_dsm = crate::crypto::sphincs::sphincs_sign(ak_secret_key, &digest) + .map_err(|e| ProducerError::Signature(e.to_string()))?; + + let evidence = DsmSuccessorEvidence::new( + rel_key, + embedded_parent, + counterparty_devid, + operation_bytes, + entropy, + sigma_dsm, + ) + .map_err(ProducerError::Encoding)?; + + Ok(PreparedSuccessor { + evidence, + trader_parent: embedded_parent, + trader_successor, + }) +} + +/// Assemble `MarketTerms` around a prepared successor. +/// +/// The two trader coordinates are taken from `prepared` and cannot be +/// overridden, so `MarketTerms::check_evidence_linkage` holds by construction. +pub fn market_terms( + intent: TradeIntent, + route_set_commitment: [u8; 32], + selected_route: Route, + prepared: &PreparedSuccessor, +) -> Result { + let terms = MarketTerms { + intent, + route_set_commitment, + selected_route, + trader_parent: prepared.trader_parent, + trader_successor: prepared.trader_successor, + recovery_material: prepared.evidence.clone(), + }; + terms.check_evidence_linkage()?; + Ok(terms) +} + +#[cfg(test)] +#[allow(clippy::disallowed_methods, clippy::unwrap_used, clippy::expect_used)] +mod tests { + use super::*; + use crate::ccb::settlement::fixtures; + + /// The produced bundle's operands, built once — SPX256f signing is not free. + fn produced() -> &'static MarketTerms { + static T: std::sync::OnceLock = std::sync::OnceLock::new(); + T.get_or_init(|| fixtures::market_terms([0xC0; 32], [0x58; 32])) + } + + /// `G4`. The successor is the chain tip RECOMPUTED over the carried bytes, + /// so the conjunct 2c-B deferred for want of a real prepared successor now + /// holds on producer output. This is the one that could not be tested at + /// all while `operation_bytes` was filler. + #[test] + fn g4_the_successor_is_the_recomputed_chain_tip() { + let t = produced(); + let ev = &t.recovery_material; + let recomputed = relationship_chain_tip_v2( + &ev.rel_key, + &ev.embedded_parent, + &ev.counterparty_devid, + &ev.operation_bytes, + &ev.entropy, + None, + ); + assert_eq!(recomputed, t.trader_successor); + } + + /// `G1` and `G2`. The carried bytes decode under the frozen grammar, + /// consuming all of them, and re-encode to exactly the same bytes. + #[test] + fn g1_g2_the_operation_bytes_decode_and_round_trip_exactly() { + let ev = &produced().recovery_material; + let op = Operation::from_bytes(&ev.operation_bytes) + .expect("produced operation_bytes decode under the frozen grammar"); + assert_eq!(op.to_bytes(), ev.operation_bytes, "canonical re-encode"); + } + + /// `G3`. Discriminator 26 and `Unilateral`, read off the decoded operation + /// rather than off the first byte. + #[test] + fn g3_the_operation_is_a_unilateral_settle() { + let ev = &produced().recovery_material; + let op = Operation::from_bytes(&ev.operation_bytes).unwrap(); + assert_eq!(ev.operation_bytes[0], 26, "discriminator"); + match op { + Operation::DlvSettle { + mode, signature, .. + } => { + assert_eq!(mode, TransactionMode::Unilateral); + assert!(!signature.is_empty(), "the settle carries its signature"); + } + other => panic!("not a settle: {other:?}"), + } + } + + /// 2c-B's second chain-tip equality, which the producer makes structural: + /// the two coordinates come from one `PreparedSuccessor` and cannot be set + /// apart. + #[test] + fn the_evidence_linkage_holds_by_construction() { + let t = produced(); + assert_eq!(t.recovery_material.embedded_parent, t.trader_parent); + assert!(t.check_evidence_linkage().is_ok()); + } + + fn identity() -> TraderIdentity { + TraderIdentity { + genesis: fixtures::FIXTURE_TRADER_GENESIS, + device_id: fixtures::FIXTURE_TRADER_DEVID, + } + } + + #[test] + fn an_unsigned_settle_is_refused() { + let mut op = Operation::from_bytes(&produced().recovery_material.operation_bytes).unwrap(); + if let Operation::DlvSettle { signature, .. } = &mut op { + signature.clear(); + } + let e = prepare_market_successor( + [0x51; 32], + [0x52; 32], + [0x42; 32], + &op, + [0x55; 32], + &identity(), + &[0u8; 128], + ); + assert_eq!(e, Err(ProducerError::Unsigned)); + } + + #[test] + fn a_bilateral_settle_is_refused() { + let mut op = Operation::from_bytes(&produced().recovery_material.operation_bytes).unwrap(); + if let Operation::DlvSettle { mode, .. } = &mut op { + *mode = TransactionMode::Bilateral; + } + let e = prepare_market_successor( + [0x51; 32], + [0x52; 32], + [0x42; 32], + &op, + [0x55; 32], + &identity(), + &[0u8; 128], + ); + assert_eq!(e, Err(ProducerError::NotUnilateral)); + } + + #[test] + fn an_operation_that_is_not_a_settle_is_refused() { + let op = Operation::DlvUnlock { + vault_id: vec![0x03; 32], + fulfillment_proof: vec![1], + requester_public_key: vec![2; 64], + signature: vec![3; 8], + mode: TransactionMode::Unilateral, + }; + let e = prepare_market_successor( + [0x51; 32], + [0x52; 32], + [0x42; 32], + &op, + [0x55; 32], + &identity(), + &[0u8; 128], + ); + assert_eq!(e, Err(ProducerError::NotASettle)); + } + + /// Changing ONE byte of the operation changes the successor. The tip is a + /// function of the bytes, which is what makes `G4` load-bearing rather + /// than decorative. + #[test] + fn a_different_operation_yields_a_different_successor() { + let a = fixtures::market_terms([0xC0; 32], [0x58; 32]); + let b = fixtures::market_terms([0xC1; 32], [0x58; 32]); + assert_ne!( + a.recovery_material.operation_bytes, + b.recovery_material.operation_bytes + ); + assert_ne!(a.trader_successor, b.trader_successor); + } +} diff --git a/dsm_client/deterministic_state_machine/dsm/src/dlv/mod.rs b/dsm_client/deterministic_state_machine/dsm/src/dlv/mod.rs index f4a8f599..3c8c31a5 100644 --- a/dsm_client/deterministic_state_machine/dsm/src/dlv/mod.rs +++ b/dsm_client/deterministic_state_machine/dsm/src/dlv/mod.rs @@ -9,6 +9,7 @@ pub mod beta_storage_profile; // the deployed three-member beta profile †pub mod binding_observation; // what a set of binding reads establishes about ONE resource key pub mod close_authorization; // the owner signature over the exact DLV release successor pub mod controller_rotation; +pub mod market_producer; // 5c-2 Step 2 — the genuine market bundle producer; nothing invented pub mod pair_identity; pub mod quorum_bind; // Def 6.21 — Class K sans-IO quorum-binding decision engine pub mod route_commit; diff --git a/dsm_client/deterministic_state_machine/dsm/tests/settlement_bundle_conformance.rs b/dsm_client/deterministic_state_machine/dsm/tests/settlement_bundle_conformance.rs index 996cc4e1..029aa78c 100644 --- a/dsm_client/deterministic_state_machine/dsm/tests/settlement_bundle_conformance.rs +++ b/dsm_client/deterministic_state_machine/dsm/tests/settlement_bundle_conformance.rs @@ -32,9 +32,8 @@ mod indep; use dsm::ccb::decode::{decode_settlement_bundle, decode_settlement_bundle_canonical, DecodeError}; use dsm::ccb::{ - Allocation, ConsumedDlvTransition, DsmSuccessorEvidence, EncumbranceSet, FeePolicy, - MarketPolicy, MarketTerms, ReleasePolicy, Route, RouteLeg, SettlementBundle, StorageSetMembers, - TradeIntent, VaultStateV2, SPX256F_SIGNATURE_LEN, + ConsumedDlvTransition, EncumbranceSet, FeePolicy, MarketPolicy, MarketTerms, ReleasePolicy, + SettlementBundle, StorageSetMembers, VaultStateV2, SPX256F_SIGNATURE_LEN, }; const NS: &[u8] = b"DSM/settlement-bundle"; @@ -124,18 +123,24 @@ const CLOSE_C_NEXT: [u8; 32] = [ 211, 33, 203, 193, 77, 214, 102, 100, 101, 238, 21, 202, ]; const MARKET_B: [u8; 32] = [ - 133, 132, 102, 202, 228, 107, 231, 91, 84, 220, 218, 237, 60, 4, 29, 208, 223, 11, 9, 23, 201, - 83, 169, 125, 122, 80, 39, 29, 254, 206, 84, 150, + 170, 0, 170, 66, 150, 170, 70, 17, 60, 254, 4, 18, 131, 150, 221, 191, 29, 53, 181, 74, 160, + 215, 208, 27, 227, 165, 77, 145, 15, 58, 77, 45, ]; const MARKET_ADDR: [u8; 32] = [ - 116, 21, 183, 190, 111, 55, 90, 16, 14, 149, 176, 90, 11, 219, 75, 233, 134, 89, 129, 214, 49, - 198, 194, 240, 157, 89, 214, 215, 183, 186, 25, 161, + 31, 13, 77, 255, 209, 9, 59, 201, 198, 241, 55, 1, 55, 92, 191, 215, 219, 178, 200, 214, 30, + 155, 133, 133, 234, 165, 56, 171, 191, 51, 35, 209, ]; const MARKET_C_NEXT: [u8; 32] = [ 88, 81, 131, 194, 192, 135, 159, 196, 243, 15, 252, 230, 230, 74, 138, 88, 40, 172, 114, 12, 214, 32, 187, 15, 162, 99, 253, 20, 36, 127, 221, 149, ]; -const MARKET_LEN: usize = 51091; +// 5c-2 Step 2 doubled this, and the reason is worth stating: `operation_bytes` +// is now a REAL signed settle preimage, which embeds the settler's own 49,856-byte +// SPHINCS+ signature. A market bundle therefore carries TWO signatures — the +// settler's inside the preimage and `sigma_dsm` over the successor — where the +// old filler carried none. Still an order of magnitude under the node's +// 512 KiB ingress cap, which the closure test asserts directly. +const MARKET_LEN: usize = 101_186; // ── the owner-close vector ─────────────────────────────────────────────────── @@ -231,43 +236,42 @@ fn owner_close_identities_are_pinned_and_the_decoder_records_the_span() { // ── the market vector: the 2c-A + 2c-B closure test ────────────────────────── -const REL_KEY: [u8; 32] = [0x51; 32]; -const TRADER_PARENT: [u8; 32] = [0x52; 32]; -const TRADER_DEVID: [u8; 32] = [0x53; 32]; -const ENTROPY: [u8; 32] = [0x55; 32]; const X: [u8; 32] = [0x58; 32]; -const TRADER_SUCCESSOR: [u8; 32] = [0x59; 32]; const NONCE: [u8; 32] = [0x5E; 32]; const CLAIM: [u8; 32] = [0x00; 32]; -fn op_bytes_fixture() -> Vec { - (0..300u32) - .map(|i| (i.wrapping_mul(7) % 256) as u8) - .collect() -} - -fn sigma_fixture() -> Vec { - (0..SPX256F_SIGNATURE_LEN) - .map(|i| ((i * 3) % 253) as u8) - .collect() +// 5c-2 Step 2. The trader coordinates and the recovery material are no longer +// this file's to choose. `op_bytes_fixture` (300 filler bytes) and +// `sigma_fixture` (a byte pattern) are DELETED: they could not have satisfied +// 2c-B's `G1`-`G4`, and a vector whose operands cannot satisfy the rules it +// exists to pin is pinning the wrong thing. +// +// The vector is still CLASS-1. What changed is where its INPUTS come from, not +// where its expected bytes come from: the operands are genuine producer output, +// and `indep::` re-encodes them independently of the production encoder, so +// agreement remains evidence rather than a tautology. +fn produced_terms() -> MarketTerms { + dsm::ccb::settlement::fixtures::market_terms(PARENT, X) } fn indep_market_bundle() -> Vec { + let produced = produced_terms(); + let ev = &produced.recovery_material; let intent = indep::trade_intent(TOKEN_A, 10_000, TOKEN_B, 4_935, FEE_BPS, NONCE); let leg = indep::allocation(PARENT, 10_000, 4_935, CLAIM, indep::fee_policy(FEE_BPS)); let terms = indep::market_terms( intent, X, indep::route(vec![leg]), - TRADER_PARENT, - TRADER_SUCCESSOR, + produced.trader_parent, + produced.trader_successor, indep::dsm_successor_evidence( - REL_KEY, - TRADER_PARENT, - TRADER_DEVID, - &op_bytes_fixture(), - ENTROPY, - &sigma_fixture(), + ev.rel_key, + ev.embedded_parent, + ev.counterparty_devid, + &ev.operation_bytes, + ev.entropy, + ev.sigma_dsm(), ), ); indep::settlement_bundle( @@ -281,41 +285,9 @@ fn indep_market_bundle() -> Vec { } fn prod_market_bundle() -> SettlementBundle { - let terms = MarketTerms { - intent: TradeIntent { - token_in: TOKEN_A, - amount_in: 10_000, - token_out: TOKEN_B, - exact_out: 4_935, - fee_bps: FEE_BPS, - nonce: NONCE, - }, - route_set_commitment: X, - selected_route: Route::new(vec![RouteLeg::Single(Allocation { - parent_binding: PARENT, - delta_in: 10_000, - delta_out: 4_935, - encumbrance_claim: CLAIM, - fee_policy: FeePolicy::new(FEE_BPS).unwrap(), - })]) - .unwrap(), - trader_parent: TRADER_PARENT, - trader_successor: TRADER_SUCCESSOR, - recovery_material: DsmSuccessorEvidence::new( - REL_KEY, - TRADER_PARENT, - TRADER_DEVID, - op_bytes_fixture(), - ENTROPY, - sigma_fixture(), - ) - .unwrap(), - }; - SettlementBundle::market( - terms, - vec![ConsumedDlvTransition::market(PARENT, prod_successor(1_010_000, 495_065)).unwrap()], - ) - .unwrap() + // The producer's own output, not a hand-assembled copy of it. If these two + // ever diverge the vector stops testing the thing that ships. + dsm::ccb::settlement::fixtures::market_bundle(PARENT, prod_successor(1_010_000, 495_065), X) } #[test] @@ -388,21 +360,25 @@ fn market_identities_are_pinned_and_the_decoder_records_the_span() { #[test] fn a_close_authorization_inside_a_market_bundle_is_refused() { + // The terms are genuine; what must be refused is the close authorization + // riding under them, so nothing about the trade itself is weakened here. + let produced = produced_terms(); + let ev = &produced.recovery_material; let intent = indep::trade_intent(TOKEN_A, 10_000, TOKEN_B, 4_935, FEE_BPS, NONCE); let leg = indep::allocation(PARENT, 10_000, 4_935, CLAIM, indep::fee_policy(FEE_BPS)); let terms = indep::market_terms( intent, X, indep::route(vec![leg]), - TRADER_PARENT, - TRADER_SUCCESSOR, + produced.trader_parent, + produced.trader_successor, indep::dsm_successor_evidence( - REL_KEY, - TRADER_PARENT, - TRADER_DEVID, - &op_bytes_fixture(), - ENTROPY, - &sigma_fixture(), + ev.rel_key, + ev.embedded_parent, + ev.counterparty_devid, + &ev.operation_bytes, + ev.entropy, + ev.sigma_dsm(), ), ); // A retired successor with an authorization, riding under market terms. @@ -456,26 +432,30 @@ fn a_transposed_parent_linkage_is_refused_inside_the_bytes() { /// 2c-B's second chain-tip equality, at the boundary a FOREIGN bundle crosses: /// field 4 names one trader parent and the nested `0x0031` field 2 another. /// The independent encoder will happily emit it; the decoder must not accept -/// it. (Its sibling conjunct — the frozen `DlvSettleOperationPreimageV1` -/// grammar and the recomputed relationship chain tip — is NOT enforced yet and -/// waits on 5c-2 Step 2/3, so this vector's `operation_bytes` stay arbitrary.) +/// it. Since 5c-2 Step 2 this vector's `operation_bytes` are a REAL signed +/// preimage and its successor a real recomputed chain tip, so the mutation +/// below is the only thing wrong with the bytes. #[test] fn market_terms_whose_evidence_names_another_trader_parent_are_refused_inside_the_bytes() { + // Every operand is genuine producer output EXCEPT the embedded parent, + // which is the single mutation under test. + let produced = produced_terms(); + let ev = &produced.recovery_material; let intent = indep::trade_intent(TOKEN_A, 10_000, TOKEN_B, 4_935, FEE_BPS, NONCE); let leg = indep::allocation(PARENT, 10_000, 4_935, CLAIM, indep::fee_policy(FEE_BPS)); let terms = indep::market_terms( intent, X, indep::route(vec![leg]), - TRADER_PARENT, - TRADER_SUCCESSOR, + produced.trader_parent, + produced.trader_successor, indep::dsm_successor_evidence( - REL_KEY, - [0xEE; 32], // NOT TRADER_PARENT - TRADER_DEVID, - &op_bytes_fixture(), - ENTROPY, - &sigma_fixture(), + ev.rel_key, + [0xEE; 32], // the ONE mutation: NOT the produced embedded parent + ev.counterparty_devid, + &ev.operation_bytes, + ev.entropy, + ev.sigma_dsm(), ), ); let bytes = indep::settlement_bundle(