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
15 changes: 11 additions & 4 deletions dsm_client/deterministic_state_machine/dsm/src/ccb/decode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -302,13 +302,20 @@ pub fn decode_vault_state(bytes: &[u8]) -> Result<VaultStateV2, DecodeError> {
// 14 StorageSet.
c.envelope(class::STORAGE_SET, StorageSetMembers::SCHEMA)?;
let member_count = c.u32()?;
let mut members: Vec<Vec<u8>> = Vec::new();
let mut entries: Vec<(Vec<u8>, [u8; 32])> = Vec::new();
for _ in 0..member_count {
let len = c.u32()? as usize;
members.push(c.take(len)?.to_vec());
let member_id = c.take(len)?.to_vec();
// The incarnation is part of the entry, not a trailing array: a
// truncated stream fails here rather than producing a set whose
// members have lost their incarnations.
entries.push((member_id, c.digest32()?));
}
let member_refs: Vec<&[u8]> = members.iter().map(|m| m.as_slice()).collect();
let storage_set = StorageSetMembers::new(&member_refs).map_err(invalid)?;
let entry_refs: Vec<(&[u8], [u8; 32])> = entries
.iter()
.map(|(id, inc)| (id.as_slice(), *inc))
.collect();
let storage_set = StorageSetMembers::new(&entry_refs).map_err(invalid)?;

let quorum = c.u32()?; // 15

Expand Down
39 changes: 31 additions & 8 deletions dsm_client/deterministic_state_machine/dsm/src/ccb/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ pub use devtree::{
};
pub use genesis::{genesis_v3_commitment, sigalg, GenesisParamsV3};
pub use state::{
EncumbranceClaim, EncumbranceSet, FeePolicy, MarketPolicy, ReleasePolicy, StorageSetMembers,
VaultStateV2,
EncumbranceClaim, EncumbranceSet, FeePolicy, MarketPolicy, ReleasePolicy, StorageSetEntry,
StorageSetMembers, VaultStateV2,
};

/// Object-class discriminants, from the single namespace of registry §3.
Expand Down Expand Up @@ -201,7 +201,17 @@ pub mod schema {
pub const BURNED: &[(u16, u16)] = &[
(super::class::VAULT_STATE_V2, 1),
(super::class::VAULT_STATE_V2, 2),
// Schema 3 nested `0x0002` at schema 2, i.e. a storage set of bare
// member ids. The register-incarnation cut makes field 14 a set of
// `(member_id, register_incarnation_id)` pairs, and §2.7 nests by
// complete CCB — so the enclosing bytes differ even though the field
// list did not move.
(super::class::VAULT_STATE_V2, 3),
(super::class::STORAGE_SET, 1),
// A set of bare member ids says only WHICH NODES a vault trusts. A
// member that rebuilt its register still satisfied it, which is the
// ambiguity schema 3 removes.
(super::class::STORAGE_SET, 2),
(super::class::ENCUMBRANCE_CLAIM, 1),
(super::class::ENCUMBRANCE_SET, 1),
];
Expand Down Expand Up @@ -255,6 +265,10 @@ pub enum CcbError {
DuplicateSetElement { class: u16 },
/// A storage set had no members, or a member id was empty.
EmptyStorageSetOrMember,
/// A storage-set entry carried an all-zero register incarnation — the
/// value a member has before it has established one. Committing it would
/// bind a vault to an incarnation the member had not yet decided.
ZeroRegisterIncarnation,
/// `token_a_policy_commit` was not strictly less than `token_b`.
TokenPairNotStrictlyOrdered,
/// `fee_bps` was at or above the denominator.
Expand Down Expand Up @@ -336,6 +350,11 @@ impl core::fmt::Display for CcbError {
CcbError::EmptyStorageSetOrMember => {
write!(f, "storage set: at least one member, and no empty member id")
}
CcbError::ZeroRegisterIncarnation => write!(
f,
"storage set: a member's register incarnation is all zero, which is the \
value it has before establishing one"
),
CcbError::TokenPairNotStrictlyOrdered => write!(
f,
"market policy: token_a must be strictly less than token_b; \
Expand Down Expand Up @@ -560,11 +579,15 @@ pub fn parent_state_commitment_for_successor_of(

/// `storage_set_id = H_dom(DSM/storage-set, CCB(S))`.
///
/// An ordinary CCB object under an ordinary domain. Both halves changed with
/// the cut: the frozen envelope-less layout became `0x0002` schema 2, and the
/// `DSM/storage-set/v1` tag that named it is burned in favour of the
/// normative `DSM/storage-set`. Set ids therefore differ from the deployed
/// ones, which is the reprovision rather than a regression.
/// An ordinary CCB object under an ordinary domain, over the canonical
/// ordered list of `(member_id, register_incarnation_id)` pairs.
///
/// The id is therefore not merely *which nodes* a vault trusts, but *which
/// durable register histories on those nodes*. That is the whole authority
/// commitment: a member that rebuilt its register is a different entry, so it
/// resolves to a different set id and cannot serve the vault that committed
/// the old one. Set ids differ from schema-2 ones, which is the reprovision
/// rather than a regression.
pub fn storage_set_id(members: &StorageSetMembers) -> Result<[u8; 32], CcbError> {
let body = members.encode()?;
let mut h: Hasher = dsm_domain_hasher(TAG_DSM_STORAGE_SET);
Expand Down Expand Up @@ -609,7 +632,7 @@ mod dlv_policy_digest_tests {
iteration_budget: None,
parent_state_commitment: [0; 32],
owner_authority_transition_digest: [0; 32],
storage_set: StorageSetMembers::new(&[&[9u8; 32][..]]).expect("set"),
storage_set: StorageSetMembers::new(&[(&[9u8; 32][..], [0xE1; 32])]).expect("set"),
quorum: 1,
};
let sa =
Expand Down
126 changes: 98 additions & 28 deletions dsm_client/deterministic_state_machine/dsm/src/ccb/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,60 +7,127 @@ use super::{
push_u32, push_u64, CcbError, CcbObject, FEE_DENOMINATOR,
};

/// `0x0002` schema 2 — the committed storage set, an ordinary CCB object.
/// One committed set entry: a member, and the register incarnation that
/// member was serving when the vault committed this set.
///
/// The pair is ONE authority fact — "this member, in this register
/// incarnation" — so it is one object rather than two index-aligned arrays.
/// `member_id` stays independently readable, because a resolver still needs
/// it to find the member's endpoint; `register_incarnation_id` stays
/// independently verifiable, because every read requires the responding
/// member to echo the exact value committed here.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StorageSetEntry {
member_id: Vec<u8>,
register_incarnation_id: [u8; 32],
}

impl StorageSetEntry {
pub fn member_id(&self) -> &[u8] {
&self.member_id
}

pub fn register_incarnation_id(&self) -> [u8; 32] {
self.register_incarnation_id
}
}

/// `0x0002` schema 3 — the committed storage set, an ordinary CCB object.
///
/// Schema 1 froze an envelope-less layout because deployed anchors committed
/// set ids under it. The state-identity cut deletes those anchors, so the
/// exception is gone and this class carries the §2.1 envelope like every
/// other. The special-case warning that used to live here is deleted with the
/// special case.
/// set ids under it. The state-identity cut deleted those anchors, so schema 2
/// carried the §2.1 envelope like every other class.
///
/// Schema 3 changes WHAT is committed, not just how. A set of bare node ids
/// says only *which nodes*; a member that lost and rebuilt its register still
/// satisfies it, and can then assert emptiness for a cell the real incarnation
/// once held — an undetectable substitution, because owning the node identity
/// was the whole test. An entry is now the pair, so the set id commits to
/// *which durable register histories on those nodes*, and a rebuilt member
/// cannot impersonate continuity merely by still holding its identity key.
///
/// Set ids therefore differ from schema-2 ones. That is the reprovision, and
/// it is the point: an ambiguous authority encoding is not worth preserving.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StorageSetMembers {
members: Vec<Vec<u8>>,
entries: Vec<StorageSetEntry>,
}

impl CcbObject for StorageSetMembers {
const CLASS: u16 = class::STORAGE_SET;
const SCHEMA: u16 = 2;
const SCHEMA: u16 = 3;
}

impl StorageSetMembers {
/// Sorts the ids and refuses an empty set, an empty id, or a duplicate.
/// Sorts by MEMBER ID and refuses an empty set, an empty member id, a
/// duplicate member id, or a zero incarnation.
///
/// Sorting is by `member_id` alone, never by the pair: sorting on the
/// whole entry would let one member appear twice under two incarnations
/// and still produce a strictly ascending list, which is exactly the
/// ambiguity this schema exists to remove. A duplicate member id is
/// therefore refused REGARDLESS of incarnation.
///
/// Sorting is canonicalization the format calls for; refusing duplicates
/// is not. A duplicate is a producer bug, and collapsing it would map two
/// logical inputs onto one encoding.
pub fn new(member_ids: &[&[u8]]) -> Result<Self, CcbError> {
if member_ids.is_empty() || member_ids.iter().any(|id| id.is_empty()) {
/// An all-zero incarnation is refused because that is the value a member
/// has before it has ever established one; committing it would bind a
/// vault to "whatever this node had not yet decided".
pub fn new(entries: &[(&[u8], [u8; 32])]) -> Result<Self, CcbError> {
if entries.is_empty() || entries.iter().any(|(id, _)| id.is_empty()) {
return Err(CcbError::EmptyStorageSetOrMember);
}
let mut members: Vec<Vec<u8>> = member_ids.iter().map(|id| id.to_vec()).collect();
members.sort_unstable();
if members.windows(2).any(|w| w[0] == w[1]) {
if entries.iter().any(|(_, inc)| inc == &[0u8; 32]) {
return Err(CcbError::ZeroRegisterIncarnation);
}
let mut entries: Vec<StorageSetEntry> = entries
.iter()
.map(|(id, inc)| StorageSetEntry {
member_id: id.to_vec(),
register_incarnation_id: *inc,
})
.collect();
entries.sort_by(|a, b| a.member_id.cmp(&b.member_id));
if entries.windows(2).any(|w| w[0].member_id == w[1].member_id) {
return Err(CcbError::DuplicateSetElement {
class: class::STORAGE_SET,
});
}
Ok(Self { members })
Ok(Self { entries })
}

pub fn len(&self) -> usize {
self.members.len()
self.entries.len()
}

pub fn is_empty(&self) -> bool {
self.members.is_empty()
self.entries.is_empty()
}

/// The committed entries, ascending by member id.
pub fn entries(&self) -> &[StorageSetEntry] {
&self.entries
}

/// `envelope ‖ u32_be(count) ‖ for each id in ascending byte order:
/// u32_be(len) ‖ id`.
/// The incarnation this set commits for `member_id`, if it is a member.
///
/// A reader resolves an endpoint by member id and then requires THIS
/// value back from whatever answers there.
pub fn register_incarnation_of(&self, member_id: &[u8]) -> Option<[u8; 32]> {
self.entries
.iter()
.find(|e| e.member_id == member_id)
.map(|e| e.register_incarnation_id)
}

/// `envelope ‖ u32_be(count) ‖ for each entry in ascending member-id
/// order: u32_be(len) ‖ member_id ‖ register_incarnation_id`.
pub fn encode(&self) -> Result<Vec<u8>, CcbError> {
let mut out = Vec::new();
push_envelope::<Self>(&mut out);
let count = u32::try_from(self.members.len()).map_err(|_| CcbError::LengthOverflow)?;
let count = u32::try_from(self.entries.len()).map_err(|_| CcbError::LengthOverflow)?;
push_u32(&mut out, count);
for id in &self.members {
push_bytes(&mut out, id)?;
for e in &self.entries {
push_bytes(&mut out, &e.member_id)?;
push_digest32(&mut out, &e.register_incarnation_id);
}
Ok(out)
}
Expand Down Expand Up @@ -290,10 +357,13 @@ pub struct VaultStateV2 {

impl CcbObject for VaultStateV2 {
const CLASS: u16 = class::VAULT_STATE_V2;
/// Schema 3. Schema 2 named field 13 but nested `0x0002`/`0x0005` at
/// schema 1; §2.7 nests by complete CCB, so its bytes differ from these
/// despite an identical field list.
const SCHEMA: u16 = 3;
/// Schema 4. The field list is unchanged from schema 3; field 14 now
/// nests `0x0002` at schema 3 (the storage set carries each member's
/// register incarnation), and §2.7 nests by complete CCB including the
/// nested schema version — so a nested bump propagates upward whether or
/// not this object's own fields moved. Schemas 1 and 2 are burned for the
/// same reason.
const SCHEMA: u16 = 4;
}

impl VaultStateV2 {
Expand Down
23 changes: 19 additions & 4 deletions dsm_client/deterministic_state_machine/dsm/src/economic/lineage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -559,16 +559,31 @@ pub fn advance_validated(
// The canonical register set for the claimant's network, resolved
// FAIL-CLOSED: an unknown network refuses rather than defaulting, and a
// winning claim naming any other set is foreign whatever its bytes say.
let canonical_set = crate::economic::register::resolve_root_register_profile(network_id)
.map_err(|e| {
let profile =
crate::economic::register::resolve_root_register_profile(network_id).map_err(|e| {
EconomicValidationError::Provenance(ProvenanceError::FaucetWinnerInvalid(match e {
crate::economic::register::RegisterResolutionError::UnknownNetwork { .. } => {
"no register profile for the claimant's network"
}
_ => "register profile not derivable",
}))
})?
.storage_set_id;
})?;
// The set id is a function of `(member_id, register_incarnation_id)`
// pairs, so it is re-derived from what the resolver offers and refused
// unless the membership is exactly this network's. The resolver supplies
// candidates; this is where they stop being taken on trust.
let candidate = resolver
.root_register_candidate_set(network_id)
.map_err(|_| {
EconomicValidationError::Provenance(ProvenanceError::FaucetWinnerInvalid(
"the network's register set could not be resolved",
))
})?;
let canonical_set = profile.derive_set_id(&candidate).map_err(|_| {
EconomicValidationError::Provenance(ProvenanceError::FaucetWinnerInvalid(
"resolved register membership is not the network's canonical membership",
))
})?;
let ctx = ProvenanceContext {
genesis,
device_id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,12 @@ pub trait PeerEvidenceFetcher {
storage_set: &crate::ccb::StorageSetMembers,
quorum: u32,
) -> crate::economic::cell_observation::CellObservation;
/// The network's root-register set as the local catalog resolves it —
/// CANDIDATE entries the caller must re-derive and check, never authority.
fn root_register_candidate_set(
&self,
network_id: &[u8],
) -> Result<crate::ccb::StorageSetMembers, PeerLineageFailure>;
/// Exact immutable bytes at `addr` under `namespace`.
fn immutable(
&self,
Expand Down Expand Up @@ -171,6 +177,13 @@ impl ProvenanceResolver for WalkingResolver<'_> {
.settlement_slot_observation(vault_id, parent_sequence, storage_set, quorum)
}

fn root_register_candidate_set(
&self,
network_id: &[u8],
) -> Result<crate::ccb::StorageSetMembers, PeerLineageFailure> {
self.fetcher.root_register_candidate_set(network_id)
}

fn immutable_evidence(
&self,
namespace: TaggedHashDomain<'static>,
Expand Down Expand Up @@ -364,7 +377,16 @@ fn walk_positions(
// canonical set — never sourced from transfer metadata or contacts.
let profile = resolve_for_trader(&facts.network_id, expected_network_id)
.map_err(|e| invalid(format!("peer network refused: {e}")))?;
if body.root_register_storage_set_id != profile.storage_set_id {
// 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
// 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)
.map_err(|e| invalid(format!("peer register set refused: {e}")))?;
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 \
committed network",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,21 @@ pub trait ProvenanceResolver {
quorum: u32,
) -> crate::economic::cell_observation::CellObservation;

/// The network's root-register set as the local catalog resolves it.
///
/// CANDIDATE entries, never authority. A set id is now a function of
/// `(member_id, register_incarnation_id)` pairs, and an incarnation is a
/// runtime fact a member generates once — so the pairs cannot be a
/// constant and must come from somewhere. This is that somewhere, and it
/// is deliberately named a candidate: the caller re-derives the id from
/// these entries and refuses any membership that is not the network's
/// canonical list, so a catalog that offers the wrong set is caught
/// rather than believed.
fn root_register_candidate_set(
&self,
network_id: &[u8],
) -> Result<crate::ccb::StorageSetMembers, PeerLineageFailure>;

/// Exact immutable bytes at `addr` under `namespace` — evidence the
/// verifier itself checks (the resolver supplies bytes, never verdicts,
/// so the acyclicity and verification stay in the verifier's hands).
Expand Down
Loading
Loading