Some checks failed
smart-byte conformance gate / Validate conformance JSON files (pull_request) Successful in 4s
smart-byte conformance gate / Second implementation reproduces every vector (pull_request) Successful in 5s
smart-byte conformance gate / Spec changes require vector-pack updates (pull_request) Successful in 4s
smart-byte conformance gate / VERSIONS.md and README.md must exist (pull_request) Successful in 3s
no-vanity-metrics / Scan committed Markdown for vanity metrics (pull_request) Successful in 4s
standards workspaces build + test / eoc-rs (pull_request) Successful in 1m51s
standards workspaces build + test / jcp-rs (pull_request) Successful in 1m24s
standards workspaces build + test / proof-rs (pull_request) Successful in 1m41s
standards workspaces build + test / sandbox-rs (pull_request) Successful in 2m10s
standards workspaces build + test / smart-byte-rs (pull_request) Successful in 3m38s
standards workspaces build + test / jouleclaw-rs (pull_request) Successful in 7m45s
standards workspaces build + test / aac-rs (pull_request) Successful in 4m3s
standards workspaces build + test / joule-code-rs (pull_request) Successful in 1m51s
standards workspaces build + test / joulecontract-rs (pull_request) Successful in 2m3s
standards workspaces build + test / map-rs (pull_request) Successful in 5m27s
standards workspaces build + test / mesh-rs (pull_request) Successful in 2m19s
standards workspaces build + test / openpay (pull_request) Successful in 10m49s
standards workspaces build + test / arl-rs (pull_request) Successful in 47m4s
standards workspaces build + test / joule-ui-rs (pull_request) Successful in 46m44s
standards workspaces build + test / jouledesk-rs (pull_request) Successful in 42m33s
standards workspaces build + test / joulehook-rs (pull_request) Failing after 18m22s
standards workspaces build + test / sense-rs (pull_request) Successful in 10m25s
standards workspaces build + test / macOS energy backend (jcp-energy) (pull_request) Successful in 4m56s
smart-byte-state implements cluster_state.md: - governance: Change and Proposal, electorates, the quorum, the time lock; - burns: Burn and the retired record; - exports: ExportStatement and Certificate; - the import checks. smart-byte-core: Transition::sign_export and the destination member. The envelope compares frames within a cluster only. smart-byte-lockstep: the committing nodes sign each committed frame's export certificates. The certificates are built from a committing node's report, never from a node whose state diverged. state_vectors.json grows to 23 vectors (state-013 to state-023 new, state-011 extended) and is regenerated in place; v1 is DRAFT. Every new check has a vector that fails an implementation leaving it out. The README documents the 14 new error classes, and the regeneration log records the change. crosscheck/verify_v1.py implements governance, burns, exports and imports from the spec text. It reproduces every vector, 117 of them, both plain and under -O. Co-Authored-By: Claude Opus 5.5 <[email protected]>
694 lines
24 KiB
Rust
694 lines
24 KiB
Rust
//! The published pack, `smart-byte/conformance/v1`, held to the reference
|
|
//! implementation under `cargo test` — so CI fails whenever the two disagree.
|
|
|
|
use std::collections::BTreeSet;
|
|
use std::path::Path;
|
|
|
|
use serde_json::{Value, json};
|
|
use smart_byte_conformance::verify::{readme_error_classes, spec_cited_said_vectors};
|
|
use smart_byte_conformance::{pack, published_pack_dir, verify::verify_pack};
|
|
|
|
fn published(file: &str) -> String {
|
|
std::fs::read_to_string(published_pack_dir().join(file))
|
|
.unwrap_or_else(|e| panic!("read {file}: {e}"))
|
|
}
|
|
|
|
fn vectors(file: &str) -> Vec<Value> {
|
|
serde_json::from_str::<Value>(&published(file))
|
|
.unwrap()
|
|
.as_array()
|
|
.unwrap()
|
|
.clone()
|
|
}
|
|
|
|
fn by_name(file: &str, name: &str) -> Value {
|
|
vectors(file)
|
|
.into_iter()
|
|
.find(|v| v["name"] == name)
|
|
.unwrap_or_else(|| panic!("{file} has no vector {name}"))
|
|
}
|
|
|
|
#[test]
|
|
fn every_published_vector_agrees_with_the_reference() {
|
|
let report = verify_pack(&published_pack_dir());
|
|
let failures = report.failures();
|
|
assert!(failures.is_empty(), "{failures:#?}");
|
|
// One structure check per file, one check per vector, two completeness
|
|
// checks: none skipped.
|
|
let total: usize = pack::FILES.iter().map(|f| vectors(f).len()).sum();
|
|
assert_eq!(report.checks.len(), pack::FILES.len() + total + 2);
|
|
}
|
|
|
|
#[test]
|
|
fn the_published_pack_is_exactly_what_the_reference_generates() {
|
|
for (file, generated) in pack::generate() {
|
|
assert!(
|
|
published(file) == generated,
|
|
"{file} differs from the reference implementation's output; regenerate with \
|
|
`cargo run -p smart-byte-conformance --bin gen-conformance-vectors`"
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Write the published pack to `dir` with `edit` applied to the files'
|
|
/// parsed contents, and return the names of the checks the verifier fails.
|
|
fn failures_with(dir: &Path, edit: impl Fn(&str, &mut Vec<Value>)) -> Vec<String> {
|
|
std::fs::create_dir_all(dir).unwrap();
|
|
for f in pack::FILES {
|
|
let mut items = vectors(f);
|
|
edit(f, &mut items);
|
|
std::fs::write(dir.join(f), serde_json::to_string_pretty(&items).unwrap()).unwrap();
|
|
}
|
|
verify_pack(dir)
|
|
.failures()
|
|
.into_iter()
|
|
.map(|c| c.vector.clone())
|
|
.collect()
|
|
}
|
|
|
|
fn scratch(name: &str) -> std::path::PathBuf {
|
|
Path::new(env!("CARGO_TARGET_TMPDIR")).join(name)
|
|
}
|
|
|
|
/// Falsification: the verifier is not vacuous. Each planted defect — a wrong
|
|
/// value, a wrong error class, a negative vector turned positive — must be
|
|
/// caught, and caught on the vector it was planted in.
|
|
#[test]
|
|
fn the_verifier_catches_a_planted_defect_in_every_file() {
|
|
let flip_last = |s: &str| {
|
|
let mut s = s.to_owned();
|
|
let last = s.pop().unwrap();
|
|
s.push(if last == '0' { '1' } else { '0' });
|
|
s
|
|
};
|
|
type Plant = (&'static str, &'static str, Box<dyn Fn(&mut Value)>);
|
|
let plants: Vec<Plant> = vec![
|
|
(
|
|
"canonicalisation_vectors.json",
|
|
"canon-001-sorted-map-keys",
|
|
Box::new(move |v| {
|
|
v["expected_cbor_hex"] = json!(flip_last(v["expected_cbor_hex"].as_str().unwrap()))
|
|
}),
|
|
),
|
|
(
|
|
"canonicalisation_vectors.json",
|
|
"canon-012-text-ordered-keys-refused",
|
|
Box::new(|v| v["error"] = json!("duplicate_map_key")),
|
|
),
|
|
(
|
|
"canonicalisation_vectors.json",
|
|
"canon-004-floats-forbidden",
|
|
Box::new(|v| v["input_cbor_hex"] = json!("a16a65786163745f68616c6601")),
|
|
),
|
|
(
|
|
"canonicalisation_vectors.json",
|
|
"canon-020-simple-value-refused",
|
|
Box::new(|v| v["input_cbor_hex"] = json!("f6")),
|
|
),
|
|
(
|
|
"said_vectors.json",
|
|
"said-008-spec-worked-example",
|
|
Box::new(move |v| {
|
|
v["expected_said"] = json!(flip_last(v["expected_said"].as_str().unwrap()))
|
|
}),
|
|
),
|
|
(
|
|
"said_vectors.json",
|
|
"said-009-non-canonical-copy-rejected",
|
|
Box::new(|v| v["error"] = json!("said_mismatch")),
|
|
),
|
|
(
|
|
"said_vectors.json",
|
|
"said-010-self-consistent-non-canonical-rejected",
|
|
Box::new(move |v| {
|
|
v["said_of_received_bytes"] =
|
|
json!(flip_last(v["said_of_received_bytes"].as_str().unwrap()))
|
|
}),
|
|
),
|
|
(
|
|
"said_vectors.json",
|
|
"said-003-transferred-envelope",
|
|
Box::new(|v| v["input"]["joule_cost"]["measured_microjoules"] = json!(1201)),
|
|
),
|
|
(
|
|
"signature_vectors.json",
|
|
"sig-001-issuer-attestation",
|
|
Box::new(move |v| v["signature"] = json!(flip_last(v["signature"].as_str().unwrap()))),
|
|
),
|
|
(
|
|
"signature_vectors.json",
|
|
"sig-003-message-swapped",
|
|
Box::new(|v| {
|
|
v["expected_verify"] = json!(true);
|
|
v["error"] = Value::Null;
|
|
}),
|
|
),
|
|
(
|
|
"signature_vectors.json",
|
|
"sig-006-non-canonical-s-rejected",
|
|
Box::new(|v| v["error"] = json!("malformed_signature")),
|
|
),
|
|
(
|
|
"signature_vectors.json",
|
|
"sig-007-non-canonical-key-rejected",
|
|
// The canonical spelling of the same point: now a well-formed key
|
|
// that merely fails to verify, so the published class is wrong.
|
|
Box::new(|v| {
|
|
v["verifying_key"] =
|
|
json!("0300000000000000000000000000000000000000000000000000000000000000")
|
|
}),
|
|
),
|
|
(
|
|
"kel_vectors.json",
|
|
"kel-004-no-next-keys",
|
|
Box::new(|v| v["expected"]["next_threshold"] = json!(1)),
|
|
),
|
|
(
|
|
"kel_vectors.json",
|
|
"kel-008-rotation-signed-by-current-keys",
|
|
Box::new(|v| v["error"] = json!("pre_rotation_mismatch")),
|
|
),
|
|
(
|
|
"kel_vectors.json",
|
|
"kel-017-rotation-after-no-next-keys",
|
|
Box::new(|v| v["error_at"] = json!(0)),
|
|
),
|
|
(
|
|
"kel_vectors.json",
|
|
"kel-022-key-written-as-integers",
|
|
// The issuer's inception in its own encoding, with its own
|
|
// signature: a log the vector says must be refused, accepted.
|
|
Box::new(|v| {
|
|
v["events"] =
|
|
by_name("kel_vectors.json", "kel-001-single-key-inception")["events"].clone()
|
|
}),
|
|
),
|
|
(
|
|
"kel_vectors.json",
|
|
"kel-013-duplicate-key",
|
|
// A seed that did not make the signature beside it.
|
|
Box::new(|v| v["events"][0]["signatures"][1]["seed"] = json!("00".repeat(32))),
|
|
),
|
|
(
|
|
"envelope_vectors.json",
|
|
"env-001-issued",
|
|
Box::new(move |v| {
|
|
v["expected"]["history_hash"] =
|
|
json!(flip_last(v["expected"]["history_hash"].as_str().unwrap()))
|
|
}),
|
|
),
|
|
(
|
|
"envelope_vectors.json",
|
|
"env-011-old-key-under-new-establishment",
|
|
Box::new(|v| v["error"] = json!("below_threshold")),
|
|
),
|
|
(
|
|
"envelope_vectors.json",
|
|
"env-018-cargo-bytes-written-as-integers",
|
|
// env-003 in its own encoding: accepted, though the vector says
|
|
// it must be refused.
|
|
Box::new(|v| {
|
|
let own = by_name("envelope_vectors.json", "env-003-empty-opaque-cargo");
|
|
v["envelope"] = own["envelope"].clone();
|
|
v["cbor_hex"] = own["cbor_hex"].clone();
|
|
}),
|
|
),
|
|
(
|
|
"envelope_vectors.json",
|
|
"env-002-transfers-across-a-rotation",
|
|
Box::new(|v| v["expected"]["owner"] = v["expected"]["issuer"].clone()),
|
|
),
|
|
(
|
|
"state_vectors.json",
|
|
"state-001-issue-transfer-rotate",
|
|
Box::new(move |v| {
|
|
let h = v["frames"][1]["expected_state_hash"]
|
|
.as_str()
|
|
.unwrap()
|
|
.to_owned();
|
|
v["frames"][1]["expected_state_hash"] = json!(flip_last(&h));
|
|
}),
|
|
),
|
|
(
|
|
"state_vectors.json",
|
|
"state-007-transfer-checks",
|
|
Box::new(|v| {
|
|
let results = v["frames"][1]["expected_results"].as_array_mut().unwrap();
|
|
let r = results
|
|
.iter_mut()
|
|
.find(|r| r["error"] == "broken_history_chain")
|
|
.unwrap();
|
|
r["error"] = json!("unknown_byte");
|
|
}),
|
|
),
|
|
(
|
|
"state_vectors.json",
|
|
"state-004-rotation-retires-old-keys",
|
|
// Application order is part of the result: the key event first.
|
|
Box::new(|v| {
|
|
v["frames"][1]["expected_results"]
|
|
.as_array_mut()
|
|
.unwrap()
|
|
.reverse()
|
|
}),
|
|
),
|
|
(
|
|
"state_vectors.json",
|
|
"state-011-malformed-inputs",
|
|
Box::new(|v| {
|
|
let results = v["frames"][0]["expected_results"].as_array_mut().unwrap();
|
|
let r = results
|
|
.iter_mut()
|
|
.find(|r| r["error"] == "non_shortest_integer")
|
|
.unwrap();
|
|
r["class"] = json!("transfer");
|
|
}),
|
|
),
|
|
(
|
|
"state_vectors.json",
|
|
"state-012-duplicate-submissions",
|
|
// A duplicate counted as a second input.
|
|
Box::new(|v| {
|
|
let results = v["frames"][0]["expected_results"].as_array_mut().unwrap();
|
|
let first = results[0].clone();
|
|
results.insert(1, first);
|
|
}),
|
|
),
|
|
(
|
|
"state_vectors.json",
|
|
"state-009-signers-need-their-log",
|
|
// Without Alice's inception her transfer cannot be checked.
|
|
Box::new(|v| {
|
|
let inputs = v["frames"][2]["inputs"].as_array_mut().unwrap();
|
|
inputs.retain(|i| i["input"].get("key_event").is_none());
|
|
}),
|
|
),
|
|
(
|
|
"state_vectors.json",
|
|
"state-013-issuer-added-after-the-delay",
|
|
Box::new(|v| v["frames"][2]["expected_results"][0]["error"] = json!("below_quorum")),
|
|
),
|
|
(
|
|
"state_vectors.json",
|
|
"state-015-removal-without-the-target",
|
|
Box::new(|v| v["frames"][1]["expected_results"][0]["error"] = json!("below_quorum")),
|
|
),
|
|
(
|
|
"state_vectors.json",
|
|
"state-018-redemption",
|
|
Box::new(|v| {
|
|
let results = v["frames"][3]["expected_results"].as_array_mut().unwrap();
|
|
let r = results
|
|
.iter_mut()
|
|
.find(|r| r["error"] == "duplicate_byte")
|
|
.unwrap();
|
|
r["error"] = json!("byte_retired");
|
|
}),
|
|
),
|
|
(
|
|
"state_vectors.json",
|
|
"state-019-export-and-return",
|
|
Box::new(move |v| {
|
|
let h = v["frames"][2]["expected_state_hash"]
|
|
.as_str()
|
|
.unwrap()
|
|
.to_owned();
|
|
v["frames"][2]["expected_state_hash"] = json!(flip_last(&h));
|
|
}),
|
|
),
|
|
(
|
|
"state_vectors.json",
|
|
"state-020-import-and-send-back",
|
|
Box::new(|v| {
|
|
let results = v["frames"][0]["expected_results"].as_array_mut().unwrap();
|
|
let r = results.iter_mut().find(|r| r["class"] == "import").unwrap();
|
|
r["outcome"] = json!("dropped");
|
|
r["error"] = json!("duplicate_import");
|
|
}),
|
|
),
|
|
(
|
|
"state_vectors.json",
|
|
"state-021-import-checks",
|
|
Box::new(|v| {
|
|
let results = v["frames"][1]["expected_results"].as_array_mut().unwrap();
|
|
let r = results
|
|
.iter_mut()
|
|
.find(|r| r["error"] == "below_threshold")
|
|
.unwrap();
|
|
r["error"] = json!("signature_invalid");
|
|
}),
|
|
),
|
|
(
|
|
"state_vectors.json",
|
|
"state-023-an-import-meets-the-byte",
|
|
// A burned byte's import reported as if the byte were still live.
|
|
Box::new(|v| v["frames"][2]["expected_results"][0]["error"] = json!("duplicate_byte")),
|
|
),
|
|
(
|
|
"lockstep_vectors.json",
|
|
"lockstep-002-supermajority-commit",
|
|
Box::new(|v| v["expected_dissent_logs"]["node_1"] = json!([])),
|
|
),
|
|
(
|
|
"lockstep_vectors.json",
|
|
"lockstep-004-three-nodes-one-diverged-split",
|
|
Box::new(|v| v["expected_commit"] = json!(true)),
|
|
),
|
|
(
|
|
"lockstep_vectors.json",
|
|
"lockstep-002-supermajority-commit",
|
|
Box::new(|v| v["expected_outcomes"]["node_4"] = json!("commit_with_dissent")),
|
|
),
|
|
(
|
|
"lockstep_vectors.json",
|
|
"lockstep-006-transport-liar-is-logged",
|
|
Box::new(|v| v["expected_outcomes"]["node_4"] = json!("minority_divergence")),
|
|
),
|
|
(
|
|
"gossip_vectors.json",
|
|
"gossip-002-said-conflict-rejected",
|
|
Box::new(|v| {
|
|
v["expected_results"][1] =
|
|
json!({"outcome": "duplicate", "said": v["expected_results"][0]["said"]})
|
|
}),
|
|
),
|
|
(
|
|
"gossip_vectors.json",
|
|
"gossip-005-non-canonical-copy-rejected",
|
|
Box::new(|v| {
|
|
let s = v["deliveries"][0]["claimed_said"].clone();
|
|
let b = v["deliveries"][0]["cbor_hex"].clone();
|
|
v["expected_results"][0] = json!({"outcome": "accepted", "said": s});
|
|
v["expected_held"] = json!([{"said": s, "cbor_hex": b}]);
|
|
}),
|
|
),
|
|
(
|
|
"gossip_vectors.json",
|
|
"gossip-001-cross-node-propagation",
|
|
Box::new(move |v| {
|
|
let h = v["expected_held"][0]["cbor_hex"]
|
|
.as_str()
|
|
.unwrap()
|
|
.to_owned();
|
|
v["expected_held"][0]["cbor_hex"] = json!(flip_last(&h));
|
|
}),
|
|
),
|
|
];
|
|
for (i, (file, name, mutate)) in plants.iter().enumerate() {
|
|
let failed = failures_with(&scratch(&format!("planted/{i}")), |f, items| {
|
|
if f == *file {
|
|
mutate(items.iter_mut().find(|v| v["name"] == *name).expect(name));
|
|
}
|
|
});
|
|
assert_eq!(
|
|
failed,
|
|
vec![name.to_string()],
|
|
"planted defect {i} in {file}/{name}"
|
|
);
|
|
}
|
|
}
|
|
|
|
/// A defect in a log the envelope vectors draw key states from fails that
|
|
/// log and every envelope vector, and nothing else.
|
|
#[test]
|
|
fn a_defect_in_a_log_fails_the_envelopes_that_rely_on_it() {
|
|
let failed = failures_with(&scratch("planted/log-dependency"), |f, items| {
|
|
if f == "kel_vectors.json" {
|
|
let v = items
|
|
.iter_mut()
|
|
.find(|v| v["name"] == "kel-003-two-of-three-rotation")
|
|
.unwrap();
|
|
// Drop the rotation's second signature: one of the two it needs.
|
|
v["events"][1]["signatures"].as_array_mut().unwrap().pop();
|
|
}
|
|
});
|
|
let mut expected = vec!["kel-003-two-of-three-rotation".to_string()];
|
|
expected.extend(
|
|
vectors("envelope_vectors.json")
|
|
.iter()
|
|
.map(|v| v["name"].as_str().unwrap().to_owned()),
|
|
);
|
|
assert_eq!(failed, expected);
|
|
}
|
|
|
|
/// Falsification: a pack cannot pass by losing vectors.
|
|
#[test]
|
|
fn an_incomplete_pack_fails() {
|
|
// Every file emptied.
|
|
let failed = failures_with(&scratch("incomplete/empty"), |_, items| items.clear());
|
|
for f in pack::FILES {
|
|
assert!(failed.contains(&"(file)".to_string()), "{f}");
|
|
}
|
|
assert!(failed.contains(&"spec-cited vectors present".to_string()));
|
|
assert!(failed.contains(&"README error classes exercised".to_string()));
|
|
|
|
// One vector the spec cites, deleted.
|
|
let failed = failures_with(&scratch("incomplete/said-010"), |f, items| {
|
|
if f == "said_vectors.json" {
|
|
items.retain(|v| !v["name"].as_str().unwrap().starts_with("said-010-"));
|
|
}
|
|
});
|
|
assert_eq!(failed, vec!["spec-cited vectors present".to_string()]);
|
|
|
|
// The only vector exercising a README error class, deleted.
|
|
let failed = failures_with(&scratch("incomplete/class"), |f, items| {
|
|
if f == "canonicalisation_vectors.json" {
|
|
items.retain(|v| v["name"] != "canon-020-simple-value-refused");
|
|
}
|
|
});
|
|
assert_eq!(failed, vec!["README error classes exercised".to_string()]);
|
|
}
|
|
|
|
/// Falsification: a vector that names an error class the README does not
|
|
/// list is caught, so the README stays the full list.
|
|
#[test]
|
|
fn an_undocumented_error_class_fails() {
|
|
let failed = failures_with(&scratch("undocumented"), |f, items| {
|
|
if f == "canonicalisation_vectors.json" {
|
|
let mut extra = items
|
|
.iter()
|
|
.find(|v| v["name"] == "canon-018-trailing-bytes-refused")
|
|
.unwrap()
|
|
.clone();
|
|
extra["name"] = json!("canon-099-made-up");
|
|
extra["error"] = json!("made_up_class");
|
|
items.push(extra);
|
|
}
|
|
});
|
|
assert!(failed.contains(&"README error classes exercised".to_string()));
|
|
assert!(failed.contains(&"canon-099-made-up".to_string()));
|
|
}
|
|
|
|
#[test]
|
|
fn a_pack_with_a_duplicated_json_member_fails() {
|
|
let dir = scratch("duplicate-member");
|
|
std::fs::create_dir_all(&dir).unwrap();
|
|
for f in pack::FILES {
|
|
let mut body = published(f);
|
|
if f == "lockstep_vectors.json" {
|
|
// Name a member twice; a plain JSON parser would keep the last.
|
|
body = body.replacen(
|
|
"\"node_count\": 4,",
|
|
"\"node_count\": 4,\n \"node_count\": 4,",
|
|
1,
|
|
);
|
|
}
|
|
std::fs::write(dir.join(f), body).unwrap();
|
|
}
|
|
let report = verify_pack(&dir);
|
|
let failed: Vec<&str> = report.failures().iter().map(|c| c.file.as_str()).collect();
|
|
assert_eq!(failed, vec!["lockstep_vectors.json"]);
|
|
}
|
|
|
|
#[test]
|
|
fn the_said_pack_holds_the_suite_the_spec_names() {
|
|
let cited = spec_cited_said_vectors().unwrap();
|
|
for n in 1..=11 {
|
|
assert!(
|
|
cited.contains(&format!("said-{n:03}")),
|
|
"the spec no longer cites said-{n:03}"
|
|
);
|
|
}
|
|
let keys = |name: &str| -> BTreeSet<String> {
|
|
by_name("said_vectors.json", name)["input"]
|
|
.as_object()
|
|
.unwrap()
|
|
.keys()
|
|
.cloned()
|
|
.collect()
|
|
};
|
|
let set = |ks: &[&str]| ks.iter().map(|k| k.to_string()).collect::<BTreeSet<_>>();
|
|
assert_eq!(keys("said-006-only-said-field"), set(&["d"]));
|
|
assert_eq!(keys("said-007-one-field-per-key"), set(&["d", "k"]));
|
|
assert_eq!(keys("said-008-spec-worked-example"), set(&["d", "n"]));
|
|
}
|
|
|
|
#[test]
|
|
fn every_canonical_rule_has_a_negative_vector() {
|
|
let classes: BTreeSet<String> = vectors("canonicalisation_vectors.json")
|
|
.into_iter()
|
|
.filter_map(|v| v["error"].as_str().map(str::to_owned))
|
|
.collect();
|
|
for rule in [
|
|
"non_shortest_integer",
|
|
"non_shortest_length",
|
|
"unsorted_map_keys",
|
|
"duplicate_map_key",
|
|
"float_forbidden",
|
|
"simple_value_forbidden",
|
|
"tag_forbidden",
|
|
"indefinite_length",
|
|
"invalid_utf8",
|
|
"trailing_bytes",
|
|
] {
|
|
assert!(
|
|
classes.contains(rule),
|
|
"no canonicalisation vector is refused with {rule}"
|
|
);
|
|
assert!(
|
|
readme_error_classes().unwrap().contains(rule),
|
|
"README omits {rule}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn the_spec_prints_the_worked_example_the_pack_computes() {
|
|
let spec = std::fs::read_to_string(
|
|
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../spec/identity_and_key_rotation.md"),
|
|
)
|
|
.unwrap();
|
|
let example = by_name("said_vectors.json", "said-008-spec-worked-example");
|
|
for field in ["blake3_hex", "expected_said", "canonical_cbor_hex"] {
|
|
let value = example[field].as_str().unwrap();
|
|
assert!(
|
|
spec.contains(value),
|
|
"identity spec §2.4 does not print the pack's {field} {value}"
|
|
);
|
|
}
|
|
assert!(
|
|
spec.contains("total 61 bytes"),
|
|
"§2.4 must give the encoding's real length"
|
|
);
|
|
}
|
|
|
|
/// The files describe one world: the SAID vectors' objects are the envelope
|
|
/// and key-event-log vectors' objects, and the signature vectors sign their
|
|
/// messages.
|
|
#[test]
|
|
fn the_files_describe_one_world() {
|
|
let said = |name: &str| by_name("said_vectors.json", name)["expected_said"].clone();
|
|
let issued = by_name("envelope_vectors.json", "env-001-issued");
|
|
let transferred = by_name(
|
|
"envelope_vectors.json",
|
|
"env-002-transfers-across-a-rotation",
|
|
);
|
|
let alice = by_name("kel_vectors.json", "kel-002-rotation-then-interaction");
|
|
assert_eq!(
|
|
said("said-001-issued-envelope"),
|
|
issued["expected"]["snapshot"]
|
|
);
|
|
assert_eq!(
|
|
said("said-002-byte-identity"),
|
|
issued["expected"]["identity"]
|
|
);
|
|
assert_eq!(
|
|
said("said-003-transferred-envelope"),
|
|
transferred["expected"]["snapshot"]
|
|
);
|
|
assert_eq!(
|
|
said("said-004-rotation-event"),
|
|
alice["expected"]["event_saids"][1]
|
|
);
|
|
assert_eq!(
|
|
said("said-005-interaction-event"),
|
|
alice["expected"]["event_saids"][2]
|
|
);
|
|
// Alice's interaction anchors the byte she was issued.
|
|
assert_eq!(
|
|
alice["events"][2]["event"]["a"][0],
|
|
issued["expected"]["identity"]
|
|
);
|
|
|
|
let sig = |name: &str| by_name("signature_vectors.json", name);
|
|
let without_algorithm = |j: &Value| j.as_str().unwrap()[2..].to_owned();
|
|
let attestation = smart_byte_conformance::world::World::build()
|
|
.issued
|
|
.provenance
|
|
.signed_bytes();
|
|
assert_eq!(
|
|
sig("sig-001-issuer-attestation")["message_hex"],
|
|
json!(hex::encode(attestation))
|
|
);
|
|
assert_eq!(
|
|
sig("sig-001-issuer-attestation")["signature"],
|
|
json!(without_algorithm(
|
|
&issued["envelope"]["provenance"]["signature"]["signatures"][0]["signature"]["$bytes"]
|
|
))
|
|
);
|
|
assert_eq!(
|
|
sig("sig-002-key-event")["message_hex"],
|
|
alice["events"][0]["cbor_hex"]
|
|
);
|
|
assert_eq!(
|
|
sig("sig-002-key-event")["signature"],
|
|
json!(without_algorithm(
|
|
&alice["events"][0]["signatures"][0]["signature"]
|
|
))
|
|
);
|
|
|
|
// The cluster's founding frame: the three logs' inceptions and env-001's
|
|
// mint, as the other files give them.
|
|
let journey = by_name("state_vectors.json", "state-001-issue-transfer-rotate");
|
|
let founding: Vec<&Value> = journey["frames"][0]["inputs"]
|
|
.as_array()
|
|
.unwrap()
|
|
.iter()
|
|
.map(|i| &i["input"])
|
|
.collect();
|
|
for log in [
|
|
"kel-001-single-key-inception",
|
|
"kel-002-rotation-then-interaction",
|
|
"kel-003-two-of-three-rotation",
|
|
] {
|
|
let event = &by_name("kel_vectors.json", log)["events"][0]["event"];
|
|
assert!(
|
|
founding.iter().any(|i| &i["key_event"]["event"] == event),
|
|
"{log}'s inception"
|
|
);
|
|
}
|
|
assert!(
|
|
founding
|
|
.iter()
|
|
.any(|i| i["mint"] == issued["envelope"]["provenance"]),
|
|
"env-001's mint"
|
|
);
|
|
// A's export is what B imports, and B's export is what A takes back.
|
|
let a = by_name("state_vectors.json", "state-019-export-and-return");
|
|
let b = by_name("state_vectors.json", "state-020-import-and-send-back");
|
|
let id = issued["expected"]["identity"].as_str().unwrap();
|
|
let imported = |frame: &Value| {
|
|
frame["inputs"]
|
|
.as_array()
|
|
.unwrap()
|
|
.iter()
|
|
.find_map(|i| i["input"].get("import").cloned())
|
|
.unwrap()["certificate"]["statement"]["export"]
|
|
.clone()
|
|
};
|
|
assert_eq!(
|
|
imported(&b["frames"][0]),
|
|
a["frames"][2]["expected_state"]["exported"][id]
|
|
);
|
|
assert_eq!(
|
|
imported(&a["frames"][4]),
|
|
b["frames"][1]["expected_state"]["exported"][id]
|
|
);
|
|
// The byte the cluster holds is env-001's.
|
|
let state = &journey["frames"][0]["expected_state"];
|
|
assert!(
|
|
state["bytes"]
|
|
.get(issued["expected"]["identity"].as_str().unwrap())
|
|
.is_some()
|
|
);
|
|
}
|