//! 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 { serde_json::from_str::(&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)) -> Vec { 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); let plants: Vec = 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 { 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::>(); 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 = 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() ); }