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 ab9276d0..e55ef0b0 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 431bee8b..3e4b4db1 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 1e8b89a3..23dbddd2 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 da0ffb6c..b422db12 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 037856ce..fb62be7b 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 7a11e9ef..316ca2db 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 c24f078d..18ea4956 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 3ea89c98..4e7483a6 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 dd3cd84a..08f4509f 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 a404a8f5..919303a0 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 36bfca1d..42dcbdea 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 75d734ca..fbfc3016 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 6ff2e040..5ee8f0b4 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 7f602347..46c361c4 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 17e0613e..98d8e8d5 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 6feb40d5..254b8c6e 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 b9b5e56d..22e3eb6c 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 cd9d0a2f..53c824fe 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"