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]>
865 lines
31 KiB
Rust
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(())
|
|
}
|