open-standards/smart-byte/smart-byte-rs/crates/smart-byte-lockstep/src/lib.rs
Transaction Science a437feb8ab Parallel ingest: 9 issues' worth of work across openpay / smart-byte / eoc
openpay/
  compliance/                     PCI-DSS readiness package (scope-map,
                                  pci-zero-architecture, hsm-kms-guidance,
                                  qsa-checklist), citing PCI-DSS v4.0.1
                                  requirements. (#1)
  examples/pci-zero/              Runnable example demonstrating a deployment
                                  where merchant code never touches a PAN. (#1)
  crates/op-core/                 Tokenized<Card> typestate + NetworkToken
                                  + CardNetwork + Cryptogram. (#2)
  crates/op-rails-card/network_token/
                                  NetworkTokenProvider trait + VTS adapter
                                  + MDES adapter, with deterministic stubs
                                  behind a 'live' feature flag. (#2)
  crates/op-driver-sdk/conformance/network_token.rs
                                  Conformance harness for third-party
                                  NetworkTokenProvider implementations. (#2)
  crates/op-cli/src/ledger/       'openpay ledger as-of' subcommand for
                                  bi-temporal queries. (#3)
  crates/op-orchestrator/src/telemetry.rs
                                  OpenTelemetry span emission per
                                  Payment<S> transition with semantic-
                                  conventions tags. (#3)
  docs/observability/             Grafana dashboard JSON + wiring guide
                                  for the bi-temporal ledger lens. (#3)

smart-byte/
  smart-byte-rs/                  Rust reference implementation: envelope
                                  encode/decode (canonical CBOR + BLAKE3
                                  content addressing), Ed25519 sign +
                                  verify, lockstep BFT commit MVP, Iroh
                                  transport scaffold, CLI. (#4)
  spec/identity_and_key_rotation.md
                                  Adopts KERI SAIDs + key-event log
                                  format; explicitly does not adopt ACDC
                                  chaining wholesale. (#5)
  conformance/v1/                 21 test vectors covering canonicalisation,
                                  SAID computation, signature verification,
                                  lockstep commit, gossip propagation.
                                  CI gate enforces spec/vector coupling. (#6)

eoc/
  eoc-rs/                         Rust + WASM reference implementation:
                                  4-stage cascade (cache -> kv -> graph ->
                                  neural), joule counters via RAPL / NVML /
                                  powermetrics, CLI, wasm-bindgen surface.
                                  18 tests pass. (#7)
  eoc-bench/                      Benchmark harness, MT-Bench and Router
                                  case sets, 'joules per MT-Bench point'
                                  metric, 'eoc-bench run | compare' CLI. (#8)
  standards/sci-for-ai-submission.md
                                  Submission proposal mapping EOC's
                                  receipt schema to GSF SCI-for-AI. (#9)
  cookbook/                       HuggingFace-style cookbook (markdown +
                                  runnable Python) demonstrating EOC's
                                  cascade with HF AI Energy Score. (#9)
  docs/bench-v0.md                Reproducibility doc for eoc-bench.

Workspace state:
- openpay clippy -- -D warnings: clean across all touched crates
- smart-byte-rs cargo test --workspace + clippy: clean
- eoc-rs cargo test --workspace + clippy: clean (18 tests)
- eoc-bench cargo check --workspace + clippy + 15 tests: clean

Rust 1.95.0 edition 2024 pinned at the open-standards root via
rust-toolchain.toml; every workspace inherits.

Closes #1 #2 #3 #4 #5 #6 #7 #8 #9.
2026-05-23 13:24:05 -04:00

231 lines
7.5 KiB
Rust

//! Deterministic lockstep BFT cluster simulation (MVP).
//!
//! Every [`Node`] in a [`Cluster`] runs the *same* state machine: a
//! BLAKE3 rolling hash that absorbs the canonical-CBOR encoding of each
//! ordered transition. After each [`Frame`] every honest node will
//! independently arrive at the same `state_hash`. A frame commits when
//! a BFT supermajority (`> 2/3` of nodes) vote for the same hash.
//!
//! Faulty nodes are modeled as nodes that *flip a bit* in their state
//! before voting; the cluster still commits if at least `floor(2n/3)+1`
//! honest nodes agree.
use std::collections::{HashMap, HashSet};
use serde::{Deserialize, Serialize};
use smart_byte_core::{Blake3Hash, Said, ownership::Transition};
/// Errors during a frame step.
#[derive(Debug, thiserror::Error)]
pub enum LockstepError {
#[error("cluster is empty")]
EmptyCluster,
#[error("no supermajority on frame {frame}: best vote {best}/{total}")]
NoSupermajority {
frame: u64,
best: usize,
total: usize,
},
}
/// A single ordered batch of state transitions.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Frame {
pub frame_number: u64,
pub ordered_transitions: Vec<Transition>,
}
/// A simulated node in the cluster.
#[derive(Clone, Debug)]
pub struct Node {
/// Stable identity.
pub id: Said,
/// Rolling state hash. Each frame absorbs into this.
pub state: Blake3Hash,
/// If true, this node introduces a deterministic per-frame bit-flip
/// before voting. Used to model Byzantine misbehavior.
pub faulty: bool,
}
impl Node {
/// Construct a fresh node with all-zero state.
pub fn new(id: Said) -> Self {
Self {
id,
state: [0u8; 32],
faulty: false,
}
}
/// Mark this node as faulty.
pub fn with_faulty(mut self, faulty: bool) -> Self {
self.faulty = faulty;
self
}
/// Compute this node's vote for the given frame, advancing its
/// internal state to the post-frame hash. Faulty nodes flip the
/// low bit of the announced vote (but still update their honest
/// state internally; in a real cluster the state divergence would
/// be the observable signal).
fn step(&mut self, frame: &Frame) -> Blake3Hash {
let mut hasher = blake3::Hasher::new();
hasher.update(&self.state);
hasher.update(&frame.frame_number.to_le_bytes());
for t in &frame.ordered_transitions {
let bytes = serde_cbor::to_vec(t).expect("Transition CBOR is infallible");
hasher.update(&bytes);
}
let honest = *hasher.finalize().as_bytes();
self.state = honest;
if self.faulty {
let mut bad = honest;
bad[0] ^= 0x01;
bad
} else {
honest
}
}
}
/// The committed result of one lockstep frame.
#[derive(Clone, Debug)]
pub struct FrameCommit {
pub frame: u64,
pub state_hash: Blake3Hash,
pub committed_by: HashSet<Said>,
}
/// A federated cluster of [`Node`]s running deterministic lockstep.
pub struct Cluster {
pub nodes: Vec<Node>,
pub frame: u64,
}
impl Cluster {
/// Build a cluster of `n` honest nodes.
pub fn new_honest(n: usize) -> Self {
let nodes = (0..n)
.map(|i| {
let id = Said::hash(format!("node-{i}").as_bytes());
Node::new(id)
})
.collect();
Self { nodes, frame: 0 }
}
/// Build a cluster of `n` nodes where `faulty_count` of them are
/// Byzantine. The faulty nodes are the *last* `faulty_count`
/// entries; honest nodes occupy indices `0..n-faulty_count`.
pub fn new_with_faulty(n: usize, faulty_count: usize) -> Self {
let mut c = Self::new_honest(n);
for i in (n - faulty_count)..n {
c.nodes[i].faulty = true;
}
c
}
/// Run one lockstep frame. Every node steps; votes are tallied;
/// the modal hash wins iff it has a `> 2/3` supermajority.
pub fn step(&mut self, transitions: Vec<Transition>) -> Result<FrameCommit, LockstepError> {
if self.nodes.is_empty() {
return Err(LockstepError::EmptyCluster);
}
let frame = Frame {
frame_number: self.frame,
ordered_transitions: transitions,
};
let total = self.nodes.len();
let mut tally: HashMap<Blake3Hash, HashSet<Said>> = HashMap::new();
for node in &mut self.nodes {
let vote = node.step(&frame);
tally.entry(vote).or_default().insert(node.id);
}
// Pick the hash with the largest backing set.
let (best_hash, voters) = tally
.into_iter()
.max_by_key(|(_, voters)| voters.len())
.expect("non-empty cluster has at least one vote");
// Strict > 2/3 (BFT supermajority).
let needed = (2 * total) / 3 + 1;
if voters.len() < needed {
return Err(LockstepError::NoSupermajority {
frame: frame.frame_number,
best: voters.len(),
total,
});
}
let commit = FrameCommit {
frame: frame.frame_number,
state_hash: best_hash,
committed_by: voters,
};
self.frame += 1;
Ok(commit)
}
}
/// Helper: synthesize a deterministic sequence of transitions for a
/// frame index. Used by tests and the CLI demo so that the demo output
/// is reproducible.
pub fn synthetic_transitions(frame_index: u64, count: usize) -> Vec<Transition> {
(0..count)
.map(|i| {
let from = Said::hash(format!("from-{frame_index}-{i}").as_bytes());
let to = Said::hash(format!("to-{frame_index}-{i}").as_bytes());
let prior = if i == 0 {
None
} else {
Some(Said::hash(format!("prior-{frame_index}-{i}").as_bytes()).0)
};
Transition::unsigned(from, to, prior)
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn four_node_honest_cluster_converges() {
let mut cluster = Cluster::new_honest(4);
let mut commits = Vec::new();
for f in 0..10 {
let commit = cluster.step(synthetic_transitions(f, 3)).unwrap();
assert_eq!(commit.committed_by.len(), 4);
commits.push(commit);
}
// All ten frames should have committed under unanimous votes.
assert_eq!(commits.len(), 10);
}
#[test]
fn one_faulty_node_still_commits() {
let mut cluster = Cluster::new_with_faulty(4, 1);
// 3-of-4 honest nodes is exactly the floor(2*4/3)+1 = 3 threshold.
for f in 0..10 {
let commit = cluster.step(synthetic_transitions(f, 3)).unwrap();
assert_eq!(commit.committed_by.len(), 3);
}
}
#[test]
fn two_faulty_nodes_in_four_breaks_supermajority() {
// 2 honest, 2 faulty: best is 2/4 which is below the >2/3 = 3/4 threshold.
let mut cluster = Cluster::new_with_faulty(4, 2);
let err = cluster.step(synthetic_transitions(0, 3)).unwrap_err();
assert!(matches!(err, LockstepError::NoSupermajority { .. }));
}
#[test]
fn determinism_across_clusters() {
let mut a = Cluster::new_honest(4);
let mut b = Cluster::new_honest(4);
for f in 0..5 {
let ca = a.step(synthetic_transitions(f, 2)).unwrap();
let cb = b.step(synthetic_transitions(f, 2)).unwrap();
assert_eq!(ca.state_hash, cb.state_hash);
}
}
}