//! The published log-seal pack (`log_seal.json`, `extensions/log-seal.md`), //! read through the reference library. `conformance/verify_seal.py` reads the //! same file without it. use jcp_logseal::{ consistent, keys_for, read_seal, verify_cosignatures, verify_extending, verify_receipt, HybridKey, Pin, Policy, ReceiptProof, StewardKeys, Trust, Witness, Witnessing, }; use jcp_schema::ContentId; use serde_json::Value; use std::path::PathBuf; fn pack() -> Value { let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../../conformance/v1/log_seal.json"); serde_json::from_slice(&std::fs::read(path).expect("read the pack")).expect("the pack parses") } fn bytes(v: &Value) -> Vec { hex::decode(v.as_str().expect("hex")).expect("hex decodes") } #[test] fn every_published_case_reads_as_the_pack_says() { let doc = pack(); let steward = doc["steward"].as_str().unwrap(); let ed: [u8; 32] = bytes(&doc["pubkey_hex"]).try_into().unwrap(); let pq = bytes(&doc["pq_key_hex"]); let cases = doc["cases"].as_array().unwrap(); assert!(cases.len() >= 35, "the pack lost cases"); let mut codes = std::collections::BTreeSet::new(); for case in cases { let name = case["name"].as_str().unwrap(); let log: Vec> = doc["logs"][case["log"].as_str().unwrap()] .as_array() .unwrap() .iter() .take(case["take"].as_u64().unwrap() as usize) .map(bytes) .collect(); let seals: Vec> = case["seals"].as_array().unwrap().iter().map(|s| bytes(&doc["seals"][s.as_str().unwrap()])).collect(); let trust = match case["trust"].as_str().unwrap() { "receipts-and-seals" => Trust::ReceiptsAndSeals, "seals-alone" => Trust::SealsAlone, other => panic!("{name}: unknown trust {other}"), }; let policy = match case["policy"].as_str().unwrap() { "report-unsealed" => Policy::ReportUnsealed, "require-sealed" => Policy::RequireSealed, other => panic!("{name}: unknown policy {other}"), }; // the seal this reader last accepted, where the case gives it one let pin = case.get("pin").map(|p| { let seal = read_seal(&bytes(&doc["seals"][p.as_str().unwrap()]), steward, &ed, &pq).expect("a pin is a valid seal"); Pin::from(&seal) }); let got = match verify_extending(&log, &seals, steward, &ed, &pq, trust, policy, pin.as_ref()) { Ok((s, _)) => serde_json::json!({ "entries": s.entries, "sealed": s.sealed }), Err(e) => { codes.insert(e.code()); Value::String(e.code().to_string()) } }; assert_eq!(got, case["expect"], "{name}: {}", case["about"]); } // every refusal the extension defines is exercised by the pack let all = [ "log_chain_broken", "log_truncated", "log_unsealed", "pq_signature_invalid", "pq_suite_unknown", "seal_chain_broken", "seal_head_mismatch", "seal_malformed", "seal_root_mismatch", "seal_signature_invalid", "seal_withheld", "seal_wrong_key", "seal_wrong_steward", ]; assert_eq!(codes.into_iter().collect::>(), all); } #[test] fn every_published_proof_reads_as_the_pack_says() { let doc = pack(); let steward = doc["steward"].as_str().unwrap(); let ed: [u8; 32] = bytes(&doc["pubkey_hex"]).try_into().unwrap(); let pq = bytes(&doc["pq_key_hex"]); let proofs = doc["proofs"].as_array().unwrap(); assert!(proofs.len() >= 14, "the pack lost proofs"); let (mut stood, mut refused) = (0, 0); for case in proofs { let name = case["name"].as_str().unwrap(); let envelope = bytes(&doc["logs"][case["log"].as_str().unwrap()][case["receipt"].as_u64().unwrap() as usize]); let proof = ReceiptProof { index: case["index"].as_u64().unwrap(), path: case["path"].as_array().unwrap().iter().map(|h| ContentId::from_hex(h.as_str().unwrap()).unwrap()).collect(), }; let seal = bytes(&doc["seals"][case["seal"].as_str().unwrap()]); let trust = match case["trust"].as_str().unwrap() { "receipts-and-seals" => Trust::ReceiptsAndSeals, "seals-alone" => Trust::SealsAlone, other => panic!("{name}: unknown trust {other}"), }; let got = match verify_receipt(&envelope, &proof, &seal, steward, &ed, &pq, trust) { Ok(s) => { stood += 1; serde_json::json!({ "count": s.count }) } Err(e) => { refused += 1; Value::String(e.code().to_string()) } }; assert_eq!(got, case["expect"], "{name}: {}", case["about"]); } assert!(stood >= 4 && refused >= 9, "the pack no longer holds both proofs that stand and proofs that do not"); } /// The pack's key id is the id of the keys it publishes, and a reader given /// only the id takes those keys from a seal. #[test] fn the_published_key_id_fixes_the_published_keys() { let doc = pack(); let keys = StewardKeys { ed: bytes(&doc["pubkey_hex"]).try_into().unwrap(), pq: bytes(&doc["pq_key_hex"]) }; let id = ContentId::from_hex(doc["key_id_hex"].as_str().unwrap()).unwrap(); assert_eq!(keys.id(), id); assert_eq!(keys_for(&bytes(&doc["seals"]["first"]), &id), Ok(keys)); assert!(keys_for(&bytes(&doc["seals"]["forgers-own"]), &id).is_err()); } /// A seal's count and root as it states them, signature unread: what a /// consistency path is checked between. fn count_and_root(seal: &[u8]) -> (u64, ContentId) { let v: Value = jcp_schema::from_canonical_cbor(seal).expect("a seal decodes"); (v["count"].as_u64().unwrap(), ContentId::from_hex(v["root"].as_str().unwrap()).unwrap()) } fn ids(v: &Value) -> Vec { v.as_array().unwrap().iter().map(|h| ContentId::from_hex(h.as_str().unwrap()).unwrap()).collect() } #[test] fn every_published_consistency_path_reads_as_the_pack_says() { let doc = pack(); let cases = doc["consistency"].as_array().unwrap(); let (mut held, mut not) = (0, 0); for case in cases { let name = case["name"].as_str().unwrap(); let (m, old) = count_and_root(&bytes(&doc["seals"][case["from"].as_str().unwrap()])); let (n, new) = count_and_root(&bytes(&doc["seals"][case["to"].as_str().unwrap()])); let got = consistent(m, &old, n, &new, &ids(&case["path"])); assert_eq!(Value::Bool(got), case["expect"], "{name}: {}", case["about"]); if got { held += 1 } else { not += 1 } } assert!(held >= 4 && not >= 5, "the pack no longer holds both paths that hold and paths that do not"); } /// The pack's witness scripts, replayed against the reference witness. The /// key a witness signs with does not change what it decides, so any key /// serves; the pack publishes the decisions, not the cosignatures. #[test] fn every_published_witness_script_decides_as_the_pack_says() { let doc = pack(); let steward = doc["steward"].as_str().unwrap(); let key_id = ContentId::from_hex(doc["key_id_hex"].as_str().unwrap()).unwrap(); let mut codes = std::collections::BTreeSet::new(); for script in doc["witness_scripts"].as_array().unwrap() { let mut w = Witness::new(HybridKey::new(ed25519_dalek::SigningKey::from_bytes(&[88; 32]), [88; 32])); w.watch(steward, key_id); for step in script["steps"].as_array().unwrap() { let seals: Vec> = step["seals"].as_array().unwrap().iter().map(|n| bytes(&doc["seals"][n.as_str().unwrap()])).collect(); let got = match w.cosign(steward, &seals, &ids(&step["path"]), step["time"].as_u64().unwrap()) { Ok(_) => "cosigned".to_string(), Err(e) => e.code().to_string(), }; codes.insert(got.clone()); assert_eq!(Value::String(got), step["expect"], "{}: {}", script["name"], step["what"]); } } let all = ["cosigned", "seal_inconsistent", "seal_withheld", "seal_wrong_key"]; assert_eq!(codes.into_iter().collect::>(), all); } #[test] fn every_published_witnessed_case_reads_as_the_pack_says() { let doc = pack(); let steward = doc["steward"].as_str().unwrap(); let witness_ids = ids(&doc["witness_key_ids"]); let cases = doc["witnessed"].as_array().unwrap(); let (mut stood, mut refused) = (0, 0); for case in cases { let name = case["name"].as_str().unwrap(); let seal = bytes(&doc["seals"][case["seal"].as_str().unwrap()]); let cosigs: Vec> = case["cosignatures"].as_array().unwrap().iter().map(|n| bytes(&doc["cosignatures"][n.as_str().unwrap()])).collect(); let trusted: Vec = case["witnesses"].as_array().unwrap().iter().map(|i| witness_ids[i.as_u64().unwrap() as usize]).collect(); let rule = Witnessing { witnesses: &trusted, quorum: case["quorum"].as_u64().unwrap() as usize, not_before: case["not_before"].as_u64().unwrap() }; let got = match verify_cosignatures(&seal, steward, &cosigs, &rule) { Ok(w) => { stood += 1; serde_json::json!({ "witnesses": w.witnesses.len(), "as_of": w.as_of }) } Err(e) => { refused += 1; Value::String(e.code().to_string()) } }; assert_eq!(got, case["expect"], "{name}: {}", case["about"]); } assert!(stood >= 4 && refused >= 10, "the pack no longer holds both quorums met and quorums not"); } /// The pack's compacted log (§5.4), each case read through the reference /// library, and the good one restoring the ledger the pack publishes. #[test] fn every_published_compaction_case_reads_as_the_pack_says() { use jcp_logseal::compact::{verify_compacted, Carried, Compacted}; let doc = pack(); let steward = doc["steward"].as_str().unwrap(); let keys = StewardKeys { ed: bytes(&doc["pubkey_hex"]).try_into().unwrap(), pq: bytes(&doc["pq_key_hex"]) }; let comp = &doc["compaction"]; let log_of = |name: &str| -> Vec> { doc["logs"][name].as_array().unwrap().iter().map(bytes).collect() }; let entries = log_of(comp["log"].as_str().unwrap()); let k = comp["cut"].as_u64().unwrap() as usize; let carried: Vec = comp["carried"] .as_array() .unwrap() .iter() .map(|c| { let index = c["index"].as_u64().unwrap(); Carried { index, envelope: entries[index as usize].clone(), path: ids(&c["path"]) } }) .collect(); let mut codes = std::collections::BTreeSet::new(); for case in comp["cases"].as_array().unwrap() { let name = case["name"].as_str().unwrap(); let mut c = carried.clone(); match case["carried"].as_str().unwrap() { "all" => {} "first" => c.truncate(1), "none" => c.clear(), "swapped" => c[0].path = c[1].path.clone(), other => panic!("{name}: {other}"), } let after_log = log_of(case["after_from"].as_str().unwrap()); let n = case["after"].as_u64().unwrap() as usize; let seal = if case["seal"] == "cut" { bytes(&comp["seal"]) } else { bytes(&comp["twin_seal"]) }; let log = Compacted { compaction: bytes(&comp["records"][case["record"].as_str().unwrap()]), seal, carried: c, after: after_log[k..k + n].to_vec(), }; let later: Vec> = case["later"].as_array().unwrap().iter().map(|l| bytes(&comp["later_seals"][l.as_str().unwrap()])).collect(); let policy = if case["policy"] == "require-sealed" { Policy::RequireSealed } else { Policy::ReportUnsealed }; let got = match verify_compacted(&log, &later, steward, &keys, &keys.ed, Trust::SealsAlone, policy) { Ok(r) => { let sums = |m: &std::collections::BTreeMap| -> Value { Value::Object(m.iter().map(|(g, n)| (g.to_hex(), serde_json::json!(n))).collect()) }; assert_eq!(sums(&r.ledger.minor), comp["ledger_after"]["minor"], "{name}: money restored"); assert_eq!(sums(&r.ledger.uj), comp["ledger_after"]["uj"], "{name}: joules restored"); assert_eq!(sums(&r.ledger.refunded_minor), comp["ledger_after"]["refunded_minor"], "{name}: refunds restored"); serde_json::json!({ "entries": r.standing.entries, "sealed": r.standing.sealed }) } Err(e) => { codes.insert(e.code()); Value::String(e.code().to_string()) } }; assert_eq!(got, case["expect"], "{name}: {}", case["about"]); } for code in ["compaction_mismatch", "compaction_unclosed"] { assert!(codes.contains(code), "the pack exercises {code}"); } } /// The pack's forks and equivocations (§5.5), through the reference library. #[test] fn every_published_fork_and_equivocation_reads_as_the_pack_says() { use jcp_logseal::{verify_equivocation, verify_fork, Divergence, Fork}; let doc = pack(); let steward = doc["steward"].as_str().unwrap(); let key_ids = [ContentId::from_hex(doc["key_id_hex"].as_str().unwrap()).unwrap()]; let fork_of = |case: &Value| Fork { a: bytes(&doc["seals"][case["a"].as_str().unwrap()]), b: bytes(&doc["seals"][case["b"].as_str().unwrap()]), divergence: (!case["divergence"].is_null()).then(|| Divergence { leaf: ContentId::from_hex(case["divergence"]["leaf"].as_str().unwrap()).unwrap(), path: ids(&case["divergence"]["path"]), }), }; let mut shown = 0; for case in doc["forks"].as_array().unwrap() { let got = match verify_fork(&fork_of(case), steward, &key_ids) { Ok(f) => { shown += 1; serde_json::json!({ "counts": f.counts }) } Err(e) => Value::String(e.code().to_string()), }; assert_eq!(got, case["expect"], "{}: {}", case["name"], case["about"]); } assert!(shown >= 2, "the pack shows forks of one count and of two"); for case in doc["equivocations"].as_array().unwrap() { let fork = doc["forks"].as_array().unwrap().iter().find(|f| f["name"] == case["fork"]).unwrap(); let c = case["cosignatures"].as_array().unwrap(); let got = match verify_equivocation(&fork_of(fork), &bytes(&c[0]), &bytes(&c[1]), steward, &key_ids) { Ok(w) => serde_json::json!({ "witness": w.to_hex() }), Err(e) => Value::String(e.code().to_string()), }; assert_eq!(got, case["expect"], "{}: {}", case["name"], case["about"]); } }