diff --git a/docs/papers/amendment-2c-c4-accepted-successor-closure.md b/docs/papers/amendment-2c-c4-accepted-successor-closure.md index 5cf3fd16..3a934dbf 100644 --- a/docs/papers/amendment-2c-c4-accepted-successor-closure.md +++ b/docs/papers/amendment-2c-c4-accepted-successor-closure.md @@ -527,11 +527,12 @@ Req 21.15 half-completion — THE WITHHOLD HALF, owed here: bind, withhold the Req 21.16 receipt-verifier — OWED BY 2c-D in full: no TA_B can be parsed here. When it lands it must establish, from the published receipt set alone: retrieve and verify TA_B; prove the commitment carries the exact bundled trader - parent/successor; verify sigma_dsm directly over it; verify the receipt leaf's - inclusion proof under the INDEPENDENTLY DERIVED/AUTHENTICATED TRADER `post_root` - (see the disambiguation below); match (b, X); and reproduce the DLV reserve - deltas — with removal or substitution of TA_B, C_dsm+, sigma_dsm or the - inclusion proof each failing closed. + parent/successor; verify sigma_dsm directly over it; verify the receipt's + facts as the EconomicSettlementReceiptState leaf the settle write set + committed, included under the INDEPENDENTLY VALIDATED economic root R_T^+ + (see the disambiguation below, as CORRECTED 2026-09-11); match (b, X); and + reproduce the DLV reserve deltas — with removal or substitution of TA_B, + C_dsm+, sigma_dsm or the inclusion proof each failing closed. ``` ### Which root — a normative clarification (owner ruling, 2026-09-10) @@ -608,6 +609,131 @@ establish that the exact bundle was economically accepted; Req 21.16 then establ that the published receipt corresponds to the authenticated device post-state rather than being a plausible standalone object. +### Corrected 2026-09-11 — the root D4 names has no independent source (owner ruling) + +**D4 is corrected, not reversed.** Its invariant stands exactly: *the verifier must anchor +to a root established independently of the receipt being verified.* What was wrong is +*which* root. Implementing PR C exposed that the source D4 names — *"obtained from the +already validated successor/economic-admission path"* — never produces a DEVICE root: + +```text +C_T^+ = relationship_chain_tip_v2(rel_key, embedded_parent, counterparty_devid, + operation_bytes, entropy, encapsulated_entropy) + -> commits no root at all +sigma_dsm signs H(G ‖ DevID ‖ C_dsm+ ‖ operation_digest) -> no root +0x0031 the settle's admission substrate -> no root +AdvanceOutcome.child_r_a discarded by the route; never published +device SMT a bounded cache with NO validity replay +``` + +A root carried by the receipt is self-authenticating, and a trader signature over a +device root would establish *provenance* but never *validity* — the device SMT has no +replay that could make it so. That is the gap Rev 15 spec:971-976 names: *a Merkle proof +authenticates membership relative to a root; it does not authenticate the provenance of +that root.* The Rev 15 conformance delta (2026-08-21, §5) had already recorded the same +divergence: Rev 15 requires `C_T^+` to commit the post-advance root, and this +implementation's does not. + +The one independently VALIDATED root on the settle path is `R_T^+`, and the settle write +set already commits the receipt's facts under it as an `EconomicSettlementReceiptState` +leaf. So Req 21.16 anchors there: + +> ```text +> RULING — D4 / Req 21.16 root correction +> +> Choose Option 1. +> +> Amend D4. +> +> The previous requirement that Req 21.16 verify the legacy device-SMT +> receipt leaf under an independently established DEVICE post_root is not +> implementable under the current protocol: +> +> * the device SMT has no validity replay; +> * no independently validated device post_root exists on the settle path; +> * a root carried by the receipt is self-authenticating and therefore +> insufficient; +> * a trader signature over such a root establishes provenance only, +> not validity. +> +> Do NOT add a signed-device-root artifact merely to satisfy the wording. +> +> Req 21.16 instead verifies the published receipt's economically relevant +> facts against the existing EconomicSettlementReceiptState leaf committed +> by the settle write set under the independently validated economic root +> R_T^+. +> +> Required chain: +> +> authenticated / validated settle +> -> deterministic economic write set +> -> EconomicSettlementReceiptState +> -> inclusion under independently validated R_T^+ +> -> receipt correspondence +> -> VerifiedReceipt +> +> The verifier MUST: +> +> 1. Take R_T^+ from the already validated economic advancement path. +> It MUST NOT accept a root supplied by the receipt itself. +> +> 2. Reconstruct the expected EconomicSettlementReceiptState from the +> authenticated settlement facts / exact receipt facts required by +> Def 14.2. +> +> 3. Derive its canonical economic-SMT key independently. +> +> 4. Verify the ordered inclusion path under R_T^+. +> +> 5. Require exact correspondence between the published receipt and the +> authenticated economic receipt state for every realization-relevant +> field. +> +> 6. Reject altered settlement identity, operation identity, amounts, +> assets, parties, key/position, path, or any other field covered by +> the economic receipt state. +> +> 7. Return a typed verified-receipt fact only after the inclusion and +> correspondence checks succeed. +> +> The existing DEVICE-SMT receipt leaf remains legacy state/cache material. +> It is NOT realization evidence and MUST NOT be used to certify value +> movement. +> +> This is a correction to D4, not a substitution of an arbitrary economic +> root for a device root. R_T^+ is appropriate because the receipt facts +> being certified are already represented by the economic receipt leaf +> inside the validated economic state transition. +> +> Do not change relationship_chain_tip_v2. +> Do not add a new signed-device-root artifact. +> Do not defer Req 21.16 from realization. +> ``` + +The three rejected options are recorded with their reasons, because each will be proposed +again. **A signed device-root artifact** repairs the wording and not the defect: it proves +who signed a root, never that the root is the state a valid transition produced. +**Deferring Req 21.16** would weaken the cutover for no reason once a validated +representation of the receipt's facts is known to exist. **Making `C_T^+` commit the root** +is the Rev 15 literal and rewrites a foundational DSM commitment — every chain tip — to +accommodate one verifier. + +```text +wrong receipt -> the receipt's claimed device root -> "verified" +insufficient receipt -> a trader-signed device root -> provenance, not validity +right validated economic transition -> R_T^+ + -> committed EconomicSettlementReceiptState + <- exact correspondence -> published receipt +``` + +**What the correction does NOT do.** It does not put the receipt leaf into the economic +SMT: the settle write set already writes `EconomicSettlementReceiptState` there, and did +before D4 was taken. It does not collapse the two trees §7 and Req 21.16 read — they now +read two different leaves (`0x0032` and the settlement receipt) under ONE validated root, +which is the honest statement of what the settle path actually authenticates. And the +device-SMT receipt leaf is not deleted: it remains the trader's local state, carries no +evidential weight, and nothing may read it as evidence that value moved. + ## Mutation controls Each disabled in turn, with a **named** test going red by performing the forbidden action, then 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 2977f556..2a749cf7 100644 --- a/docs/papers/amendment-2c-d-bundle-acceptance-and-realization.md +++ b/docs/papers/amendment-2c-d-bundle-acceptance-and-realization.md @@ -650,10 +650,12 @@ cannot proceed without. encoder, never the production encoder under test. 8 Req 21.15's realize half and Req 21.16 in full. Req 21.16 verifies the - published Receipt under the INDEPENDENTLY DERIVED TRADER `post_root` (the - device SMT), never under `R_T^+` (the economic SMT) and never under a root - the Receipt itself carries — see 2c-C4 §9's root disambiguation. The two - roots authenticate different claims and neither substitutes for the other. + published Receipt's facts as the EconomicSettlementReceiptState leaf the + settle write set committed, included under the INDEPENDENTLY VALIDATED + economic root `R_T^+` — never under a root the Receipt itself carries, and + never under the legacy device-SMT `post_root`, which has no independent + source (2c-C4 §9, D4 as corrected 2026-09-11). §7 and Req 21.16 read two + different leaves under one validated root. 9 the fifteenth Lean module's successor, or an extension of DSMAcceptedSuccessorWalk, discharging §7's ordering and the two-conjunct 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 a6d9fa72..a0c795c3 100644 --- a/dsm_client/deterministic_state_machine/dsm/src/dlv/mod.rs +++ b/dsm_client/deterministic_state_machine/dsm/src/dlv/mod.rs @@ -12,7 +12,7 @@ pub mod controller_rotation; pub mod market_evidence; // 2c-B G1-G4 — the validity half of market successor evidence pub mod market_producer; // 5c-2 Step 2 — the genuine market bundle producer; nothing invented pub mod pair_identity; -pub mod published_receipt; // Req 21.16 — a receipt proven under an INDEPENDENT post_root +pub mod published_receipt; // Req 21.16 — a receipt's facts proven under the VALIDATED R_T^+ pub mod quorum_bind; // Def 6.21 — Class K sans-IO quorum-binding decision engine pub mod route_commit; pub mod settlement_bundle; // Def 6.14 — the canonical immutable SettlementBundle + K(B) diff --git a/dsm_client/deterministic_state_machine/dsm/src/dlv/published_receipt.rs b/dsm_client/deterministic_state_machine/dsm/src/dlv/published_receipt.rs index 41432ed3..07f0bc46 100644 --- a/dsm_client/deterministic_state_machine/dsm/src/dlv/published_receipt.rs +++ b/dsm_client/deterministic_state_machine/dsm/src/dlv/published_receipt.rs @@ -1,8 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 -//! REQ 21.16 — verifying a published settlement receipt against an -//! INDEPENDENTLY established trader `post_root` (amendment 2c-D, owner ruling -//! D4, recorded at 2c-C4 §9). +//! REQ 21.16 — verifying a published settlement receipt against the +//! INDEPENDENTLY VALIDATED economic root `R_T^+` (amendment 2c-D; owner ruling +//! D4 as corrected 2026-09-11, recorded at 2c-C4 §9). //! //! ## Why this exists beside [`verify_trader_settlement_receipt`] //! @@ -15,25 +15,37 @@ //! consistent, which is **serialization authenticity, not settlement //! authenticity**. //! -//! This verifier takes every one of those four from OUTSIDE the receipt. A -//! receipt cannot prove itself by carrying a root and then supplying a path to -//! that same root; the expected `post_root` comes from the already-validated -//! successor / economic-admission path, and the authority from the resolver. +//! ## Which root, and why it is not the device `post_root` //! -//! ## Which root, and why it is not `R_T^+` -//! -//! Two authenticated structures are in play and they prove different things: +//! D4 originally required the receipt's DEVICE-SMT leaf under an +//! independently established device `post_root`. Implementing the cutover +//! showed no such root exists on the settle path: `C_T^+` commits no root, +//! `sigma_dsm` signs none, the 0x0031 substrate carries none, the route +//! discards `child_r_a`, and the device SMT has no validity replay — so a +//! device root could at best be trader-SIGNED, which is provenance and never +//! validity. The one independently VALIDATED root is `R_T^+`, and the settle +//! write set already commits the receipt's facts under it as an +//! [`EconomicSettlementReceiptState`] leaf. That is what this verifier folds: //! //! ```text -//! acceptance_leaf -> economic SMT -> R_T^+ 2c-D §7 -//! receipt leaf -> trader DEVICE SMT -> post_root HERE +//! validated settle -> deterministic economic write set +//! -> EconomicSettlementReceiptState -> included under validated R_T^+ +//! <- exact correspondence -> published receipt //! ``` //! -//! Reusing `R_T^+` would be a category error — the public receipt leaf is not -//! an economic-state leaf. `R_T^+` is the root §7 already uses to prove the -//! exact bundle `b` was accepted into the economic post-state; this root -//! proves the published receipt corresponds to the authenticated device -//! post-state rather than being a plausible standalone object. +//! **The fold is the correspondence check.** The leaf is REBUILT from the +//! receipt's facts and keyed from the proven identity and the settlement being +//! verified; it folds to `R_T^+` only if every field equals what the write set +//! committed. There is no second copy of the facts here to compare against, +//! and so no list of equalities that could fall out of step with the leaf. +//! +//! **The receipt's legacy fields carry no weight.** `post_root`, +//! `smt_siblings`, `trader_public_key` and `trader_signature` are the +//! device-SMT construction the correction retires. They are not read: a +//! signature over them would feed the retired device root back into this +//! verifier, and everything it would attest is already established by +//! inclusion under a root the lineage walk validated — a walk that itself +//! authenticated the trader under the proven authority. //! //! ## What a `VerifiedReceipt` is NOT //! @@ -41,13 +53,13 @@ //! verification machinery, and the behavioural cutover is a separate change. //! Holding one does **not** release a fence, publish anything, promote a //! market fold out of `PartialPendingRealization`, or construct an -//! [`crate::dlv::successor_validity::IndependentRealization`]. It is one of the -//! facts that will make realization *checkable*. +//! [`crate::dlv::successor_validity::IndependentRealization`]. -use crate::dlv::settlement_receipt_leaf::{ - settlement_receipt_key, settlement_receipt_value, SettledTrade, SignedTraderSettlementReceipt, -}; -use crate::merkle::sparse_merkle_tree::{SmtInclusionProof, SparseMerkleTree}; +use crate::ccb::CcbError; +use crate::dlv::settlement_receipt_leaf::{SettledTrade, SignedTraderSettlementReceipt}; +use crate::economic::lineage::ValidatedEconomicRoot; +use crate::economic::state::{EconomicLeafState, EconomicSettlementReceiptState}; +use crate::economic::tree::{leaf_node, root_from_path, ECONOMIC_SMT_HEIGHT}; /// Why a published receipt was refused. /// @@ -56,7 +68,7 @@ use crate::merkle::sparse_merkle_tree::{SmtInclusionProof, SparseMerkleTree}; /// not have to parse prose to tell them apart. #[derive(Debug, Clone, PartialEq, Eq)] pub enum PublishedReceiptInvalid { - /// The sibling vector is not exactly 256. + /// The economic path is not exactly [`ECONOMIC_SMT_HEIGHT`] siblings. BadSiblingCount { expected: usize, got: usize }, /// `new_sequence != parent_sequence + 1`. NonUnitStep { parent: u64, new: u64 }, @@ -79,17 +91,16 @@ pub enum PublishedReceiptInvalid { /// The receipt names an identity other than the independently established /// one. IdentityMismatch, - /// The receipt names a post-root other than the independently established - /// one. Refused BEFORE the fold, so the fold is never run against a root - /// the receipt chose. - PostRootMismatch { - receipt: [u8; 32], - established: [u8; 32], + /// The receipt's facts are not a canonical economic receipt state at all + /// (a zero amount, or one asset on both legs), so no leaf could commit them. + FactsNotCanonical(CcbError), + /// The leaf rebuilt from the receipt's facts does not fold to the + /// validated economic root: some fact differs from what the write set + /// committed, or the path proves some other tree. + FactsNotCommittedUnderTheValidatedRoot { + folded: [u8; 32], + validated: [u8; 32], }, - /// The signature does not verify under the independently proven authority. - SignatureInvalid, - /// The leaf is not included under the established root. - InclusionRejected, } impl core::fmt::Display for PublishedReceiptInvalid { @@ -97,7 +108,7 @@ impl core::fmt::Display for PublishedReceiptInvalid { match self { Self::BadSiblingCount { expected, got } => write!( f, - "a device-SMT path is exactly {expected} siblings; this receipt carries {got}" + "an economic-SMT path is exactly {expected} siblings; {got} were supplied" ), Self::NonUnitStep { parent, new } => write!( f, @@ -122,31 +133,25 @@ impl core::fmt::Display for PublishedReceiptInvalid { Self::IdentityMismatch => write!( f, "the receipt names an identity other than the independently established trader; \ - its own fields are not authority for whose chain it witnesses" + its own fields are not authority for whose state it witnesses" ), - Self::PostRootMismatch { - receipt, - established, - } => write!( + Self::FactsNotCanonical(e) => write!( f, - "the receipt names post-root {receipt:02x?} while the validated advance \ - established {established:02x?}; a receipt does not get to choose the root it is \ - proven under" + "the receipt's facts are not a canonical economic receipt state ({e}), so no \ + committed leaf could correspond to them" ), - Self::SignatureInvalid => write!( + Self::FactsNotCommittedUnderTheValidatedRoot { folded, validated } => write!( f, - "the receipt does not verify under the independently proven trader authority" - ), - Self::InclusionRejected => write!( - f, - "the recomputed receipt leaf is not included under the established post-root" + "the receipt's facts fold to {folded:02x?}, not to the validated economic root \ + {validated:02x?}; the settlement they describe is not the one this trader's \ + validated transition committed" ), } } } -/// **Req 21.16's fact.** This published receipt corresponds to the -/// authenticated device post-state. +/// **Req 21.16's fact.** This published receipt's facts are exactly the +/// settlement receipt the trader's validated economic transition committed. /// /// Private fields and no public constructor: one exists only because /// [`verify_published_receipt`] returned it, so holding one IS the fact. It is @@ -156,7 +161,7 @@ pub struct VerifiedReceipt { vault_id: [u8; 32], receipt_id: [u8; 32], trade: SettledTrade, - established_post_root: [u8; 32], + economic_root: [u8; 32], } impl VerifiedReceipt { @@ -175,45 +180,44 @@ impl VerifiedReceipt { self.trade } - /// The INDEPENDENTLY established root the leaf was proven under — never a - /// value the receipt supplied. - pub const fn established_post_root(&self) -> [u8; 32] { - self.established_post_root + /// The VALIDATED economic root the facts were proven under — `R_T^+`, + /// never a value the receipt supplied. + pub const fn economic_root(&self) -> [u8; 32] { + self.economic_root } } /// **Req 21.16.** Verify a published receipt against facts established /// independently of it. /// -/// Every operand that the legacy verifier reads out of the receipt is a -/// parameter here: -/// /// | fact | where it must come from | /// |---|---| -/// | `established_post_root` | the validated successor / economic-admission path | +/// | `validated` (`R_T^+`) | the lineage walk for `(proven_genesis, proven_devid)` | +/// | `economic_path` | anywhere — it is checked, never trusted (the trader's published inclusion proof is the natural source) | /// | `proven_genesis`, `proven_devid` | the authority resolution | -/// | `proven_trader_ak` | the authority resolution | /// | `expected_vault_id`, `expected_x` | the settlement being verified | /// -/// Passing values taken from the receipt would reproduce exactly the defect -/// this function exists to remove, so a caller that has only the receipt has -/// nothing to call this with — which is the intended shape. +/// `validated` must be the root validated for `(proven_genesis, +/// proven_devid)` — the same contract §7 places on its own `validated` +/// parameter. A caller that has only the receipt has nothing to call this +/// with, which is the intended shape. pub fn verify_published_receipt( receipt: &SignedTraderSettlementReceipt, - established_post_root: [u8; 32], + validated: &ValidatedEconomicRoot, + economic_path: &[[u8; 32]], proven_genesis: [u8; 32], proven_devid: [u8; 32], - proven_trader_ak: &[u8], expected_vault_id: [u8; 32], expected_x: [u8; 32], ) -> Result { - // ── 1. shape ───────────────────────────────────────────────────────── - if receipt.smt_siblings.len() != 256 { - return Err(PublishedReceiptInvalid::BadSiblingCount { - expected: 256, - got: receipt.smt_siblings.len(), - }); - } + // ── shape ──────────────────────────────────────────────────────────── + let siblings: &[[u8; 32]; ECONOMIC_SMT_HEIGHT] = + economic_path + .try_into() + .map_err(|_| PublishedReceiptInvalid::BadSiblingCount { + expected: ECONOMIC_SMT_HEIGHT, + got: economic_path.len(), + })?; if receipt.trade.new_sequence != receipt.trade.parent_sequence.saturating_add(1) { return Err(PublishedReceiptInvalid::NonUnitStep { parent: receipt.trade.parent_sequence, @@ -221,7 +225,7 @@ pub fn verify_published_receipt( }); } - // ── 2/3. settlement identity correspondence ────────────────────────── + // ── settlement and party identity: the SETTLEMENT'S, never the receipt's ─ if receipt.vault_id != expected_vault_id { return Err(PublishedReceiptInvalid::VaultMismatch { receipt: receipt.vault_id, @@ -234,87 +238,115 @@ pub fn verify_published_receipt( expected: expected_x, }); } - // RE-DERIVED from the settlement being verified, never read. - let receipt_id = - crate::dlv::settlement_receipt_leaf::derive_receipt_id(&expected_vault_id, &expected_x); - if receipt.receipt_id != receipt_id { - return Err(PublishedReceiptInvalid::ReceiptIdNotDerived { - carried: receipt.receipt_id, - derived: receipt_id, - }); - } - - // ── the identity and the root are ESTABLISHED, not read ────────────── if receipt.trader_genesis != proven_genesis || receipt.trader_devid != proven_devid { return Err(PublishedReceiptInvalid::IdentityMismatch); } - // Checked BEFORE the fold. The signature below covers `post_root`, so - // requiring the receipt to NAME the established root is what binds the - // trader's signature to it — and the fold then runs against the - // established value regardless. - if receipt.post_root != established_post_root { - return Err(PublishedReceiptInvalid::PostRootMismatch { - receipt: receipt.post_root, - established: established_post_root, - }); - } - // ── signature, under the PROVEN authority ──────────────────────────── - let payload = crate::dlv::settlement_receipt_leaf::receipt_sign_payload( - &expected_vault_id, - &receipt_id, - &receipt.trade, - &proven_genesis, - &proven_devid, - &established_post_root, - ); - let ok = crate::crypto::sphincs::sphincs_verify( - proven_trader_ak, - &payload, - &receipt.trader_signature, + // ── rebuild the leaf the write set would have committed ───────────── + // + // From the receipt's FACTS, with vault and x taken from the settlement + // being verified. The constructor re-derives `receipt_id`, so the carried + // one is checked against it rather than read. + let state = EconomicSettlementReceiptState::new( + expected_vault_id, + expected_x, + receipt.trade.parent_sequence, + receipt.trade.new_sequence, + receipt.trade.input_policy_commit, + receipt.trade.input_amount, + receipt.trade.output_policy_commit, + receipt.trade.output_amount, ) - .map_err(|_| PublishedReceiptInvalid::SignatureInvalid)?; - if !ok { - return Err(PublishedReceiptInvalid::SignatureInvalid); + .map_err(PublishedReceiptInvalid::FactsNotCanonical)?; + if receipt.receipt_id != state.receipt_id { + return Err(PublishedReceiptInvalid::ReceiptIdNotDerived { + carried: receipt.receipt_id, + derived: state.receipt_id, + }); } + let receipt_id = state.receipt_id; + let leaf = EconomicLeafState::SettlementReceipt(state); - // ── 4/5. reconstruct the leaf and fold to the ESTABLISHED root ─────── + // ── fold under the VALIDATED root ──────────────────────────────────── // - // Every input to the key comes from outside the receipt, so a receipt - // cannot move its own leaf to a position where some path happens to work. - let key = settlement_receipt_key( - &proven_genesis, - &proven_devid, - &expected_vault_id, - &receipt_id, - ); - let proof = SmtInclusionProof { - key, - value: Some(settlement_receipt_value(&receipt.trade)), - siblings: receipt.smt_siblings.clone(), - }; - if !SparseMerkleTree::verify_proof_against_root(&proof, &established_post_root) { - return Err(PublishedReceiptInvalid::InclusionRejected); + // Every input to the key is independent of the receipt, so it cannot move + // its own leaf to a position where some path happens to work; and the + // value is the canonical encoding of the facts, so a single altered field + // lands the fold on a different root. + let key = leaf.leaf_key(&proven_genesis, &proven_devid); + let value = leaf + .leaf_value() + .map_err(PublishedReceiptInvalid::FactsNotCanonical)?; + let folded = root_from_path(&key, &leaf_node(&key, Some(&value)), siblings); + let economic_root = validated.economic_root(); + if folded != economic_root { + return Err( + PublishedReceiptInvalid::FactsNotCommittedUnderTheValidatedRoot { + folded, + validated: economic_root, + }, + ); } Ok(VerifiedReceipt { vault_id: expected_vault_id, receipt_id, trade: receipt.trade, - established_post_root, + economic_root, }) } #[cfg(test)] mod tests { use super::*; - use crate::dlv::settlement_receipt_leaf::{derive_receipt_id, sign_trader_settlement_receipt}; + use std::collections::BTreeMap; + + use crate::dlv::settlement_receipt_leaf::{ + derive_receipt_id, settlement_receipt_key, settlement_receipt_value, + sign_trader_settlement_receipt, verify_trader_settlement_receipt, + }; + use crate::economic::state::EconomicBalanceState; + use crate::economic::tree::EconomicSmt; + use crate::economic::write_set::{ + build_write_set, CreditSourceFacts, EconomicPreState, EconomicWriteContext, + }; + use crate::merkle::sparse_merkle_tree::SparseMerkleTree; + use crate::types::operations::{Operation, TransactionMode}; const G: [u8; 32] = [0x11; 32]; const DEV: [u8; 32] = [0x22; 32]; const VAULT: [u8; 32] = [0x03; 32]; const X: [u8; 32] = [0xA0; 32]; + const C_DSM_PLUS: [u8; 32] = [0xC5; 32]; + const POSITION: u64 = 3; + + 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: [0x10; 32], + output_policy_commit: [0x20; 32], + 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: derive_receipt_id(&VAULT, &X), + signature: vec![0x77; 48], + mode: TransactionMode::Unilateral, + } + } + /// The trade exactly as the device advance records it for this settle + /// (`device_state.rs`, the `DlvSettle` arm) — the facts a published + /// receipt carries. fn trade() -> SettledTrade { SettledTrade { x: X, @@ -327,165 +359,279 @@ mod tests { } } - /// The receipt leaf inserted into a real device SMT, and the root that - /// results. The root is computed HERE, from the tree — it is the - /// "independently established" value the verifier is given, and the - /// receipt merely names it. - fn established_root(g: [u8; 32], dev: [u8; 32]) -> ([u8; 32], Vec<[u8; 32]>) { - let receipt_id = derive_receipt_id(&VAULT, &X); + /// The economic post-state the REAL settle write set leaves behind. The + /// receipt leaf in it is production's, not one this file assembled to be + /// checkable — and it shares the tree with the debit, credit and + /// acceptance leaves, so its path is a real one. + fn settled_tree() -> EconomicSmt { + let mut tree = EconomicSmt::new(); + let funded = EconomicLeafState::Balance( + EconomicBalanceState::new([0x10; 32], 5_000).expect("balance"), + ); + tree.insert( + funded.leaf_key(&G, &DEV), + funded.leaf_value().expect("value"), + ); + let mut balances = BTreeMap::new(); + balances.insert([0x10; 32], 5_000u64); + build_write_set( + &settle(), + &G, + &DEV, + &crate::economic::faucet::dsm_economic_operation_id(&G, &DEV, &C_DSM_PLUS), + &EconomicPreState::balances_only(&balances), + &mut tree, + &CreditSourceFacts::DlvReserveConsumption { + owner_economic_position: 3, + reserve_consumption_evidence_addr: [0xEE; 32], + }, + &EconomicWriteContext::DlvSettle { + bundle_id: [0xB0; 32], + }, + ) + .expect("the settle write set builds"); + tree + } + + /// A legacy receipt object for `trade`, internally consistent over its OWN + /// one-leaf device tree — the construction the legacy verifier accepts. + fn legacy_receipt( + g: [u8; 32], + dev: [u8; 32], + trade: SettledTrade, + ) -> SignedTraderSettlementReceipt { + let (pk, sk) = crate::crypto::sphincs::generate_sphincs_keypair().expect("keypair"); + let receipt_id = derive_receipt_id(&VAULT, &trade.x); let key = settlement_receipt_key(&g, &dev, &VAULT, &receipt_id); - let value = settlement_receipt_value(&trade()); - let mut tree = SparseMerkleTree::new(64); - tree.update_leaf(&key, &value).expect("update_leaf"); - let proof = tree.get_inclusion_proof(&key, 256).expect("proof"); - (*tree.root(), proof.siblings) + let mut device = SparseMerkleTree::new(64); + device + .update_leaf(&key, &settlement_receipt_value(&trade)) + .expect("update_leaf"); + let proof = device.get_inclusion_proof(&key, 256).expect("proof"); + sign_trader_settlement_receipt( + &VAULT, + &receipt_id, + trade, + &g, + &dev, + device.root(), + proof.siblings, + &pk, + &sk, + ) + .expect("signed") } struct Fixture { receipt: SignedTraderSettlementReceipt, - root: [u8; 32], - ak: Vec, + validated: ValidatedEconomicRoot, + path: Vec<[u8; 32]>, } fn honest() -> Fixture { - let (pk, sk) = crate::crypto::sphincs::generate_sphincs_keypair().expect("keypair"); - let (root, siblings) = established_root(G, DEV); - let receipt = sign_trader_settlement_receipt( - &VAULT, - &derive_receipt_id(&VAULT, &X), - trade(), - &G, - &DEV, - &root, - siblings, - &pk, - &sk, - ) - .expect("signed"); + let tree = settled_tree(); + let state = + EconomicSettlementReceiptState::new(VAULT, X, 7, 8, [0x10; 32], 1_000, [0x20; 32], 900) + .expect("state"); + let key = EconomicLeafState::SettlementReceipt(state).leaf_key(&G, &DEV); Fixture { - receipt, - root, - ak: pk, + receipt: legacy_receipt(G, DEV, trade()), + validated: ValidatedEconomicRoot::rehydrate_from_admitted_store(POSITION, tree.root()), + path: tree.siblings(&key).to_vec(), } } fn verify(f: &Fixture) -> Result { - verify_published_receipt(&f.receipt, f.root, G, DEV, &f.ak, VAULT, X) + verify_published_receipt(&f.receipt, &f.validated, &f.path, G, DEV, VAULT, X) } #[test] - fn an_honest_receipt_verifies_under_the_established_root() { + fn an_honest_receipt_verifies_under_the_validated_economic_root() { let f = honest(); let v = verify(&f).expect("Req 21.16 holds"); - assert_eq!(v.established_post_root(), f.root); + assert_eq!(v.economic_root(), f.validated.economic_root()); assert_eq!(v.receipt_id(), derive_receipt_id(&VAULT, &X)); assert_eq!(v.trade(), trade()); + assert_eq!(v.vault_id(), VAULT); } - /// **THE DEFECT THIS FUNCTION EXISTS TO REMOVE.** A receipt that carries - /// its own root, signs over it, and supplies a path to it is internally - /// consistent — the legacy verifier accepts it. Req 21.16 refuses it, - /// because the root it was asked to prove against is not the one the - /// validated advance established. + /// **THE DEFECT THIS FUNCTION EXISTS TO REMOVE.** A receipt claiming a + /// trade the trader never committed — here, a better output — is + /// internally consistent over its own device tree, and the legacy + /// verifier accepts it. Req 21.16 refuses it, because the validated + /// economic state does not commit those facts. #[test] - fn a_self_rooted_receipt_is_refused_though_it_is_internally_consistent() { - // A receipt built over a DIFFERENT identity's tree: self-consistent, - // and rooted at a value nobody else established. - let other_g = [0x99; 32]; - let (pk, sk) = crate::crypto::sphincs::generate_sphincs_keypair().expect("keypair"); - let (rogue_root, siblings) = established_root(other_g, DEV); - let receipt = sign_trader_settlement_receipt( - &VAULT, - &derive_receipt_id(&VAULT, &X), - trade(), - &other_g, - &DEV, - &rogue_root, - siblings, - &pk, - &sk, - ) - .expect("signed"); + fn a_self_rooted_receipt_is_refused_though_the_legacy_verifier_accepts_it() { + let mut f = honest(); + let mut inflated = trade(); + inflated.output_amount = 950; + f.receipt = legacy_receipt(G, DEV, inflated); - // The legacy verifier, which reads everything out of the receipt, - // accepts it — that is the property being contrasted, not a bug here. assert!( - crate::dlv::settlement_receipt_leaf::verify_trader_settlement_receipt(&receipt).is_ok(), + verify_trader_settlement_receipt(&f.receipt).is_ok(), "the self-referential check passes, which is exactly the problem" ); - - // Req 21.16, given the ESTABLISHED root, refuses. - let established = honest().root; assert!(matches!( - verify_published_receipt(&receipt, established, G, DEV, &pk, VAULT, X), - Err(PublishedReceiptInvalid::IdentityMismatch) - | Err(PublishedReceiptInvalid::PostRootMismatch { .. }) + verify(&f), + Err(PublishedReceiptInvalid::FactsNotCommittedUnderTheValidatedRoot { .. }) )); } - /// The root is not negotiable: a receipt naming any other root is refused - /// BEFORE the fold, so the fold never runs against a receipt-chosen value. + /// **THE D4 CORRECTION AS A CONTROL.** The receipt's OWN device path does + /// prove its leaf — under the device root the receipt carries. Handed to + /// this verifier it proves nothing: it folds to that device root, which is + /// not the validated economic root, and the legacy leaf is not realization + /// evidence. #[test] - fn a_receipt_naming_another_root_is_refused() { - let f = honest(); + fn the_receipts_own_device_path_is_not_evidence() { + let mut f = honest(); + f.path = f.receipt.smt_siblings.clone(); assert!(matches!( - verify_published_receipt(&f.receipt, [0x77; 32], G, DEV, &f.ak, VAULT, X), - Err(PublishedReceiptInvalid::PostRootMismatch { .. }) + verify(&f), + Err(PublishedReceiptInvalid::FactsNotCommittedUnderTheValidatedRoot { .. }) )); } - /// The authority is not the receipt's. A receipt signed by someone else is - /// refused even though its own `trader_public_key` would verify it. + /// The legacy fields carry no weight in EITHER direction: a receipt whose + /// device root, path and signature are garbage still verifies when its + /// facts are the committed ones. Pinned so the removed signature check is + /// a visible decision rather than an accident — reinstating it would feed + /// the retired device root back into this verifier. #[test] - fn a_receipt_signed_by_another_authority_is_refused() { - let f = honest(); - let (other_pk, _) = crate::crypto::sphincs::generate_sphincs_keypair().expect("keypair"); - assert_eq!( - verify_published_receipt(&f.receipt, f.root, G, DEV, &other_pk, VAULT, X), - Err(PublishedReceiptInvalid::SignatureInvalid) - ); + fn the_legacy_device_fields_carry_no_weight_either_way() { + let mut f = honest(); + f.receipt.post_root = [0x77; 32]; + f.receipt.smt_siblings = vec![[0x55; 32]; 3]; + f.receipt.trader_signature = vec![0u8; 8]; + f.receipt.trader_public_key = vec![0u8; 8]; + verify(&f).expect("the facts are committed under R_T^+, and only they are read"); + } + + /// One named single-field change to a receipt's trade. + type Alteration = (&'static str, fn(&mut SettledTrade)); + + /// Every realization-relevant fact is covered by the fold. Each mutation + /// below changes ONE field and lands on a different root. + #[test] + fn an_altered_amount_asset_or_sequence_is_refused() { + let alterations: [Alteration; 4] = [ + ("input amount", |t| t.input_amount += 1), + ("output amount", |t| t.output_amount -= 1), + ("input asset", |t| t.input_policy_commit = [0x30; 32]), + ("sequence", |t| { + t.parent_sequence += 1; + t.new_sequence += 1; + }), + ]; + for (what, alter) in alterations { + let mut f = honest(); + alter(&mut f.receipt.trade); + assert!( + matches!( + verify(&f), + Err(PublishedReceiptInvalid::FactsNotCommittedUnderTheValidatedRoot { .. }) + ), + "an altered {what} must not fold to R_T^+" + ); + } + } + + /// Key/position: the leaf key is derived from the PROVEN identity, so a + /// receipt naming another trader is refused before any fold. + #[test] + fn a_receipt_naming_another_trader_is_refused() { + let mut f = honest(); + f.receipt.trader_genesis = [0x99; 32]; + assert_eq!(verify(&f), Err(PublishedReceiptInvalid::IdentityMismatch)); + } + + /// And a proven identity other than the one whose state committed the + /// receipt keys the leaf elsewhere, so the fold fails even when the receipt + /// agrees with that identity. + #[test] + fn facts_committed_by_another_trader_do_not_verify_for_this_one() { + let mut f = honest(); + let other = [0x98; 32]; + f.receipt = legacy_receipt(G, other, trade()); + assert!(matches!( + verify_published_receipt(&f.receipt, &f.validated, &f.path, G, other, VAULT, X), + Err(PublishedReceiptInvalid::FactsNotCommittedUnderTheValidatedRoot { .. }) + )); } - /// The settlement identity must correspond: a receipt for another trade is - /// refused even when everything else is honest. #[test] fn a_receipt_for_another_trade_is_refused() { let f = honest(); assert!(matches!( - verify_published_receipt(&f.receipt, f.root, G, DEV, &f.ak, VAULT, [0xA1; 32]), + verify_published_receipt(&f.receipt, &f.validated, &f.path, G, DEV, VAULT, [0xA1; 32]), Err(PublishedReceiptInvalid::ExternalCommitmentMismatch { .. }) )); } - /// A receipt for another vault is refused. #[test] fn a_receipt_for_another_vault_is_refused() { let f = honest(); assert!(matches!( - verify_published_receipt(&f.receipt, f.root, G, DEV, &f.ak, [0x04; 32], X), + verify_published_receipt(&f.receipt, &f.validated, &f.path, G, DEV, [0x04; 32], X), Err(PublishedReceiptInvalid::VaultMismatch { .. }) )); } - /// An altered path cannot fold to the established root. + /// A carried id that does not derive from `(vault, x)`. + #[test] + fn a_receipt_whose_id_does_not_derive_is_refused() { + let mut f = honest(); + f.receipt.receipt_id = [0xEE; 32]; + assert!(matches!( + verify(&f), + Err(PublishedReceiptInvalid::ReceiptIdNotDerived { .. }) + )); + } + + /// An altered path cannot fold to the validated root. #[test] fn an_altered_path_is_refused() { let mut f = honest(); - f.receipt.smt_siblings[0] = [0xFF; 32]; - assert_eq!(verify(&f), Err(PublishedReceiptInvalid::InclusionRejected)); + f.path[0] = [0xFF; 32]; + assert!(matches!( + verify(&f), + Err(PublishedReceiptInvalid::FactsNotCommittedUnderTheValidatedRoot { .. }) + )); + } + + /// A root validated for some other state proves nothing about this one. + #[test] + fn a_path_checked_against_another_validated_root_is_refused() { + let mut f = honest(); + f.validated = ValidatedEconomicRoot::rehydrate_from_admitted_store(POSITION, [0x77; 32]); + assert!(matches!( + verify(&f), + Err(PublishedReceiptInvalid::FactsNotCommittedUnderTheValidatedRoot { .. }) + )); } /// A short path is refused rather than padded. #[test] fn a_short_path_is_refused() { let mut f = honest(); - f.receipt.smt_siblings.pop(); - assert!(matches!( + f.path.pop(); + assert_eq!( verify(&f), Err(PublishedReceiptInvalid::BadSiblingCount { expected: 256, got: 255 }) - )); + ); + } + + /// A step that is not +1 is refused by name. + #[test] + fn a_non_unit_step_is_refused() { + let mut f = honest(); + f.receipt.trade.new_sequence = 10; + assert_eq!( + verify(&f), + Err(PublishedReceiptInvalid::NonUnitStep { parent: 7, new: 10 }) + ); } }