Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,20 @@

//! The beta storage profile — a FIXED PROFILE, not a quorum function.
//!
//! OWNER DECISION (2026-08-23): the deployed beta storage set is the
//! THREE-member Alibaba ECS fleet, and the profile is fixed at `q = 2`. If
//! one member is unavailable, both remaining members are required; if two or
//! more are unavailable, a new settlement decision cannot be established
//! until the fixed threshold is again reachable. Rev 15 Req 6.13 states
//! this same three-member/q=2 profile (amended 2026-08-24 from the earlier
//! five-member/q=4 draft).
//! OWNER DECISION (2026-09-12): the deployed beta storage set is the
//! FIVE-member GCP fleet, and the profile is fixed at `q = 3`. Up to two
//! members may be unavailable; with three or more gone, a new settlement
//! decision cannot be established until the fixed threshold is again
//! reachable. Req 6.13 is amended to this five-member/q=3 profile.
//!
//! On the threshold specifically. Rev 15 Req 6.13 originally fixed `n=5, q=4`
//! for an intersection margin of `4 + 4 − 5 = 3`, and the 2026-08-21
//! conformance delta warned that deploying five nodes under a majority rule
//! collapses that margin to `≥ 1`. The owner chose `q = 3` knowing this: it is
//! the SAME margin already accepted when Req 6.13 was amended on 2026-08-24 to
//! the three-member fleet (`2 + 2 − 3 = 1`), and it keeps the profile equal to
//! the canonical majority a consumer actually calls, which the test below
//! pins. What changed is the fleet, not the safety argument.
//!
//! That is a threshold for ONE cardinality. There is no quorum function
//! over arbitrary `n`, so this module refuses every other set size rather than
Expand Down Expand Up @@ -38,16 +45,16 @@
//!
//! The state-identity cut commits `q` inside the signed `V_n` (field 15),
//! sourced at birth from `quorum_for(|S|)` over the resolved catalog set —
//! which equals this profile's threshold for the three-member fleet. This
//! which equals this profile's threshold for the five-member fleet. This
//! module remains the named, testable statement of the fixed profile a
//! conformance check reads against, rather than a recomputation.

/// The one storage-set cardinality the beta profile defines a threshold
/// for: the deployed three-member fleet.
pub const SOFI_BETA_MEMBERS: usize = 3;
/// for: the deployed five-member fleet.
pub const SOFI_BETA_MEMBERS: usize = 5;

/// The fixed threshold for that cardinality.
pub const SOFI_BETA_QUORUM: u32 = 2;
pub const SOFI_BETA_QUORUM: u32 = 3;

