Skip to content
Open
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
6 changes: 5 additions & 1 deletion consensus/src/merkle_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,14 +133,18 @@ pub fn compute_global_state_root(subnet_roots: &HashMap<SubnetId, MerkleHash>) -
return MerkleHash::zero();
}

let entries: Vec<SubnetStateEntry> = subnet_roots
let mut entries: Vec<SubnetStateEntry> = subnet_roots
.iter()
.map(|(subnet_id, root)| {
// Convert SubnetId to [u8; 32]
let subnet_bytes = subnet_id.to_bytes();
SubnetStateEntry::new(subnet_bytes, *root)
})
.collect();
// Sort by subnet_id bytes for deterministic ordering across all validators.
// HashMap iteration order is non-deterministic, so without sorting different
// validators would compute different global_state_root from identical inputs.
entries.sort_by(|a, b| a.subnet_id.cmp(&b.subnet_id));

let tree = SubnetAggregationTree::build(entries);
tree.root()
Expand Down
17 changes: 12 additions & 5 deletions types/src/consensus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ impl Anchor {
event_ids: &[EventId],
vlc_snapshot: &VLCSnapshot,
state_root: &str,
timestamp: u64,
_timestamp: u64,
) -> AnchorId {
let mut hasher = blake3::Hasher::new();
hasher.update(b"SETU_ANCHOR_ID:");
Expand All @@ -97,7 +97,9 @@ impl Anchor {
}
hasher.update(&vlc_snapshot.logical_time.to_le_bytes());
hasher.update(state_root.as_bytes());
hasher.update(&timestamp.to_le_bytes());
// NOTE: timestamp is intentionally excluded from the ID computation.
// Using SystemTime::now() made anchor IDs non-deterministic across validators,
// breaking consensus. The event_ids + vlc + state_root already ensure uniqueness.
hex::encode(hasher.finalize().as_bytes())
}

Expand Down Expand Up @@ -308,14 +310,16 @@ impl ConsensusFrame {
}
}

fn compute_id(round: u64, anchor: &Anchor, proposer: &str, timestamp: u64) -> CFId {
fn compute_id(round: u64, anchor: &Anchor, proposer: &str, _timestamp: u64) -> CFId {
let mut hasher = blake3::Hasher::new();
// V2 domain separator: includes round to bind proposer<->round (PR-4).
hasher.update(b"SETU_CF_ID_V2:");
hasher.update(&round.to_le_bytes());
hasher.update(anchor.id.as_bytes());
hasher.update(proposer.as_bytes());
hasher.update(&timestamp.to_le_bytes());
// NOTE: timestamp is intentionally excluded from the ID computation.
// Using SystemTime::now() made CF IDs non-deterministic across validators,
// breaking consensus. The round + anchor + proposer already ensure uniqueness.
hex::encode(hasher.finalize().as_bytes())
}

Expand All @@ -332,7 +336,10 @@ impl ConsensusFrame {
}

pub fn check_quorum(&self, total_validators: usize) -> bool {
let threshold = (total_validators * 2) / 3 + 1;
// BFT quorum: ceil(2n/3). For n=3 → 2, n=4 → 3, n=6 → 4.
// Previous formula `(n * 2) / 3 + 1` required unanimity for n=3 (3/3),
// which is incorrect for BFT consensus.
let threshold = (total_validators * 2 + 2) / 3;
self.approve_count() >= threshold
}

Expand Down
17 changes: 10 additions & 7 deletions types/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -555,10 +555,10 @@ impl Event {
///
/// The validator builds the genesis event deterministically
/// (`setu-validator/src/main.rs`): empty `parent_ids`, fixed creator
/// `"genesis"`, `timestamp = 0`, and `logical_time = 0`. `compute_id` hashes
/// exactly those four inputs, so the genesis event id is a fixed,
/// chain-independent constant
/// (`691c8dd61cdc0391ae5414ce9b6ac9be3ef205ba61daa9cb58dc0b3989e0dd6d`).
/// `"genesis", `timestamp = 0`, and `logical_time = 0`. `compute_id` hashes
/// exactly those three inputs (timestamp excluded for determinism), so the
/// genesis event id is a fixed, chain-independent constant
/// (`1efb63d540602ba4b6487dd7da94ff3bdb31fd0c24e21644f9c333e550d5e8cd`).
///
/// Task preparation uses this to drop the genesis parent edge from a
/// never-moved coin before `compute_id`, preventing `ParentTooOld` once the
Expand Down Expand Up @@ -750,7 +750,7 @@ impl Event {
parent_ids: &[EventId],
vlc_snapshot: &VLCSnapshot,
creator: &str,
timestamp: u64,
_timestamp: u64,
) -> EventId {
let mut hasher = blake3::Hasher::new();
hasher.update(b"SETU_EVENT_ID:");
Expand All @@ -759,7 +759,10 @@ impl Event {
}
hasher.update(&vlc_snapshot.logical_time.to_le_bytes());
hasher.update(creator.as_bytes());
hasher.update(&timestamp.to_le_bytes());
// NOTE: timestamp is intentionally excluded from the ID computation.
// Using SystemTime::now() made event IDs non-deterministic across validators,
// breaking consensus. The VLC logical_time already provides causal ordering,
// and creator + parent_ids ensure uniqueness.
hex::encode(hasher.finalize().as_bytes())
}

Expand Down Expand Up @@ -1027,7 +1030,7 @@ mod tests {
#[test]
fn test_genesis_event_id_is_canonical_constant() {
const EXPECTED: &str =
"691c8dd61cdc0391ae5414ce9b6ac9be3ef205ba61daa9cb58dc0b3989e0dd6d";
"1efb63d540602ba4b6487dd7da94ff3bdb31fd0c24e21644f9c333e550d5e8cd";
assert_eq!(Event::genesis_event_id(), EXPECTED);

// Mirror setu-validator/src/main.rs genesis construction.
Expand Down
6 changes: 3 additions & 3 deletions types/src/object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -420,9 +420,9 @@ impl<T: Serialize + Clone> Object<T> {
hasher.update(b"SETU_OBJ_DIGEST:");
hasher.update(self.metadata.id.as_bytes());
hasher.update(&self.metadata.version.to_le_bytes());
if let Ok(data_bytes) = bcs::to_bytes(&self.data) {
hasher.update(&data_bytes);
}
let data_bytes = bcs::to_bytes(&self.data)
.expect("BCS serialization failed for object data — this indicates a type-level bug");
hasher.update(&data_bytes);
self.metadata.digest = ObjectDigest::new(*hasher.finalize().as_bytes());
}

Expand Down