open-standards/smart-byte/smart-byte-rs/crates/smart-byte-conformance/src/verify.rs
Transaction Science c4d3a091f3 smart-byte: the cluster state machine, and its conformance vectors
smart-byte-state (new):
- the state and its hash;
- inputs;
- the frame transition;
- checkpoint-and-log persistence. Checkpoints are written atomically, and
  replay refuses a gap or a wrong hash.
- a deterministic demo workload.

smart-byte-lockstep: the cluster runs these state machines in place of the
rolling hash that stood in for the state.

smart-byte-keri-witness: exposes a controller's state and resumes a replay
from it, so the cluster extends a log one event at a time.

The CLI's `cluster simulate` runs real mints and transfers through the
supermajority commit.

smart-byte-conformance: state_vectors.json, 12 vectors. Each is a genesis
state and frames of inputs, with every input's result and every frame's
state and state hash. The founding frame uses kel-001 to kel-003 and
env-001's mint. Planted defects in the state vectors are caught on their
own vectors.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-01 06:19:03 -04:00

865 lines
31 KiB
Rust

//! Check a vector pack on disk against the reference implementation.
//!
//! This reads only the published JSON: every expected value is recomputed
//! from the vector's own inputs, and every negative vector must be refused
//! with the error class it names. It does not consult the generator, so it
//! checks a pack whoever produced it.
//!
//! It also checks that the pack is complete, against texts that live outside
//! the pack: every vector the identity spec cites must be present, and every
//! error class the conformance README lists must be exercised (and no other).
//! A pack cannot pass by losing vectors.
use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::path::{Path, PathBuf};
use serde_json::Value as S;
use smart_byte_core::canonical::{self, Value};
use smart_byte_core::inbox::{Delivery, Inbox};
use smart_byte_core::said::{self, SAID_PLACEHOLDER};
use smart_byte_core::sign::{self, Signer as _, SigningKey};
use smart_byte_core::{Envelope, KeyBook, Said};
use smart_byte_keri_witness::{KeyEvent, LogVerifier, SignedEvent, VerificationReport};
use smart_byte_lockstep::{node_view, supermajority_threshold, tally};
use smart_byte_state::State;
use crate::json::{from_json, parse_strict};
use crate::pack::FILES;
/// One vector's result.
#[derive(Debug, Clone)]
pub struct Check {
pub file: String,
pub vector: String,
pub result: Result<(), String>,
}
/// Every check a pack verification made.
#[derive(Debug, Default)]
pub struct Report {
pub checks: Vec<Check>,
}
impl Report {
pub fn failures(&self) -> Vec<&Check> {
self.checks.iter().filter(|c| c.result.is_err()).collect()
}
pub fn passed(&self) -> bool {
!self.checks.is_empty() && self.failures().is_empty()
}
fn record(&mut self, file: &str, vector: &str, result: Result<(), String>) {
self.checks.push(Check {
file: file.to_owned(),
vector: vector.to_owned(),
result,
});
}
}
type R = Result<(), String>;
fn field<'a>(v: &'a S, key: &str) -> Result<&'a S, String> {
v.get(key).ok_or_else(|| format!("missing `{key}`"))
}
fn text<'a>(v: &'a S, key: &str) -> Result<&'a str, String> {
field(v, key)?
.as_str()
.ok_or_else(|| format!("`{key}` is not a string"))
}
fn hex_field(v: &S, key: &str) -> Result<Vec<u8>, String> {
hex::decode(text(v, key)?).map_err(|e| format!("`{key}` is not hex: {e}"))
}
fn hash32(v: &S, what: &str) -> Result<[u8; 32], String> {
v.as_str()
.and_then(|s| hex::decode(s).ok())
.and_then(|b| b.try_into().ok())
.ok_or_else(|| format!("{what} is not 32 bytes of hex"))
}
fn same(what: &str, computed: impl PartialEq<String> + std::fmt::Display, published: &str) -> R {
if computed == published.to_owned() {
Ok(())
} else {
Err(format!(
"{what}: published {published}, reference computes {computed}"
))
}
}
/// The texts the completeness checks are held to: the identity spec and the
/// conformance README in this repository.
fn repo_text(relative: &str) -> Result<String, String> {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(relative);
std::fs::read_to_string(&path).map_err(|e| format!("read {}: {e}", path.display()))
}
/// The SAID vector prefixes the identity spec cites (`said-006`, and every
/// member of a range written `said-001` to `said-005`).
pub fn spec_cited_said_vectors() -> Result<BTreeSet<String>, String> {
let spec = repo_text("../../../spec/identity_and_key_rotation.md")?;
let mut cited = BTreeSet::new();
let words: Vec<&str> = spec
.split(|c: char| !(c.is_ascii_alphanumeric() || c == '-'))
.filter(|w| !w.is_empty())
.collect();
let number = |w: &str| w.strip_prefix("said-").and_then(|n| n.parse::<u32>().ok());
for (i, w) in words.iter().enumerate() {
let Some(n) = number(w) else { continue };
cited.insert(format!("said-{n:03}"));
if words.get(i + 1) == Some(&"to")
&& let Some(end) = words.get(i + 2).and_then(|w| number(w))
{
for k in n..=end {
cited.insert(format!("said-{k:03}"));
}
}
}
if cited.is_empty() {
return Err("the identity spec cites no SAID vector".into());
}
Ok(cited)
}
/// The error classes the conformance README's table lists.
pub fn readme_error_classes() -> Result<BTreeSet<String>, String> {
let readme = repo_text("../../../conformance/README.md")?;
let section = readme
.split("### Error classes")
.nth(1)
.ok_or("the README has no error-class table")?;
let classes: BTreeSet<String> = section
.lines()
.skip_while(|l| !l.starts_with('|'))
.take_while(|l| l.starts_with('|'))
.filter_map(|l| l.split('`').nth(1).map(str::to_owned))
.collect();
if classes.is_empty() {
return Err("the README's error-class table is empty".into());
}
Ok(classes)
}
/// Check every file of the pack in `dir`.
pub fn verify_pack(dir: &Path) -> Report {
let mut report = Report::default();
let mut files: BTreeMap<&str, Vec<S>> = BTreeMap::new();
for file in FILES {
let loaded = std::fs::read(dir.join(file))
.map_err(|e| format!("read: {e}"))
.and_then(|b| parse_strict(&b).map_err(|e| format!("parse: {e}")))
.and_then(|j| match j {
S::Array(items) => Ok(items),
_ => Err("the top-level value must be an array".to_owned()),
});
match loaded {
Ok(items) => {
report.record(file, "(file)", structure(&items));
files.insert(file, items);
}
Err(e) => report.record(file, "(file)", Err(e)),
}
}
// Key-event logs first: the envelope vectors are checked against the key
// states the accepted logs establish.
let mut logs: BTreeMap<String, VerificationReport> = BTreeMap::new();
for v in files.get("kel_vectors.json").into_iter().flatten() {
let name = v.get("name").and_then(S::as_str).unwrap_or("(unnamed)");
let result = kel(v).map(|accepted| {
if let Some(r) = accepted {
logs.insert(name.to_owned(), r);
}
});
report.record("kel_vectors.json", name, result);
}
for (file, items) in &files {
for v in items {
let name = v.get("name").and_then(S::as_str).unwrap_or("(unnamed)");
let result = match *file {
"canonicalisation_vectors.json" => canonicalisation(v),
"said_vectors.json" => said_vector(v),
"signature_vectors.json" => signature(v),
"kel_vectors.json" => continue,
"envelope_vectors.json" => envelope(v, &logs),
"state_vectors.json" => state_vector(v),
"lockstep_vectors.json" => lockstep(v),
"gossip_vectors.json" => gossip(v),
_ => unreachable!("FILES lists exactly these"),
};
report.record(file, name, result);
}
}
report.record(
"(pack)",
"spec-cited vectors present",
cited_present(&files),
);
report.record(
"(pack)",
"README error classes exercised",
classes_exercised(&files),
);
report
}
/// Names and descriptions present, names unique, supersessions resolvable,
/// and at least one vector.
fn structure(items: &[S]) -> R {
if items.is_empty() {
return Err("the file holds no vectors".into());
}
let mut names = BTreeSet::new();
for v in items {
let name = text(v, "name")?;
text(v, "description").map_err(|e| format!("{name}: {e}"))?;
if !names.insert(name) {
return Err(format!("duplicate vector name {name}"));
}
}
for v in items {
if let Some(target) = v.get("superseded_by").and_then(S::as_str)
&& !names.contains(target)
{
return Err(format!(
"{} is superseded by {target}, which is not in the file",
v["name"]
));
}
}
Ok(())
}
fn cited_present(files: &BTreeMap<&str, Vec<S>>) -> R {
let names: Vec<&str> = files
.get("said_vectors.json")
.into_iter()
.flatten()
.filter_map(|v| v.get("name").and_then(S::as_str))
.collect();
let missing: Vec<String> = spec_cited_said_vectors()?
.into_iter()
.filter(|prefix| !names.iter().any(|n| n.starts_with(&format!("{prefix}-"))))
.collect();
if missing.is_empty() {
Ok(())
} else {
Err(format!(
"the identity spec cites vectors the pack lacks: {missing:?}"
))
}
}
fn classes_exercised(files: &BTreeMap<&str, Vec<S>>) -> R {
let mut used = BTreeSet::new();
let mut results = |holder: &S| {
for r in holder
.get("expected_results")
.and_then(S::as_array)
.into_iter()
.flatten()
{
if let Some(e) = r.get("error").and_then(S::as_str) {
used.insert(e.to_owned());
}
}
};
let mut errors = BTreeSet::new();
for v in files.values().flatten() {
if let Some(e) = v.get("error").and_then(S::as_str) {
errors.insert(e.to_owned());
}
results(v);
for frame in v.get("frames").and_then(S::as_array).into_iter().flatten() {
results(frame);
}
}
used.extend(errors);
let listed = readme_error_classes()?;
let unexercised: Vec<&String> = listed.difference(&used).collect();
let undocumented: Vec<&String> = used.difference(&listed).collect();
if unexercised.is_empty() && undocumented.is_empty() {
Ok(())
} else {
Err(format!(
"README classes no vector exercises: {unexercised:?}; classes the README does not list: {undocumented:?}"
))
}
}
fn refused_as(what: &str, class: Result<(), &'static str>, v: &S) -> R {
let want = text(v, "error")?;
match class {
Ok(()) => Err(format!("{what} accepted input it must refuse with {want}")),
Err(got) if got == want => Ok(()),
Err(got) => Err(format!("refused with {got}, but the vector says {want}")),
}
}
fn canonicalisation(v: &S) -> R {
if let Some(input) = v.get("input") {
let value = from_json(input)?;
let bytes = canonical::encode(&value).map_err(|e| e.to_string())?;
same(
"expected_cbor_hex",
hex::encode(&bytes),
text(v, "expected_cbor_hex")?,
)?;
let back = canonical::decode(&bytes)
.map_err(|e| format!("decoder refuses the encoder's output: {e}"))?;
if canonical::encode(&back).map_err(|e| e.to_string())? != bytes {
return Err("decode then encode does not reproduce the bytes".into());
}
Ok(())
} else {
let input = hex_field(v, "input_cbor_hex")?;
refused_as(
"the decoder",
canonical::decode(&input).map(drop).map_err(|e| e.class()),
v,
)
}
}
fn said_vector(v: &S) -> R {
let field_name = text(v, "said_field")?;
if let Some(input) = v.get("input") {
let value = from_json(input)?;
if value.get(field_name) != Some(&Value::text(SAID_PLACEHOLDER)) {
return Err(format!(
"`input` must hold the placeholder in `{field_name}` (identity spec §2.1 step 1)"
));
}
let d = said::derive(&value, field_name).map_err(|e| e.to_string())?;
same(
"canonical_cbor_hex",
hex::encode(&d.hashed),
text(v, "canonical_cbor_hex")?,
)?;
same(
"blake3_hex",
hex::encode(d.said.as_bytes()),
text(v, "blake3_hex")?,
)?;
same(
"expected_said",
d.said.to_said_text(),
text(v, "expected_said")?,
)?;
same(
"said_cbor_hex",
hex::encode(&d.encoded),
text(v, "said_cbor_hex")?,
)?;
let verified = said::verify(&hex_field(v, "said_cbor_hex")?, field_name)
.map_err(|e| format!("said_cbor_hex does not verify: {e}"))?;
same(
"verified SAID",
verified.to_said_text(),
text(v, "expected_said")?,
)
} else {
let input = hex_field(v, "input_cbor_hex")?;
refused_as(
"SAID verification",
said::verify(&input, field_name)
.map(drop)
.map_err(|e| e.class()),
v,
)?;
if let Some(received) = v.get("said_of_received_bytes").and_then(S::as_str) {
same(
"said_of_received_bytes",
said_of_received_bytes(&input, field_name)?,
received,
)?;
}
if let Some(reencoded) = v.get("said_after_reencoding").and_then(S::as_str) {
let value = lenient_decode(&input)?;
let d = said::derive(&value, field_name).map_err(|e| e.to_string())?;
same("said_after_reencoding", d.said.to_said_text(), reencoded)?;
}
Ok(())
}
}
/// The SAID a verifier that skips the canonical check would compute: the
/// received bytes with the stated SAID overwritten in place by the placeholder.
fn said_of_received_bytes(input: &[u8], field_name: &str) -> Result<String, String> {
let stated = match lenient_decode(input)?.get(field_name) {
Some(Value::Text(t)) if t.len() == SAID_PLACEHOLDER.len() => t.clone(),
_ => {
return Err(format!(
"no {}-character `{field_name}` to substitute",
SAID_PLACEHOLDER.len()
));
}
};
let at = input
.windows(stated.len())
.position(|w| w == stated.as_bytes())
.ok_or("the stated SAID is not in the bytes")?;
let mut bytes = input.to_vec();
bytes[at..at + stated.len()].copy_from_slice(SAID_PLACEHOLDER.as_bytes());
Ok(Said::hash(&bytes).to_said_text())
}
/// A decoder that accepts over-long heads and maps in any order — only so the
/// verifier can show what a lenient implementation would compute. Nothing
/// that decides acceptance uses it.
fn lenient_decode(input: &[u8]) -> Result<Value, String> {
fn item(b: &[u8], pos: &mut usize, depth: usize) -> Result<Value, String> {
if depth > canonical::MAX_DEPTH {
return Err("too deep".into());
}
let initial = *b.get(*pos).ok_or("truncated")?;
*pos += 1;
let (major, ai) = (initial >> 5, initial & 0x1f);
if major == 7 {
return match ai {
20 => Ok(Value::Bool(false)),
21 => Ok(Value::Bool(true)),
22 => Ok(Value::Null),
_ => Err(format!(
"lenient decoder: unsupported simple/float {initial:#x}"
)),
};
}
let width = match ai {
0..=23 => 0,
24 => 1,
25 => 2,
26 => 4,
27 => 8,
_ => return Err(format!("lenient decoder: unsupported head {initial:#x}")),
};
let mut arg = if width == 0 { u64::from(ai) } else { 0 };
for _ in 0..width {
arg = (arg << 8) | u64::from(*b.get(*pos).ok_or("truncated")?);
*pos += 1;
}
let mut take = |n: u64| -> Result<Vec<u8>, String> {
let n = usize::try_from(n).map_err(|e| e.to_string())?;
let end = pos.checked_add(n).ok_or("truncated")?;
let s = b.get(*pos..end).ok_or("truncated")?.to_vec();
*pos = end;
Ok(s)
};
Ok(match major {
0 => Value::Unsigned(arg),
1 => Value::Negative(arg),
2 => Value::Bytes(take(arg)?),
3 => Value::Text(String::from_utf8(take(arg)?).map_err(|e| e.to_string())?),
4 => {
let mut items = Vec::new();
for _ in 0..arg {
items.push(item(b, pos, depth + 1)?);
}
Value::Array(items)
}
5 => {
let mut entries = Vec::new();
for _ in 0..arg {
let k = item(b, pos, depth + 1)?;
entries.push((k, item(b, pos, depth + 1)?));
}
Value::Map(entries)
}
_ => return Err(format!("lenient decoder: unsupported major type {major}")),
})
}
let mut pos = 0;
let v = item(input, &mut pos, 0)?;
if pos != input.len() {
return Err("trailing bytes".into());
}
Ok(v)
}
fn signature(v: &S) -> R {
let message = hex_field(v, "message_hex")?;
let key = match field(v, "signing_key_seed")? {
S::Null => None,
seed => {
let seed: [u8; 32] = hash32(seed, "signing_key_seed")?;
let key = SigningKey::from_bytes(&seed);
same(
"verifying_key",
hex::encode(key.verifying_key().to_bytes()),
text(v, "verifying_key")?,
)?;
Some(key)
}
};
let sig = hex_field(v, "signature")?;
let result = sign::verify(&hex_field(v, "verifying_key")?, &message, &sig);
match field(v, "expected_verify")?.as_bool() {
Some(true) => {
result.map_err(|e| format!("a positive vector fails to verify: {e}"))?;
if !field(v, "error")?.is_null() {
return Err("a positive vector names an error".into());
}
// Ed25519 is deterministic: the published signature is THE
// signature, so a positive vector must carry its seed.
let key = key.ok_or("a positive vector must carry its signing seed")?;
same(
"signature",
hex::encode(key.sign(&message).to_bytes()),
text(v, "signature")?,
)
}
Some(false) => refused_as("signature verification", result.map_err(|e| e.class()), v),
None => Err("expected_verify is not a boolean".into()),
}
}
/// A vector's typed-JSON form of an object, when it gives one, must be the
/// object its bytes encode.
fn typed_matches_bytes(v: &S, typed: &str, bytes: &[u8]) -> R {
if let Some(j) = v.get(typed) {
let encoded = canonical::encode(&from_json(j)?).map_err(|e| e.to_string())?;
if encoded != bytes {
return Err(format!("`{typed}` does not encode to the vector's bytes"));
}
}
Ok(())
}
/// Check one key-event-log vector; an accepted log's report is returned for
/// the envelope vectors to draw key states from.
fn kel(v: &S) -> Result<Option<VerificationReport>, String> {
let events = field(v, "events")?
.as_array()
.ok_or("events is not an array")?;
let mut log = Vec::new();
for (i, e) in events.iter().enumerate() {
let bytes = hex_field(e, "cbor_hex")?;
typed_matches_bytes(e, "event", &bytes).map_err(|m| format!("event {i}: {m}"))?;
let mut signatures = Vec::new();
for s in field(e, "signatures")?
.as_array()
.ok_or("signatures is not an array")?
{
let index = field(s, "index")?
.as_u64()
.and_then(|n| u32::try_from(n).ok())
.ok_or("a signature index is not an integer")?;
let signature = hex_field(s, "signature")?;
// A signature with a seed is the one that seed's key makes:
// Ed25519 is deterministic.
if let S::String(_) = field(s, "seed")? {
let key = SigningKey::from_bytes(&hash32(&s["seed"], "seed")?);
let made = [&[0x01][..], &key.sign(&bytes).to_bytes()].concat();
if made != signature {
return Err(format!(
"event {i}: a signature is not the one its seed makes"
));
}
}
signatures.push((index, signature));
}
log.push(SignedEvent { bytes, signatures });
}
let result = LogVerifier::new().verify_encoded(&log, &HashMap::new());
match (v.get("expected"), v.get("error")) {
(Some(expected), None) => {
let report = result.map_err(|r| {
format!(
"refused at event {} with {}: {}",
r.index,
r.error.class(),
r.error
)
})?;
let saids: Vec<String> = log
.iter()
.map(|e| KeyEvent::from_cbor(&e.bytes).map(|ev| ev.said().to_said_text()))
.collect::<Result<_, _>>()
.map_err(|e| e.to_string())?;
let computed = serde_json::json!({
"controller": report.controller.to_text(),
"event_saids": saids,
"establishments": report.establishments.iter().map(|e| serde_json::json!({
"sequence": e.sequence,
"keys": e.keys.iter().map(|k| hex::encode(&k.0)).collect::<Vec<_>>(),
"threshold": e.threshold.0,
})).collect::<Vec<_>>(),
"next_key_digests": report.next_keys.iter().map(Said::to_said_text).collect::<Vec<_>>(),
"next_threshold": report.next_threshold.0,
});
if *expected != computed {
return Err(format!("expected: reference computes {computed}"));
}
Ok(Some(report))
}
(None, Some(_)) => {
let at = field(v, "error_at")?
.as_u64()
.ok_or("error_at is not an integer")?;
match result {
Ok(_) => Err(format!(
"the log is accepted, but the vector says {}",
text(v, "error")?
)),
Err(r) => {
refused_as("the log verifier", Err(r.error.class()), v)?;
if r.index as u64 != at {
return Err(format!(
"refused at event {}, but the vector says {at}",
r.index
));
}
Ok(None)
}
}
}
_ => Err("a vector has exactly one of `expected` and `error`".into()),
}
}
fn envelope(v: &S, logs: &BTreeMap<String, VerificationReport>) -> R {
let mut keys = KeyBook::new();
for name in field(v, "key_event_logs")?
.as_array()
.ok_or("key_event_logs is not an array")?
{
let name = name
.as_str()
.ok_or("a key_event_logs entry is not a string")?;
logs.get(name)
.ok_or_else(|| format!("{name} is not an accepted log of kel_vectors.json"))?
.add_to(&mut keys);
}
let bytes = hex_field(v, "cbor_hex")?;
typed_matches_bytes(v, "envelope", &bytes)?;
let result =
Envelope::from_cbor(&bytes).and_then(|e| e.verify(&keys).map(|verified| (e, verified)));
match (v.get("expected"), v.get("error")) {
(Some(expected), None) => {
let (e, verified) = result.map_err(|e| format!("refused with {}: {e}", e.class()))?;
let computed = serde_json::json!({
"identity": verified.identity.to_said_text(),
"snapshot": e.said().to_said_text(),
"issuer": verified.issuer.to_text(),
"owner": verified.owner.to_text(),
"history_hash": hex::encode(verified.history_hash),
"transfers": e.ownership_chain.len(),
});
if *expected != computed {
return Err(format!("expected: reference computes {computed}"));
}
Ok(())
}
(None, Some(_)) => refused_as(
"envelope verification",
result.map(drop).map_err(|e| e.class()),
v,
),
_ => Err("a vector has exactly one of `expected` and `error`".into()),
}
}
fn state_vector(v: &S) -> R {
let genesis = field(v, "genesis")?;
let bytes = hex_field(genesis, "cbor_hex")?;
typed_matches_bytes(genesis, "state", &bytes)?;
let mut state = State::from_cbor(&bytes).map_err(|e| format!("genesis: {e}"))?;
if state.frame != 0 {
return Err("a genesis state is at frame 0".into());
}
same(
"genesis state_hash",
hex::encode(state.hash()),
text(genesis, "state_hash")?,
)?;
for (i, frame) in field(v, "frames")?
.as_array()
.ok_or("frames is not an array")?
.iter()
.enumerate()
{
let at = |m: String| format!("frame {i}: {m}");
if field(frame, "frame")?.as_u64() != Some(state.frame) {
return Err(at(format!(
"the vector's frame number is not the state's, {}",
state.frame
)));
}
let mut inputs = Vec::new();
for input in field(frame, "inputs")?
.as_array()
.ok_or("inputs is not an array")?
{
let bytes = hex_field(input, "cbor_hex")?;
typed_matches_bytes(input, "input", &bytes).map_err(at)?;
inputs.push(bytes);
}
let report = state.apply_frame(&inputs);
let computed: Vec<S> = report
.results
.iter()
.map(|r| {
let mut j = serde_json::json!({
"input": hex::encode(r.input),
"class": r.class.map(|c| c.as_str()),
});
match &r.outcome {
Ok(()) => j["outcome"] = "applied".into(),
Err(e) => {
j["outcome"] = "dropped".into();
j["error"] = e.class().into();
}
}
j
})
.collect();
if field(frame, "expected_results")? != &S::Array(computed.clone()) {
return Err(at(format!(
"expected_results: reference computes {}",
S::Array(computed)
)));
}
let after = state.to_cbor();
typed_matches_bytes(frame, "expected_state", &after).map_err(at)?;
same(
"expected_state_cbor_hex",
hex::encode(&after),
text(frame, "expected_state_cbor_hex")?,
)
.map_err(at)?;
same(
"expected_state_hash",
hex::encode(state.hash()),
text(frame, "expected_state_hash")?,
)
.map_err(at)?;
}
Ok(())
}
fn lockstep(v: &S) -> R {
let n = field(v, "node_count")?
.as_u64()
.ok_or("node_count is not an integer")? as usize;
let frame = field(v, "frame_number")?
.as_u64()
.ok_or("frame_number is not an integer")?;
let reported_map = field(v, "node_reported_hashes")?
.as_object()
.ok_or("node_reported_hashes is not an object")?;
let computed_map = field(v, "node_computed_hashes")?
.as_object()
.ok_or("node_computed_hashes is not an object")?;
if reported_map.keys().ne(computed_map.keys()) {
return Err("reported and computed hashes must name the same nodes".into());
}
let mut reported = Vec::new();
for (node, h) in reported_map {
reported.push((node.clone(), hash32(h, node)?));
}
let public = tally(n, &reported).map_err(|e| e.to_string())?;
same(
"expected_threshold",
supermajority_threshold(n).to_string(),
&field(v, "expected_threshold")?.to_string(),
)?;
same(
"expected_commit",
public.committed().to_string(),
&field(v, "expected_commit")?.to_string(),
)?;
let state = public.supermajority_hash.map(hex::encode);
if field(v, "expected_state_hash")?.as_str() != state.as_deref() {
return Err(format!("expected_state_hash: reference computes {state:?}"));
}
let outcomes = field(v, "expected_outcomes")?
.as_object()
.ok_or("expected_outcomes is not an object")?;
let logs = field(v, "expected_dissent_logs")?
.as_object()
.ok_or("expected_dissent_logs is not an object")?;
if outcomes.len() != reported.len() || logs.len() != reported.len() {
return Err("expected_outcomes and expected_dissent_logs must name every node".into());
}
for (node, computed) in computed_map {
let computed = hash32(computed, node)?;
let view = node_view(n, &reported, node, &computed).map_err(|e| e.to_string())?;
let published = outcomes
.get(node)
.and_then(S::as_str)
.ok_or_else(|| format!("no outcome for {node}"))?;
same(
&format!("{node}'s outcome"),
view.outcome(&computed).as_str().to_owned(),
published,
)?;
let log: Vec<S> = view
.dissent_entry(frame, &computed)
.into_iter()
.map(|e| {
serde_json::json!({
"frame": e.frame,
"dissenting_peers": e.dissenting_peers,
"local_hash": hex::encode(e.local_hash),
"supermajority_hash": hex::encode(e.supermajority_hash),
})
})
.collect();
if logs.get(node) != Some(&S::Array(log.clone())) {
return Err(format!(
"{node}'s dissent log: reference computes {}",
S::Array(log)
));
}
}
Ok(())
}
fn gossip(v: &S) -> R {
let deliveries = field(v, "deliveries")?
.as_array()
.ok_or("deliveries is not an array")?;
let expected = field(v, "expected_results")?
.as_array()
.ok_or("expected_results is not an array")?;
if deliveries.len() != expected.len() {
return Err("one expected result per delivery".into());
}
let mut inbox = Inbox::new();
for (i, (d, want)) in deliveries.iter().zip(expected).enumerate() {
let got = match inbox.deliver(text(d, "claimed_said")?, &hex_field(d, "cbor_hex")?) {
Delivery::Accepted(s) => {
serde_json::json!({"outcome": "accepted", "said": s.to_said_text()})
}
Delivery::Duplicate(s) => {
serde_json::json!({"outcome": "duplicate", "said": s.to_said_text()})
}
Delivery::Rejected(e) => {
serde_json::json!({"outcome": "rejected", "error": e.class()})
}
};
if &got != want {
return Err(format!(
"delivery {i}: published {want}, reference computes {got}"
));
}
}
let held: Vec<S> = inbox
.held()
.iter()
.map(|(s, b)| serde_json::json!({"said": s.to_said_text(), "cbor_hex": hex::encode(b)}))
.collect();
if field(v, "expected_held")? != &S::Array(held.clone()) {
return Err(format!("expected_held: reference holds {}", S::Array(held)));
}
Ok(())
}