All checks were successful
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Successful in 11m9s
Witnesses that keep their rule never cosign two lines of seals, and the spec said a witness that did could be shown to have; nothing showed it. - verify_fork: two seals of one steward, each read under keys among its key ids, whose logs part. Of one count, their roots differ; of two, the larger log has, at the smaller seal's last place, a receipt that is not the smaller seal's head, shown with its path to the larger root. - prove_fork: the evidence built by a reader holding the larger log. - verify_equivocation: a fork with two cosignatures, of its two seals, by one witness, which a witness keeping its rule never writes. - New codes fork_not_shown and equivocation_not_shown; log-seal 5.5, the section 6 bullet on witnesses, and C33 in the extension and SPEC. - log_seal.json: seven forks (one count, two counts, a prefix offered with another history's leaf and with its own true leaf, no divergence, one seal twice, a foreign key) and three equivocations; verify_seal.py decides them on its own with four rules, each noticed when removed. Seven seeded faults, each caught; the one first missed (a divergence at the smaller seal's own head) is caught by the prefix-with-its-true-leaf case added for it. Co-Authored-By: Claude Opus 5.5 <[email protected]>
307 lines
14 KiB
Rust
307 lines
14 KiB
Rust
//! 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<u8> {
|
|
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<Vec<u8>> = 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<Vec<u8>> =
|
|
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::<Vec<_>>(), 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<ContentId> {
|
|
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<Vec<u8>> = 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::<Vec<_>>(), 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<Vec<u8>> = case["cosignatures"].as_array().unwrap().iter().map(|n| bytes(&doc["cosignatures"][n.as_str().unwrap()])).collect();
|
|
let trusted: Vec<ContentId> = 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<Vec<u8>> { 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<Carried> = 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<Vec<u8>> = 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<ContentId, u64>| -> 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"]);
|
|
}
|
|
}
|