open-standards/jcp/jcp-rs/crates/jcp-conformance/tests/log_seal_pack.rs
Transaction Science acc9e4ca07
All checks were successful
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Successful in 11m9s
Log seals: a steward's fork, and a witness on both sides of one, shown by evidence anyone can check
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]>
2026-11-11 15:53:27 -05:00

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"]);
}
}