/// A storage set that is not the Rev 15 beta profile.
///
Expand Down Expand Up @@ -109,21 +116,21 @@ mod tests {

/// The profile the owner fixed for the deployed fleet.
#[test]
fn the_three_member_beta_set_commits_two() {
assert_eq!(sofi_beta_quorum_for(3).expect("the beta profile"), 2);
assert_eq!(SOFI_BETA_QUORUM, 2);
assert_eq!(SOFI_BETA_MEMBERS, 3);
fn the_five_member_beta_set_commits_three() {
assert_eq!(sofi_beta_quorum_for(5).expect("the beta profile"), 3);
assert_eq!(SOFI_BETA_QUORUM, 3);
assert_eq!(SOFI_BETA_MEMBERS, 5);
}

/// EVERY other cardinality is refused, including the ones that look
/// plausible. `5` is the superseded profile, `1` is local dev, and `2`
/// and `4` are one step either side — none of them is the beta storage
/// plausible. `3` is the superseded Alibaba profile, `1` is local dev, and
/// `4` and `6` are one step either side — none of them is the beta storage
/// set, and none may be silently mapped to a threshold.
#[test]
fn every_other_cardinality_is_refused_rather_than_mapped() {
for n in [0usize, 1, 2, 4, 5, 6, 7, 9, 100] {
for n in [0usize, 1, 2, 3, 4, 6, 7, 9, 100] {
let err = sofi_beta_quorum_for(n)
.expect_err("only the three-member profile has a defined threshold");
.expect_err("only the five-member profile has a defined threshold");
assert_eq!(err.members, n, "the error names the set it was handed");
}
}
Expand All @@ -134,7 +141,7 @@ mod tests {
/// helper would have produced are NOT returned.
#[test]
fn no_majority_fallback_survives_behind_the_refusal() {
for (n, majority) in [(1usize, 1u32), (5, 3), (6, 4), (7, 4)] {
for (n, majority) in [(1usize, 1u32), (3, 2), (6, 4), (7, 4)] {
match sofi_beta_quorum_for(n) {
Err(_) => {}
Ok(q) => panic!(
Expand All @@ -146,18 +153,18 @@ mod tests {
}

/// The profile's threshold and the canonical rule verification now applies
/// agree where both are defined — the three-member fleet — and the
/// agree where both are defined — the five-member fleet — and the
/// canonical rule is what a consumer actually calls.
#[test]
fn the_profile_threshold_is_the_canonical_quorum_for_the_deployed_fleet() {
assert_eq!(
sofi_beta_quorum_for(SOFI_BETA_MEMBERS).expect("the deployed cardinality"),
crate::economic::cell_observation::canonical_quorum(SOFI_BETA_MEMBERS),
);
crate::economic::cell_observation::require_canonical_quorum(3, 2)
crate::economic::cell_observation::require_canonical_quorum(5, 3)
.expect("the profile's own q is canonical");
for not_q in [1u32, 3] {
crate::economic::cell_observation::require_canonical_quorum(3, not_q)
for not_q in [2u32, 4] {
crate::economic::cell_observation::require_canonical_quorum(5, not_q)
.expect_err("only the canonical value is accepted");
}
}
Expand Down
48 changes: 33 additions & 15 deletions dsm_client/deterministic_state_machine/dsm/src/economic/register.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,34 +245,52 @@ impl std::error::Error for RegisterResolutionError {}
/// 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
/// Provisioned 2026-09-12 (UTC): the beta fleet moved from the three-member
/// Alibaba set to the FIVE-member GCP set (us-central1). Each node was created
/// empty and minted its incarnation on first boot into its own fresh database;
/// these are the values 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] = [
/// Nothing from the Alibaba set carries over — its members, its incarnations and
/// its set id are retired, and a claim made under it cannot validate here.
const BETA_ROOT_REGISTER_MEMBERS: [PinnedMember; 5] = [
(
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,
0x2E, 0xF7, 0xC8, 0x0E, 0x0B, 0x01, 0x1E, 0x8A, 0xC0, 0x83, 0xDE, 0xE6, 0xA4, 0xB9,
0x0C, 0xC5, 0xBF, 0xD8, 0xAD, 0x90, 0x01, 0x21, 0x9E, 0x67, 0x0C, 0x2F, 0xF0, 0x3F,
0xC4, 0x48, 0xD0, 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,
0xA0, 0x39, 0x3A, 0xCD, 0x83, 0x0B, 0xCA, 0x06, 0x82, 0x1F, 0xE1, 0xA5, 0xEA, 0x27,
0xDF, 0xD3, 0x72, 0x34, 0x70, 0x6A, 0x69, 0xE1, 0xEF, 0xD6, 0xA4, 0x05, 0x6F, 0xB4,
0x4E, 0xD7, 0x1B, 0xCA,
],
),
(
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,
0x9B, 0x15, 0x05, 0x78, 0xF8, 0x28, 0x51, 0x9A, 0xC8, 0x0E, 0xAA, 0x58, 0x24, 0x81,
0xEB, 0xE3, 0xCA, 0x91, 0x6D, 0x67, 0x3D, 0x39, 0x35, 0x45, 0x19, 0xD5, 0x21, 0xB5,
0xB1, 0xFA, 0x5F, 0x05,
],
),
(
b"dsm-node-4",
[
0x51, 0x29, 0x0F, 0xFC, 0xA1, 0x90, 0x81, 0x59, 0xFA, 0xAC, 0x2B, 0xC4, 0x60, 0x43,
0xD6, 0xB6, 0x63, 0x12, 0x6F, 0x68, 0x77, 0x22, 0x8F, 0x0C, 0xA5, 0x7E, 0x50, 0xD7,
0xC1, 0x1A, 0x77, 0x10,
],
),
(
b"dsm-node-5",
[
0xBD, 0x71, 0xB9, 0xEA, 0x93, 0x44, 0x6D, 0xE4, 0xB7, 0xD5, 0xC9, 0xAA, 0xFF, 0xE0,
0xF7, 0x8B, 0x12, 0x3F, 0x21, 0xF9, 0x5C, 0x02, 0x10, 0x17, 0x08, 0xAB, 0x4B, 0x63,
0xAC, 0x67, 0x4E, 0x22,
],
),
];
Expand Down Expand Up @@ -325,7 +343,7 @@ pub fn resolve_root_register_profile(
.map(|e| e.member_id().to_vec())
.collect();
Ok(RootRegisterProfile {
// Req 6.13's fixed three-member profile. Read from the DLV profile
// Req 6.13's fixed five-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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,10 @@ fn each_position_of_each_identity_is_its_own_cell() {
// ── Network-scoped resolution, fail closed ─────────────────────────────────

#[test]
fn the_beta_register_resolves_to_the_three_member_fleet_at_q_two() {
fn the_beta_register_resolves_to_the_five_member_fleet_at_q_three() {
let p = resolve_root_register_profile(b"dsm-testnet").expect("known network");
assert_eq!(p.members.len(), 3);
assert_eq!(p.quorum, 2);
assert_eq!(p.members.len(), 5);
assert_eq!(p.quorum, 3);
// The set id is a re-derivation over `(member, incarnation)` pairs, not a
// constant somebody typed — so a member list that drifts, or a member
// that rebuilt its register, changes the id rather than silently
Expand Down Expand Up @@ -90,7 +90,7 @@ fn a_candidate_whose_membership_is_not_the_networks_is_refused() {
other => panic!("a foreign membership must be refused, got {other:?}"),
}

// A SHORT set is refused too: a quorum argument over two of the three
// A SHORT set is refused too: a quorum argument over a subset of the pinned
// members is not this network's register.
let short = dsm::ccb::StorageSetMembers::new(&[
(&b"dsm-node-1"[..], [0xC1; 32]),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,43 @@ impl Drop for FleetGuard {
}
}

/// Point the loader at a fleet whose member NAMES are the canonical register
/// members (`dsm-node-1..3`).
/// The network's PINNED root-register member ids, in pin order.
///
/// Every fixture fleet is derived from this rather than restating a member
/// count, so re-pinning the register cannot leave a fixture resolving to a set
/// the network no longer commits to.
pub fn canonical_member_ids() -> Vec<String> {
dsm::economic::register::pinned_root_register_members(NETWORK)
.expect("the beta network is pinned")
.iter()
.map(|(id, _)| String::from_utf8(id.to_vec()).expect("pinned member ids are UTF-8"))
.collect()
}

/// The pinned register quorum `q`.
pub fn canonical_quorum() -> usize {
dsm::economic::register::resolve_root_register_profile(NETWORK)
.expect("the beta network is pinned")
.quorum as usize
}

/// How many nodes one b0x submit lands on: the submit loop stops at `q`
/// successes, so a fleet of `n > q` holds a message on `q` nodes, not all `n`.
/// "Every node" only held while the fleet was exactly `q` wide.
pub fn delivery_quorum() -> usize {
canonical_quorum().min(canonical_member_ids().len())
}

/// The fewest pinned members whose loss leaves the fleet below quorum: the
/// LAST `n − q + 1` in pin order, so exactly `q − 1` stay reachable and the
/// members earlier in pin order are the ones still answering.
pub fn members_to_break_quorum() -> Vec<String> {
let ids = canonical_member_ids();
ids[canonical_quorum() - 1..].to_vec()
}

/// Point the loader at a fleet whose member NAMES are the pinned register
/// members.
///
/// The profile resolves its set by RE-HASHING member ids, so the default
/// `test-1..3` fleet can never satisfy it. The endpoints are irrelevant: all
Expand All @@ -65,10 +100,11 @@ pub fn install_canonical_fleet() -> FleetGuard {
let mut cfg = String::from(
"protocol = \"http\"\nlan_ip = \"127.0.0.1\"\nallow_localhost = true\nports = [8080]\n",
);
for i in 1..=3 {
let inc = fixture_register_incarnation(&format!("dsm-node-{i}"));
for (i, id) in canonical_member_ids().iter().enumerate() {
let inc = fixture_register_incarnation(id);
let port = 8081 + i;
cfg.push_str(&format!(
"\n[[nodes]]\nname = \"dsm-node-{i}\"\nendpoint = \"http://127.0.0.1:808{i}\"\n\
"\n[[nodes]]\nname = \"{id}\"\nendpoint = \"http://127.0.0.1:{port}\"\n\
register_incarnation = \"{inc}\"\n"
));
}
Expand Down Expand Up @@ -333,15 +369,15 @@ pub fn mint_asset(router: &AppRouterImpl, ticker: &str, decimals: u32, amount: u
/// must burn nothing and advance nothing") stays reachable without reaching for
/// an unfunded or fabricated head.
pub fn take_register_offline() {
for i in 1..=3 {
crate::sdk::storage_io::fake_registers::fail_member(&format!("dsm-node-{i}"), true);
for id in canonical_member_ids() {
crate::sdk::storage_io::fake_registers::fail_member(&id, true);
}
}

/// Bring the register fleet back up.
pub fn bring_register_online() {
for i in 1..=3 {
crate::sdk::storage_io::fake_registers::fail_member(&format!("dsm-node-{i}"), false);
for id in canonical_member_ids() {
crate::sdk::storage_io::fake_registers::fail_member(&id, false);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -525,7 +525,11 @@ async fn r4_a_storage_ack_cannot_release_the_sender_gate() {
.first()
.map(|s| s.message_id.clone())
.expect("a submit");
assert_eq!(p.acked_count(&transfer_id), 3, "B ACKed on every node");
assert_eq!(
p.acked_count(&transfer_id),
crate::economic_fixtures::delivery_quorum(),
"B ACKed on every node that holds the transfer"
);

p.a.enter();
let calibrated =
Expand Down Expand Up @@ -569,8 +573,8 @@ async fn r4_a_storage_ack_cannot_release_the_sender_gate() {
.collect();
assert_eq!(
certificates.len(),
3,
"exactly one certificate per node (deterministic id, quorum 3)"
crate::economic_fixtures::delivery_quorum(),
"exactly one certificate per delivering node (deterministic id)"
);
assert!(
certificates
Expand Down Expand Up @@ -692,7 +696,10 @@ async fn r7_a_frozen_checkpoint_is_replayed_byte_identically_after_the_fleet_ret
pending_heads, 0,
"the certificate's signing key was promoted and deleted"
);
let submits_before = p.submits().len();
// Per node: `submits()` concatenates node by node, and nodes no longer
// receive equal counts once the fleet is wider than the delivery quorum,
// so a flat skip would drop the wrong posts.
let submits_before: Vec<usize> = p.nodes.iter().map(|n| n.submits().len()).collect();

// The fleet returns; the sweep replays.
p.override_all_submits(None);
Expand All @@ -710,12 +717,17 @@ async fn r7_a_frozen_checkpoint_is_replayed_byte_identically_after_the_fleet_ret
assert_eq!(proposal_statuses(&rel).len(), 1, "no second proposal");
assert_eq!(p.a.era_balance(), 990, "no second debit");
let replayed: Vec<_> = p
.submits()
.into_iter()
.skip(submits_before)
.nodes
.iter()
.zip(&submits_before)
.flat_map(|(n, before)| n.submits().into_iter().skip(*before))
.filter(|s| s.message_id == frozen_id)
.collect();
assert_eq!(replayed.len(), 3, "one replay per node under the frozen id");
assert_eq!(
replayed.len(),
crate::economic_fixtures::delivery_quorum(),
"one replay per delivering node under the frozen id"
);
for r in &replayed {
assert_eq!(
r.body, frozen_bytes,
Expand Down Expand Up @@ -767,7 +779,9 @@ async fn r8_a_next_generation_transfer_is_held_until_the_certificate_lands() {
// A's second transfer arrives at B before the certificate.
let sent2 = p.a.send(&p.b, 7).await;
assert!(sent2.success, "{:?}", sent2.error_message);
let transfer2_id = p
// Node 0 is first in the submit loop's order, so it receives every submit;
// the flat `p.submits()` ends on whichever wider-fleet node posted last.
let transfer2_id = p.nodes[0]
.submits()
.last()
.map(|s| s.message_id.clone())
Expand Down Expand Up @@ -803,7 +817,10 @@ async fn r8_a_next_generation_transfer_is_held_until_the_certificate_lands() {
staging_states(),
vec!["accepted".to_string(), "accepted".to_string()]
);
assert_eq!(p.acked_count(&transfer2_id), 3);
assert_eq!(
p.acked_count(&transfer2_id),
crate::economic_fixtures::delivery_quorum()
);
// And generation #2 finalizes normally on both sides.
let a_sync = p.a.sync().await;
assert!(a_sync.success, "{:?}", a_sync.errors);
Expand Down
Loading
Loading