From a129552ce8e0ba64054b26fb23baf931c5e4a2f4 Mon Sep 17 00:00:00 2001 From: Cryptskii <47649969+cryptskii@users.noreply.github.com> Date: Sat, 5 Sep 2026 16:54:13 -0400 Subject: [PATCH] feat(economic): the beta root register is pinned to the register histories the real fleet holds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #770 gave a vault's birth commitment `(member_id, register_incarnation_id)` pairs, but the network-wide root register still derived its set id from whatever candidate the catalog offered, constrained only to the right member ids. "Who chose the incarnation values?" answered "the resolved catalog candidate" — and `derive_set_id(&candidate) -> Result<[u8; 32]>` returned the id derived from its own input, which both call sites then used as the expected id. A checker that returns what it derived hands the authority to its input. `RootRegisterProfile` now carries `storage_set_id` as a pinned commitment, and `verify_candidate` replaces `derive_set_id` at every call site: the candidate's membership must be the network's AND the id re-derived from its pairs must equal the pin. A member that rebuilt its register passes the first and fails the second, and is refused (`SetIdIsNotThePinnedOne`) rather than silently resolved to a different register. Endpoints remain transport metadata. The pin is the three real `(member, incarnation)` pairs, not an opaque digest: member list and set id both derive from the one source, so they cannot drift, and an auditor can read which register histories are trusted. `pinned_root_register_members()` is public — a commitment, not a secret — and every fixture builds from it, so nothing holds parallel values that must match. Before the values existed the placeholder was all-zero, which the encoder already refuses; an unprovisioned network fails closed by construction (`ProfileNotDerivable(ZeroRegisterIncarnation)`), not by anyone remembering to check. The values came off the provisioned nodes in the owner's order. Each member: forensic `pg_dump`, `DROP SCHEMA public CASCADE`, boot on merged `f00d1e0c` with `[storage_set]` absent, mint and log the incarnation. Then the collected pairs were written as `[[storage_set.members]]` to every node and to the client config, and every node restarted into the set logging the identical id `E05YS8101EJH33KY2CG625JJE8A0Z4GJNSEM335TX1XVTWM9RR8G` with no refusal. Two tests pin this end to end: the pinned bytes render to exactly the strings each node logged (cross-checked against the raw `register_incarnation` rows), and core's derivation equals the id every node logged. Mutation control: disabling the pin comparison turned `one_member_rebuilding_its_register_changes_the_set_id` red (`got Ok(())` — the rebuilt member was accepted); restored on an identical tree stamp. Not changed, surfaced: no node sets `node.network_id`, so the faucet-ticket register is inactive fleet-wide until it is configured. A fail-open in `main.rs` (optional config + a defaulted `database.url` that quietly points at localhost) is filed separately; it turned a wrong-image build into a crash loop during provisioning rather than a refusal. Verification (Rust 1.98.0, the version rust-toolchain.toml declares): - root `make lint` exit 0 - workspace board 3984 passed, 0 failed - node board (local-dev,strict / SQLite) 288 passed, 0 failed - node board (default features / Postgres 14) 258 passed, 0 failed - production_safety_checks (incl. TLA+), ci_scan, no_clock_and_no_json, check_forbidden_symbols all exit 0 --- .../dsm/src/economic/lineage.rs | 7 +- .../dsm/src/economic/peer_lineage.rs | 9 +- .../dsm/src/economic/register.rs | 156 +++++++++++++++--- .../dsm/tests/economic_admission_lifecycle.rs | 14 +- .../dsm/tests/economic_authorized_issuance.rs | 11 +- .../economic_dlv_owner_apply_provenance.rs | 11 +- .../tests/economic_dlv_settle_provenance.rs | 11 +- .../dsm/tests/economic_lineage_register.rs | 78 ++++----- .../dsm/tests/economic_peer_evidence.rs | 11 +- .../tests/economic_provenance_semantics.rs | 11 +- .../dsm/tests/era_faucet_wire.rs | 11 +- .../dsm_sdk/src/economic_fixtures.rs | 17 +- .../dsm_sdk/src/handlers/faucet_flow_tests.rs | 2 +- .../src/handlers/sender_admission_tests.rs | 4 +- .../src/sdk/economic_admission_flow.rs | 4 +- .../dsm_sdk/src/sdk/economic_registers.rs | 8 +- .../dsm_sdk/src/sdk/storage_set.rs | 53 ++++++ scripts/dsm_env_config.alibaba.toml | 10 ++ 18 files changed, 310 insertions(+), 118 deletions(-) diff --git a/dsm_client/deterministic_state_machine/dsm/src/economic/lineage.rs b/dsm_client/deterministic_state_machine/dsm/src/economic/lineage.rs index ab9276d0d..e55ef0b00 100644 --- a/dsm_client/deterministic_state_machine/dsm/src/economic/lineage.rs +++ b/dsm_client/deterministic_state_machine/dsm/src/economic/lineage.rs @@ -579,11 +579,14 @@ pub fn advance_validated( "the network's register set could not be resolved", )) })?; - let canonical_set = profile.derive_set_id(&candidate).map_err(|_| { + // The candidate must re-derive the network's PINNED set id. Membership + // alone would leave the incarnations to whatever the catalog offered. + profile.verify_candidate(&candidate).map_err(|_| { EconomicValidationError::Provenance(ProvenanceError::FaucetWinnerInvalid( - "resolved register membership is not the network's canonical membership", + "the resolved register set is not this network's pinned register", )) })?; + let canonical_set = profile.storage_set_id; let ctx = ProvenanceContext { genesis, device_id, diff --git a/dsm_client/deterministic_state_machine/dsm/src/economic/peer_lineage.rs b/dsm_client/deterministic_state_machine/dsm/src/economic/peer_lineage.rs index 431bee8bc..3e4b4db17 100644 --- a/dsm_client/deterministic_state_machine/dsm/src/economic/peer_lineage.rs +++ b/dsm_client/deterministic_state_machine/dsm/src/economic/peer_lineage.rs @@ -378,14 +378,15 @@ fn walk_positions( let profile = resolve_for_trader(&facts.network_id, expected_network_id) .map_err(|e| invalid(format!("peer network refused: {e}")))?; // The id is re-derived from the resolved `(member, incarnation)` - // pairs, and `derive_set_id` refuses a candidate whose membership is - // not this network's. A member that rebuilt its register is a + // pairs, and `verify_candidate` refuses a candidate that does not + // re-derive this network's PINNED id. A member that rebuilt its register is a // different entry, so a claim written under the old incarnation no // longer names the set this network resolves to. let candidate = fetcher.root_register_candidate_set(&facts.network_id)?; - let expected_set_id = profile - .derive_set_id(&candidate) + profile + .verify_candidate(&candidate) .map_err(|e| invalid(format!("peer register set refused: {e}")))?; + let expected_set_id = profile.storage_set_id; if body.root_register_storage_set_id != expected_set_id { return Err(invalid( "claim binds a register set that is not the canonical set of the peer's \ diff --git a/dsm_client/deterministic_state_machine/dsm/src/economic/register.rs b/dsm_client/deterministic_state_machine/dsm/src/economic/register.rs index 1e8b89a33..23dbddd2a 100644 --- a/dsm_client/deterministic_state_machine/dsm/src/economic/register.rs +++ b/dsm_client/deterministic_state_machine/dsm/src/economic/register.rs @@ -95,26 +95,44 @@ pub fn economic_root_register_key( pub struct RootRegisterProfile { pub quorum: u32, pub members: Vec>, + /// The PINNED set id this network's root register lives under. + /// + /// This is the authority commitment, and it is why the field exists. With + /// membership alone, the answer to "who chose the incarnation values the + /// set id was derived from?" is "the resolved catalog candidate" — the + /// catalog would be constrained to the right member ids and otherwise + /// believed. Pinning the digest makes the catalog's job resolution and + /// nothing else: it may say WHERE a member is reached and WHICH + /// incarnation it claims, and this value decides whether that is the + /// register this network actually commits to. + /// + /// Derived once, from the incarnations the real provisioned members + /// minted. It is not derivable from anything in this source tree, which + /// is the point. + pub storage_set_id: [u8; 32], } impl RootRegisterProfile { - /// Re-derive this profile's set id from CANDIDATE entries, refusing any - /// candidate whose membership is not exactly this network's. + /// Check a CANDIDATE set against this network's pinned commitment. /// - /// The set id is a function of `(member_id, register_incarnation_id)` - /// pairs, and a member's incarnation is a runtime fact — a value it - /// generates once and cannot re-derive from its identity key — so it - /// cannot be a constant here. A resolver supplies the candidate pairs; - /// this function is what stops the resolver from being believed. + /// Two conjuncts, in this order, and neither is sufficient alone: /// - /// A catalog entry is NEVER accepted merely for existing: the candidate's - /// member ids must equal this network's canonical members exactly, and - /// only then is the id computed from the canonical encoding of the pairs. - /// Endpoints are transport metadata and are not inputs. - pub fn derive_set_id( + /// 1. the candidate's member ids are exactly this network's members — + /// a cheap, legible refusal that names what was wrong; + /// 2. the id RE-DERIVED from the candidate's `(member_id, + /// register_incarnation_id)` pairs equals `storage_set_id`. + /// + /// Clause 2 is the one that makes the catalog non-authoritative. A member + /// that rebuilt its register still has the right id, so clause 1 passes; + /// its incarnation changed, so the re-derived digest does not match and + /// the set is refused. That is the substitution this exists to stop, and + /// it is refused rather than silently resolved to a different register. + /// + /// Endpoints are transport metadata and are not inputs to either clause. + pub fn verify_candidate( &self, candidate: &StorageSetMembers, - ) -> Result<[u8; 32], RegisterResolutionError> { + ) -> Result<(), RegisterResolutionError> { let mut want: Vec<&[u8]> = self.members.iter().map(|m| m.as_slice()).collect(); want.sort_unstable(); let got: Vec<&[u8]> = candidate.entries().iter().map(|e| e.member_id()).collect(); @@ -124,7 +142,15 @@ impl RootRegisterProfile { got: got.iter().map(|m| m.to_vec()).collect(), }); } - storage_set_id(candidate).map_err(RegisterResolutionError::ProfileNotDerivable) + let derived = + storage_set_id(candidate).map_err(RegisterResolutionError::ProfileNotDerivable)?; + if derived != self.storage_set_id { + return Err(RegisterResolutionError::SetIdIsNotThePinnedOne { + pinned: self.storage_set_id, + derived, + }); + } + Ok(()) } } @@ -142,6 +168,11 @@ pub enum RegisterResolutionError { expected: Vec>, got: Vec>, }, + /// The candidate has this network's members, but the id re-derived from + /// its `(member, incarnation)` pairs is not the pinned one — the members + /// are right and at least one is not serving the register this network + /// committed to. A rebuilt or restored member lands here. + SetIdIsNotThePinnedOne { pinned: [u8; 32], derived: [u8; 32] }, /// The trader's committed network is not the one being settled against. NetworkMismatch { claimed: Vec, expected: Vec }, /// The profile resolved, but its set id could not be re-derived from the @@ -170,6 +201,15 @@ impl core::fmt::Display for RegisterResolutionError { .map(|m| String::from_utf8_lossy(m)) .collect::>() ), + // The digests are carried on the variant rather than rendered + // here: this crate has no Base32-Crockford encoder (it lives in + // the SDK), and the repository forbids hex. + Self::SetIdIsNotThePinnedOne { .. } => write!( + f, + "the resolved register set has this network's members but does not derive the \ + pinned set id — a member is not serving the register history this network \ + committed to" + ), Self::NetworkMismatch { claimed, expected } => write!( f, "trader genesis commits network {:?} but this is network {:?} — a genesis \ @@ -186,12 +226,56 @@ impl core::fmt::Display for RegisterResolutionError { impl std::error::Error for RegisterResolutionError {} -/// The beta fleet's member ids. +/// The beta fleet's PINNED authority set: each member and the register +/// incarnation it is serving. +/// +/// This is the network's root-register commitment, expressed as the pairs +/// rather than as an opaque digest so it can be audited by reading it — the +/// set id is derived from exactly these bytes, and `verify_candidate` refuses +/// anything that does not re-derive to it. +/// +/// The incarnations are NOT chosen here. Each is the value that member's own +/// database minted on first boot, read from its startup log; nothing in this +/// source tree can derive them, which is what makes the pin meaningful. A +/// member that loses and rebuilds its register mints a different one, stops +/// re-deriving this id, and is refused rather than silently substituted. /// -/// Member IDENTITIES live here and not in `dlv::beta_storage_profile`, which -/// deliberately fixes cardinality and threshold only. Two modules asserting -/// membership would be two places for the fleet to be described differently. -const BETA_MEMBERS: [&[u8]; 3] = [b"dsm-node-1", b"dsm-node-2", b"dsm-node-3"]; +/// An all-zero incarnation is the value a node has before it has established +/// one, and `StorageSetMembers::new` refuses it — so an UNPROVISIONED network +/// fails closed here by construction rather than by anyone remembering to +/// check. +/// +/// Provisioned 2026-09-05 (UTC): each member's database was snapshotted for +/// forensics, wiped, and booted on the merged register-incarnation binary +/// (`f00d1e0c`); these are the values each node's `register_incarnation` +/// row holds and each logged at that boot. Their Base32-Crockford +/// renderings are pinned in `beta_root_register_pins_render_to_the_logged_values`. +const BETA_ROOT_REGISTER_MEMBERS: [PinnedMember; 3] = [ + ( + b"dsm-node-1", + [ + 0x6F, 0x79, 0x83, 0xF1, 0x32, 0x13, 0x8A, 0xAC, 0xDC, 0xAB, 0x92, 0xFC, 0xF0, 0x8F, + 0xFF, 0x74, 0xC3, 0xB7, 0xEB, 0xEF, 0xF5, 0x78, 0xAF, 0x59, 0xE1, 0x9D, 0x74, 0x86, + 0x0C, 0x7E, 0xB6, 0xE8, + ], + ), + ( + b"dsm-node-2", + [ + 0x89, 0x3F, 0x96, 0xC0, 0x64, 0xA0, 0x57, 0x9B, 0xDE, 0x28, 0xD2, 0x79, 0xCE, 0x7C, + 0xC5, 0xF2, 0x41, 0xEE, 0x26, 0xFE, 0x13, 0x3D, 0x8C, 0x09, 0xD0, 0x1C, 0x4C, 0x20, + 0xED, 0xB0, 0x90, 0xF8, + ], + ), + ( + b"dsm-node-3", + [ + 0xDF, 0x07, 0x87, 0x2B, 0x8A, 0x3D, 0xB0, 0x60, 0x23, 0xC4, 0x57, 0x87, 0xBE, 0x85, + 0x14, 0x42, 0xDC, 0x44, 0x09, 0x16, 0x7D, 0xAB, 0xBD, 0x40, 0x68, 0x07, 0x76, 0x14, + 0x46, 0x6F, 0x46, 0x73, + ], + ), +]; /// The network the beta fleet serves. Matches the client database's /// `network_id` default. The real mainnet gets its OWN id (and with it a @@ -199,6 +283,27 @@ const BETA_MEMBERS: [&[u8]; 3] = [b"dsm-node-1", b"dsm-node-2", b"dsm-node-3"]; /// claimed under this network can validate there. const BETA_NETWORK_ID: &[u8] = b"dsm-testnet"; +/// One pinned entry: a member id and the register incarnation it serves. +pub type PinnedMember = (&'static [u8], [u8; 32]); + +/// A network's PINNED root-register members, for callers that must construct +/// or display the committed set — a catalog being provisioned, a fixture that +/// has to resolve to the real register, an operator tool. +/// +/// Public because it is a commitment, not a secret: it says which members and +/// which register histories this network trusts, and anyone verifying a claim +/// against this network needs to be able to check that. +pub fn pinned_root_register_members( + network_id: &[u8], +) -> Result<&'static [PinnedMember], RegisterResolutionError> { + if network_id != BETA_NETWORK_ID { + return Err(RegisterResolutionError::UnknownNetwork { + network_id: network_id.to_vec(), + }); + } + Ok(&BETA_ROOT_REGISTER_MEMBERS) +} + /// Resolve the register for a network. Unknown network ⇒ fail closed. pub fn resolve_root_register_profile( network_id: &[u8], @@ -208,12 +313,23 @@ pub fn resolve_root_register_profile( network_id: network_id.to_vec(), }); } - let members: Vec> = BETA_MEMBERS.iter().map(|m| m.to_vec()).collect(); + // The pinned pairs are the ONE source: both the member list and the set id + // come from them, so the two cannot drift apart. + let pinned = StorageSetMembers::new(&BETA_ROOT_REGISTER_MEMBERS) + .map_err(RegisterResolutionError::ProfileNotDerivable)?; + let storage_set_id = + storage_set_id(&pinned).map_err(RegisterResolutionError::ProfileNotDerivable)?; + let members: Vec> = pinned + .entries() + .iter() + .map(|e| e.member_id().to_vec()) + .collect(); Ok(RootRegisterProfile { // Req 6.13's fixed three-member profile. Read from the DLV profile // module rather than restated, so the threshold has one home. quorum: crate::dlv::beta_storage_profile::SOFI_BETA_QUORUM, members, + storage_set_id, }) } diff --git a/dsm_client/deterministic_state_machine/dsm/tests/economic_admission_lifecycle.rs b/dsm_client/deterministic_state_machine/dsm/tests/economic_admission_lifecycle.rs index da0ffb6c2..b422db125 100644 --- a/dsm_client/deterministic_state_machine/dsm/tests/economic_admission_lifecycle.rs +++ b/dsm_client/deterministic_state_machine/dsm/tests/economic_admission_lifecycle.rs @@ -217,8 +217,7 @@ const SUBSTRATE_ADDR: [u8; 32] = [0xA4; 32]; fn canonical_set_id() -> [u8; 32] { dsm::economic::register::resolve_root_register_profile(b"dsm-testnet") .expect("beta profile") - .derive_set_id(&beta_candidate_set()) - .expect("canonical membership") + .storage_set_id } struct FaucetFixture { @@ -978,10 +977,9 @@ fn policy_bytes_that_do_not_hash_to_the_leg_are_refused() { /// a fixture cannot state one as a constant — it derives it the same way /// production does, from candidate entries the profile then checks. fn beta_candidate_set() -> dsm::ccb::StorageSetMembers { - dsm::ccb::StorageSetMembers::new(&[ - (&b"dsm-node-1"[..], [0xC1; 32]), - (&b"dsm-node-2"[..], [0xC2; 32]), - (&b"dsm-node-3"[..], [0xC3; 32]), - ]) - .expect("beta candidate set") + // Built from the network's PINNED pairs, so a fixture resolves to the + // real committed register rather than to values a fixture chose. + let pinned = dsm::economic::register::pinned_root_register_members(b"dsm-testnet") + .expect("the beta network is known"); + dsm::ccb::StorageSetMembers::new(pinned).expect("pinned beta set") } diff --git a/dsm_client/deterministic_state_machine/dsm/tests/economic_authorized_issuance.rs b/dsm_client/deterministic_state_machine/dsm/tests/economic_authorized_issuance.rs index 037856cef..fb62be7b2 100644 --- a/dsm_client/deterministic_state_machine/dsm/tests/economic_authorized_issuance.rs +++ b/dsm_client/deterministic_state_machine/dsm/tests/economic_authorized_issuance.rs @@ -569,10 +569,9 @@ fn the_same_fixture_with_a_named_second_signer_verifies() { /// a fixture cannot state one as a constant — it derives it the same way /// production does, from candidate entries the profile then checks. fn beta_candidate_set() -> dsm::ccb::StorageSetMembers { - dsm::ccb::StorageSetMembers::new(&[ - (&b"dsm-node-1"[..], [0xC1; 32]), - (&b"dsm-node-2"[..], [0xC2; 32]), - (&b"dsm-node-3"[..], [0xC3; 32]), - ]) - .expect("beta candidate set") + // Built from the network's PINNED pairs, so a fixture resolves to the + // real committed register rather than to values a fixture chose. + let pinned = dsm::economic::register::pinned_root_register_members(b"dsm-testnet") + .expect("the beta network is known"); + dsm::ccb::StorageSetMembers::new(pinned).expect("pinned beta set") } diff --git a/dsm_client/deterministic_state_machine/dsm/tests/economic_dlv_owner_apply_provenance.rs b/dsm_client/deterministic_state_machine/dsm/tests/economic_dlv_owner_apply_provenance.rs index 7a11e9ef1..316ca2db7 100644 --- a/dsm_client/deterministic_state_machine/dsm/tests/economic_dlv_owner_apply_provenance.rs +++ b/dsm_client/deterministic_state_machine/dsm/tests/economic_dlv_owner_apply_provenance.rs @@ -554,10 +554,9 @@ fn an_unresolvable_trader_lineage_fails_closed() { /// a fixture cannot state one as a constant — it derives it the same way /// production does, from candidate entries the profile then checks. fn beta_candidate_set() -> dsm::ccb::StorageSetMembers { - dsm::ccb::StorageSetMembers::new(&[ - (&b"dsm-node-1"[..], [0xC1; 32]), - (&b"dsm-node-2"[..], [0xC2; 32]), - (&b"dsm-node-3"[..], [0xC3; 32]), - ]) - .expect("beta candidate set") + // Built from the network's PINNED pairs, so a fixture resolves to the + // real committed register rather than to values a fixture chose. + let pinned = dsm::economic::register::pinned_root_register_members(b"dsm-testnet") + .expect("the beta network is known"); + dsm::ccb::StorageSetMembers::new(pinned).expect("pinned beta set") } diff --git a/dsm_client/deterministic_state_machine/dsm/tests/economic_dlv_settle_provenance.rs b/dsm_client/deterministic_state_machine/dsm/tests/economic_dlv_settle_provenance.rs index c24f078dc..18ea49561 100644 --- a/dsm_client/deterministic_state_machine/dsm/tests/economic_dlv_settle_provenance.rs +++ b/dsm_client/deterministic_state_machine/dsm/tests/economic_dlv_settle_provenance.rs @@ -1453,10 +1453,9 @@ fn an_over_paying_trade_is_refused_by_re_simulation_alone() { /// a fixture cannot state one as a constant — it derives it the same way /// production does, from candidate entries the profile then checks. fn beta_candidate_set() -> dsm::ccb::StorageSetMembers { - dsm::ccb::StorageSetMembers::new(&[ - (&b"dsm-node-1"[..], [0xC1; 32]), - (&b"dsm-node-2"[..], [0xC2; 32]), - (&b"dsm-node-3"[..], [0xC3; 32]), - ]) - .expect("beta candidate set") + // Built from the network's PINNED pairs, so a fixture resolves to the + // real committed register rather than to values a fixture chose. + let pinned = dsm::economic::register::pinned_root_register_members(b"dsm-testnet") + .expect("the beta network is known"); + dsm::ccb::StorageSetMembers::new(pinned).expect("pinned beta set") } diff --git a/dsm_client/deterministic_state_machine/dsm/tests/economic_lineage_register.rs b/dsm_client/deterministic_state_machine/dsm/tests/economic_lineage_register.rs index 3ea89c988..4e7483a67 100644 --- a/dsm_client/deterministic_state_machine/dsm/tests/economic_lineage_register.rs +++ b/dsm_client/deterministic_state_machine/dsm/tests/economic_lineage_register.rs @@ -63,9 +63,12 @@ fn the_beta_register_resolves_to_the_three_member_fleet_at_q_two() { // that rebuilt its register, changes the id rather than silently // resolving the old register. let candidate = beta_candidate_set(); + p.verify_candidate(&candidate) + .expect("the pinned pairs verify against the pinned id"); assert_eq!( - p.derive_set_id(&candidate).expect("canonical membership"), - dsm::ccb::storage_set_id(&candidate).unwrap() + p.storage_set_id, + dsm::ccb::storage_set_id(&candidate).unwrap(), + "the pinned id IS the digest of the pinned pairs — one source, not two" ); } @@ -82,7 +85,7 @@ fn a_candidate_whose_membership_is_not_the_networks_is_refused() { (&b"attacker-node"[..], [0xC3; 32]), ]) .expect("well-formed but wrong"); - match p.derive_set_id(&impostor) { + match p.verify_candidate(&impostor) { Err(RegisterResolutionError::MembershipNotCanonical { .. }) => {} other => panic!("a foreign membership must be refused, got {other:?}"), } @@ -95,7 +98,7 @@ fn a_candidate_whose_membership_is_not_the_networks_is_refused() { ]) .expect("well-formed but short"); assert!( - p.derive_set_id(&short).is_err(), + p.verify_candidate(&short).is_err(), "a subset of the members is not the set" ); } @@ -106,24 +109,29 @@ fn a_candidate_whose_membership_is_not_the_networks_is_refused() { #[test] fn one_member_rebuilding_its_register_changes_the_set_id() { let p = resolve_root_register_profile(b"dsm-testnet").expect("known network"); - let before = p.derive_set_id(&beta_candidate_set()).expect("canonical"); - - let rebuilt = dsm::ccb::StorageSetMembers::new(&[ - (&b"dsm-node-1"[..], [0xC1; 32]), - (&b"dsm-node-2"[..], [0xC2; 32]), - // node-3 lost its register and generated a new incarnation. Same - // node, same identity key, different durable history. - (&b"dsm-node-3"[..], [0x99; 32]), - ]) - .expect("canonical membership, new incarnation"); - let after = p - .derive_set_id(&rebuilt) - .expect("membership is still canonical"); - - assert_ne!( - before, after, - "a rebuilt register must not resolve to the set it used to serve" - ); + p.verify_candidate(&beta_candidate_set()) + .expect("the pinned set verifies"); + + // node-3 lost its register and minted a new incarnation. Same node, same + // identity key, canonical membership — and a different durable history. + let pinned = dsm::economic::register::pinned_root_register_members(b"dsm-testnet") + .expect("beta network"); + let mut rebuilt_pairs: Vec<(&[u8], [u8; 32])> = pinned.to_vec(); + let last = rebuilt_pairs.len() - 1; + rebuilt_pairs[last].1 = [0x99; 32]; + let rebuilt = dsm::ccb::StorageSetMembers::new(&rebuilt_pairs) + .expect("canonical membership, new incarnation"); + + match p.verify_candidate(&rebuilt) { + Err(RegisterResolutionError::SetIdIsNotThePinnedOne { pinned, derived }) => { + assert_eq!(pinned, p.storage_set_id); + assert_ne!(derived, pinned, "a rebuilt member derives a different id"); + } + other => panic!( + "a member that rebuilt its register must be REFUSED against the pin, not \ + silently resolved to a different register; got {other:?}" + ), + } } #[test] @@ -177,8 +185,7 @@ fn a_signed_claim_round_trips_and_a_tampered_one_does_not() { let (pk, sk) = keypair(); let set = resolve_root_register_profile(b"dsm-testnet") .unwrap() - .derive_set_id(&beta_candidate_set()) - .expect("canonical membership"); + .storage_set_id; let b = body(&pk, set); let envelope = sign_economic_root_claim(&b, &sk).expect("signable"); @@ -214,8 +221,7 @@ fn a_claim_signed_for_one_position_does_not_verify_at_another() { let (pk, sk) = keypair(); let set = resolve_root_register_profile(b"dsm-testnet") .unwrap() - .derive_set_id(&beta_candidate_set()) - .expect("canonical membership"); + .storage_set_id; let at7 = body(&pk, set); let envelope = sign_economic_root_claim(&at7, &sk).expect("signable"); let verified = decode_and_verify_economic_root_claim(&envelope).expect("verifies"); @@ -234,8 +240,7 @@ fn a_member_refuses_a_claim_that_is_not_the_callers() { let (pk, sk) = keypair(); let set = resolve_root_register_profile(b"dsm-testnet") .unwrap() - .derive_set_id(&beta_candidate_set()) - .expect("canonical membership"); + .storage_set_id; let envelope = sign_economic_root_claim(&body(&pk, set), &sk).expect("signable"); let claim = decode_and_verify_economic_root_claim(&envelope).expect("verifies"); @@ -326,8 +331,7 @@ fn registering_an_arbitrary_root_yields_nothing_validated() { admission_manifest_addr: [0xDD; 32], storage_set_id: resolve_root_register_profile(b"dsm-testnet") .unwrap() - .derive_set_id(&beta_candidate_set()) - .expect("canonical membership"), + .storage_set_id, }; assert_eq!( registered.register_key(), @@ -358,8 +362,7 @@ fn a_decodable_but_noncanonical_envelope_is_refused() { let (pk, sk) = keypair(); let set = resolve_root_register_profile(b"dsm-testnet") .unwrap() - .derive_set_id(&beta_candidate_set()) - .expect("canonical membership"); + .storage_set_id; let envelope = sign_economic_root_claim(&body(&pk, set), &sk).expect("signable"); assert!(decode_and_verify_economic_root_claim(&envelope).is_ok()); @@ -379,10 +382,9 @@ fn a_decodable_but_noncanonical_envelope_is_refused() { /// The beta fleet as a catalog resolves it: the network's canonical member /// ids paired with the register incarnations those members are serving. fn beta_candidate_set() -> dsm::ccb::StorageSetMembers { - dsm::ccb::StorageSetMembers::new(&[ - (&b"dsm-node-1"[..], [0xC1; 32]), - (&b"dsm-node-2"[..], [0xC2; 32]), - (&b"dsm-node-3"[..], [0xC3; 32]), - ]) - .expect("beta candidate set") + // Built from the network's PINNED pairs, so a fixture resolves to the + // real committed register rather than to values a fixture chose. + let pinned = dsm::economic::register::pinned_root_register_members(b"dsm-testnet") + .expect("the beta network is known"); + dsm::ccb::StorageSetMembers::new(pinned).expect("pinned beta set") } diff --git a/dsm_client/deterministic_state_machine/dsm/tests/economic_peer_evidence.rs b/dsm_client/deterministic_state_machine/dsm/tests/economic_peer_evidence.rs index dd3cd84a0..08f4509f7 100644 --- a/dsm_client/deterministic_state_machine/dsm/tests/economic_peer_evidence.rs +++ b/dsm_client/deterministic_state_machine/dsm/tests/economic_peer_evidence.rs @@ -671,10 +671,9 @@ fn the_addr_checked_acceptance_bytes_must_hash_to_the_descriptor_address() { /// a fixture cannot state one as a constant — it derives it the same way /// production does, from candidate entries the profile then checks. fn beta_candidate_set() -> dsm::ccb::StorageSetMembers { - dsm::ccb::StorageSetMembers::new(&[ - (&b"dsm-node-1"[..], [0xC1; 32]), - (&b"dsm-node-2"[..], [0xC2; 32]), - (&b"dsm-node-3"[..], [0xC3; 32]), - ]) - .expect("beta candidate set") + // Built from the network's PINNED pairs, so a fixture resolves to the + // real committed register rather than to values a fixture chose. + let pinned = dsm::economic::register::pinned_root_register_members(b"dsm-testnet") + .expect("the beta network is known"); + dsm::ccb::StorageSetMembers::new(pinned).expect("pinned beta set") } diff --git a/dsm_client/deterministic_state_machine/dsm/tests/economic_provenance_semantics.rs b/dsm_client/deterministic_state_machine/dsm/tests/economic_provenance_semantics.rs index a404a8f5f..919303a00 100644 --- a/dsm_client/deterministic_state_machine/dsm/tests/economic_provenance_semantics.rs +++ b/dsm_client/deterministic_state_machine/dsm/tests/economic_provenance_semantics.rs @@ -392,10 +392,9 @@ fn a_transition_with_no_credits_needs_no_provenance() { /// a fixture cannot state one as a constant — it derives it the same way /// production does, from candidate entries the profile then checks. fn beta_candidate_set() -> dsm::ccb::StorageSetMembers { - dsm::ccb::StorageSetMembers::new(&[ - (&b"dsm-node-1"[..], [0xC1; 32]), - (&b"dsm-node-2"[..], [0xC2; 32]), - (&b"dsm-node-3"[..], [0xC3; 32]), - ]) - .expect("beta candidate set") + // Built from the network's PINNED pairs, so a fixture resolves to the + // real committed register rather than to values a fixture chose. + let pinned = dsm::economic::register::pinned_root_register_members(b"dsm-testnet") + .expect("the beta network is known"); + dsm::ccb::StorageSetMembers::new(pinned).expect("pinned beta set") } diff --git a/dsm_client/deterministic_state_machine/dsm/tests/era_faucet_wire.rs b/dsm_client/deterministic_state_machine/dsm/tests/era_faucet_wire.rs index 36bfca1de..42dcbdeaf 100644 --- a/dsm_client/deterministic_state_machine/dsm/tests/era_faucet_wire.rs +++ b/dsm_client/deterministic_state_machine/dsm/tests/era_faucet_wire.rs @@ -635,10 +635,9 @@ fn the_ticket_model_has_no_shared_state_idioms() { /// a fixture cannot state one as a constant — it derives it the same way /// production does, from candidate entries the profile then checks. fn beta_candidate_set() -> dsm::ccb::StorageSetMembers { - dsm::ccb::StorageSetMembers::new(&[ - (&b"dsm-node-1"[..], [0xC1; 32]), - (&b"dsm-node-2"[..], [0xC2; 32]), - (&b"dsm-node-3"[..], [0xC3; 32]), - ]) - .expect("beta candidate set") + // Built from the network's PINNED pairs, so a fixture resolves to the + // real committed register rather than to values a fixture chose. + let pinned = dsm::economic::register::pinned_root_register_members(b"dsm-testnet") + .expect("the beta network is known"); + dsm::ccb::StorageSetMembers::new(pinned).expect("pinned beta set") } diff --git a/dsm_client/deterministic_state_machine/dsm_sdk/src/economic_fixtures.rs b/dsm_client/deterministic_state_machine/dsm_sdk/src/economic_fixtures.rs index 75d734cac..fbfc30166 100644 --- a/dsm_client/deterministic_state_machine/dsm_sdk/src/economic_fixtures.rs +++ b/dsm_client/deterministic_state_machine/dsm_sdk/src/economic_fixtures.rs @@ -397,5 +397,20 @@ pub fn fixture_register_incarnation(member_id: &str) -> String { /// the vault's own storage set stops being resolvable — which is correct /// behaviour and a useless test failure. pub fn fixture_register_incarnation_bytes(member_id: &str) -> [u8; 32] { - *blake3::hash(format!("dsm-test-incarnation/{member_id}").as_bytes()).as_bytes() + // A PINNED member gets its pinned incarnation: a fixture fleet must + // resolve to the network's real committed register, or every economic + // path in the fixture fails closed for the right reason and the test + // proves nothing. Non-members (extra fake nodes) get a derived value — + // they cannot be in the pinned set by construction. + dsm::economic::register::pinned_root_register_members(b"dsm-testnet") + .ok() + .and_then(|pinned| { + pinned + .iter() + .find(|(id, _)| *id == member_id.as_bytes()) + .map(|(_, inc)| *inc) + }) + .unwrap_or_else(|| { + *blake3::hash(format!("dsm-test-incarnation/{member_id}").as_bytes()).as_bytes() + }) } diff --git a/dsm_client/deterministic_state_machine/dsm_sdk/src/handlers/faucet_flow_tests.rs b/dsm_client/deterministic_state_machine/dsm_sdk/src/handlers/faucet_flow_tests.rs index 6ff2e040b..5ee8f0b4e 100644 --- a/dsm_client/deterministic_state_machine/dsm_sdk/src/handlers/faucet_flow_tests.rs +++ b/dsm_client/deterministic_state_machine/dsm_sdk/src/handlers/faucet_flow_tests.rs @@ -135,7 +135,7 @@ fn canonical_set() -> StorageSet { .find(|s| { crate::sdk::storage_set::as_ccb_members(s) .ok() - .and_then(|m| profile.derive_set_id(&m).ok()) + .and_then(|m| profile.verify_candidate(&m).ok()) .is_some() }) .cloned() diff --git a/dsm_client/deterministic_state_machine/dsm_sdk/src/handlers/sender_admission_tests.rs b/dsm_client/deterministic_state_machine/dsm_sdk/src/handlers/sender_admission_tests.rs index 7f602347f..46c361c40 100644 --- a/dsm_client/deterministic_state_machine/dsm_sdk/src/handlers/sender_admission_tests.rs +++ b/dsm_client/deterministic_state_machine/dsm_sdk/src/handlers/sender_admission_tests.rs @@ -79,7 +79,7 @@ async fn an_admitted_burn_advances_the_lineage_and_is_foreign_walkable() { .find(|s| { crate::sdk::storage_set::as_ccb_members(s) .ok() - .and_then(|m| profile.derive_set_id(&m).ok()) + .and_then(|m| profile.verify_candidate(&m).ok()) .is_some() }) .cloned() @@ -667,7 +667,7 @@ async fn token_routes_admit_an_authorized_mint_that_is_foreign_walkable() { .find(|s| { crate::sdk::storage_set::as_ccb_members(s) .ok() - .and_then(|m| profile.derive_set_id(&m).ok()) + .and_then(|m| profile.verify_candidate(&m).ok()) .is_some() }) .cloned() 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 17e0613ea..98d8e8d52 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 @@ -92,7 +92,7 @@ pub(crate) fn canonical_set(network_id: &[u8]) -> Result { StorageSetCatalog::from_env_config().map_err(|e| storage_err("load storage catalog", e))?; // The set id is a function of `(member_id, register_incarnation_id)` // pairs, so it cannot be asked for by name: the catalog offers candidates - // and `derive_set_id` refuses any whose membership is not this network's. + // and `verify_candidate` refuses any that does not re-derive the pinned id. // A member that rebuilt its register therefore stops resolving here // rather than silently serving the register it used to. catalog @@ -101,7 +101,7 @@ pub(crate) fn canonical_set(network_id: &[u8]) -> Result { .find(|s| { crate::sdk::storage_set::as_ccb_members(s) .ok() - .and_then(|m| profile.derive_set_id(&m).ok()) + .and_then(|m| profile.verify_candidate(&m).ok()) .is_some() }) .cloned() diff --git a/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/economic_registers.rs b/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/economic_registers.rs index 6feb40d53..254b8c6e8 100644 --- a/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/economic_registers.rs +++ b/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/economic_registers.rs @@ -358,14 +358,14 @@ impl dsm::economic::peer_lineage::PeerEvidenceFetcher for LiveRegisterResolver<' let catalog = crate::sdk::storage_set::StorageSetCatalog::from_env_config() .map_err(|e| PeerLineageFailure::Incomplete(e.to_string()))?; // The catalog holds sets, not networks: find the one whose membership - // IS this network's, and let `derive_set_id` be the thing that decides + // IS this network's PINNED register, and let `verify_candidate` decide // whether it really is. let candidate = catalog .sets() .iter() .find_map(|s| { let members = crate::sdk::storage_set::as_ccb_members(s).ok()?; - profile.derive_set_id(&members).ok().map(|_| members) + profile.verify_candidate(&members).ok().map(|()| members) }) .ok_or_else(|| { PeerLineageFailure::Incomplete( @@ -673,14 +673,14 @@ impl ProvenanceResolver for LiveRegisterResolver<'_> { let catalog = crate::sdk::storage_set::StorageSetCatalog::from_env_config() .map_err(|e| PeerLineageFailure::Incomplete(e.to_string()))?; // The catalog holds sets, not networks: find the one whose membership - // IS this network's, and let `derive_set_id` be the thing that decides + // IS this network's PINNED register, and let `verify_candidate` decide // whether it really is. let candidate = catalog .sets() .iter() .find_map(|s| { let members = crate::sdk::storage_set::as_ccb_members(s).ok()?; - profile.derive_set_id(&members).ok().map(|_| members) + profile.verify_candidate(&members).ok().map(|()| members) }) .ok_or_else(|| { PeerLineageFailure::Incomplete( diff --git a/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/storage_set.rs b/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/storage_set.rs index b9b5e56d7..22e3eb6cc 100644 --- a/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/storage_set.rs +++ b/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/storage_set.rs @@ -246,6 +246,59 @@ impl StorageSetCatalog { mod tests { use super::*; + /// THE PIN, RENDERED BACK TO WHAT THE NODES LOGGED. The byte values in + /// `dsm::economic::register` are the authority; this proves they are the + /// same values each provisioned member logged as + /// `register incarnation for node : ` at first boot, + /// so the pin cannot silently drift from what the fleet actually holds. + /// THE FLEET'S OWN DERIVATION, PINNED. Every provisioned member computed + /// this set id from its configured `[[storage_set.members]]` at restart and + /// logged `storage set configured: 3 members, id=`. Core must + /// re-derive the same digest from the same pairs, or the pin and the fleet + /// name different registers. + #[test] + fn the_pinned_beta_set_id_is_the_one_every_provisioned_member_logged() { + let profile = dsm::economic::register::resolve_root_register_profile(b"dsm-testnet") + .expect("the beta network is provisioned"); + assert_eq!( + crate::util::text_id::encode_base32_crockford(&profile.storage_set_id), + "E05YS8101EJH33KY2CG625JJE8A0Z4GJNSEM335TX1XVTWM9RR8G", + "core's derivation must equal what dsm-node-1, -2 and -3 each logged" + ); + } + + #[test] + fn beta_root_register_pins_render_to_the_logged_values() { + let logged = [ + ( + "dsm-node-1", + "DXWR7W9J2E5ASQ5BJBYF13ZZEK1VFTZFYNWAYPF1KNT8C33YPVM0", + ), + ( + "dsm-node-2", + "H4ZSDG34M1BSQQH8T9WWWZ65Y90YW9QY2CYRR2EG3H621VDGJ3W0", + ), + ( + "dsm-node-3", + "VW3REAWA7PR608Y4AY3VX18M8BE4828PFPNVTG380XV18HKF8SSG", + ), + ]; + let pinned = dsm::economic::register::pinned_root_register_members(b"dsm-testnet") + .expect("beta network"); + assert_eq!(pinned.len(), logged.len()); + for (id, b32) in logged { + let (_, inc) = pinned + .iter() + .find(|(m, _)| *m == id.as_bytes()) + .unwrap_or_else(|| panic!("{id} is pinned")); + assert_eq!( + crate::util::text_id::encode_base32_crockford(inc), + b32, + "{id}: the pinned bytes must render to what that node logged" + ); + } + } + fn m(id: &str, ep: &str) -> StorageMember { // A distinct incarnation per member id, so a test never accidentally // asserts over a set whose entries collide. diff --git a/scripts/dsm_env_config.alibaba.toml b/scripts/dsm_env_config.alibaba.toml index cd9d0a2f9..53c824fe0 100644 --- a/scripts/dsm_env_config.alibaba.toml +++ b/scripts/dsm_env_config.alibaba.toml @@ -29,14 +29,24 @@ custom_ca_certs = ["ca.crt"] # A name that merely describes the node ("us-west-1a-1") makes the client # compute a set id no node belongs to: every settlement-slot claim is refused as # a foreign set and no publication ever reaches quorum. +# +# `register_incarnation` is the register history that node is serving, as its +# own database minted it at first boot and logged it. The set id is derived from +# the (name, register_incarnation) pairs, and the network's root-register +# profile PINS that id in core — so a node that rebuilt its register stops +# resolving here rather than silently serving the register it used to. +# Provisioned 2026-09-05; set id E05YS8101EJH33KY2CG625JJE8A0Z4GJNSEM335TX1XVTWM9RR8G. [[nodes]] name = "dsm-node-1" endpoint = "https://47.251.246.93:8080" +register_incarnation = "DXWR7W9J2E5ASQ5BJBYF13ZZEK1VFTZFYNWAYPF1KNT8C33YPVM0" [[nodes]] name = "dsm-node-2" endpoint = "https://47.251.250.159:8080" +register_incarnation = "H4ZSDG34M1BSQQH8T9WWWZ65Y90YW9QY2CYRR2EG3H621VDGJ3W0" [[nodes]] name = "dsm-node-3" endpoint = "https://47.251.88.58:8080" +register_incarnation = "VW3REAWA7PR608Y4AY3VX18M8BE4828PFPNVTG380XV18HKF8SSG"