All checks were successful
joule-code conformance gate / joule-moq-relay, every target (pull_request) Successful in 2m7s
joule-code conformance gate / joule-moq-ingest, every target (pull_request) Successful in 2m14s
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 30s
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 1m47s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 3m43s
joule-code conformance gate / Build + round-trip C1-C4 vectors (pull_request) Successful in 3m25s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 7m0s
Found while hand-checking a candidate my own cross-standard audit had flagged. The audit was wrong — `verify_cascade` and `verify_gated_cascade` both exist, so C4 and C5 are implemented — but reading the crate to disprove the flag turned up three stale claims in its own documentation. **It says four suites; there are five.** "runs the four suites: C1 content addressing, C2 signing, C3 receipt, C4 cascade" omits C5 gated cascade, whose six vectors demonstrably run (`c5-ungranted-l4-denied-even-when-it-would-close` and friends). A reader of the header would not know C5 is covered at all. This is the understating direction of the same defect the last few changes here have been about: documentation narrower than the code. **It implies something is outstanding when nothing is.** Both the lib header and `main.rs` explain SKIP as "derived fields pending regeneration", while `conformance/v1/regeneration.md` says "Status: all fields are real ... all vectors pass, none skipped. There are no placeholders." The run agrees: 24 passed, 0 failed, 0 skipped. The SKIP mechanism is kept, because a future input change re-opens that state — but it no longer reads as a backlog. All three documents now say the same thing. **And three `cargo doc` warnings were the docs' own fault.** `issuer key = [1;32], agent key = [2;32], steward key = [7;32]` are Rust array literals for the fixed test keys, written without backticks, so rustdoc parsed each as an intra-doc link and could not resolve it. Backticked; `cargo doc --no-deps` for this crate goes from 3 warnings to 0. Verified: pack 24 passed / 0 failed / 0 skipped, tests pass, clippy `-D warnings` clean, `cargo doc` clean. Co-Authored-By: Claude Opus 5 <[email protected]>
590 lines
20 KiB
Rust
590 lines
20 KiB
Rust
//! Joule Code conformance harness (SPEC §8).
|
|
//!
|
|
//! Loads the published wire vectors and runs the five suites: C1 content
|
|
//! addressing, C2 signing, C3 receipt, C4 cascade, and C5 gated cascade
|
|
//! ([`verify_gated_cascade`]). Each vector is reported PASS / FAIL / SKIP, and
|
|
//! any FAIL makes the run non-zero (CI-ready).
|
|
//!
|
|
//! **Nothing is skipped today.** SKIP exists for a positive crypto vector
|
|
//! whose derived field is a placeholder pending regeneration against this
|
|
//! library, and `conformance/v1/regeneration.md` records that every derived
|
|
//! field in the pack is now real — the harness reports 24 passed, 0 failed,
|
|
//! 0 skipped. The mechanism is kept because a future input change re-opens
|
|
//! that state; what it must not do is read as though something were still
|
|
//! outstanding.
|
|
|
|
use std::collections::BTreeMap;
|
|
use std::path::Path;
|
|
|
|
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
|
|
use joule_code_cargo::{Anchor, CargoError, CodingOp, Edit};
|
|
use joule_code_receipt::{Energy, Provenance, Receipt, ReceiptError};
|
|
use joule_code_cascade::Cascade;
|
|
use joule_code_schema::{ContentId, Tier};
|
|
use serde::Deserialize;
|
|
|
|
/// The outcome of a run over one or more suites.
|
|
#[derive(Default, Debug)]
|
|
pub struct Report {
|
|
pub pass: u32,
|
|
pub fail: u32,
|
|
pub skip: u32,
|
|
pub lines: Vec<String>,
|
|
}
|
|
|
|
impl Report {
|
|
fn pass(&mut self, name: &str) {
|
|
self.pass += 1;
|
|
self.lines.push(format!(" PASS {name}"));
|
|
}
|
|
fn fail(&mut self, name: &str, why: &str) {
|
|
self.fail += 1;
|
|
self.lines.push(format!(" FAIL {name}: {why}"));
|
|
}
|
|
fn skip(&mut self, name: &str, why: &str) {
|
|
self.skip += 1;
|
|
self.lines.push(format!(" SKIP {name}: {why}"));
|
|
}
|
|
/// Merge another report into this one.
|
|
pub fn absorb(&mut self, other: Report) {
|
|
self.pass += other.pass;
|
|
self.fail += other.fail;
|
|
self.skip += other.skip;
|
|
self.lines.extend(other.lines);
|
|
}
|
|
}
|
|
|
|
fn is_placeholder(s: &str) -> bool {
|
|
s.contains("PLACEHOLDER")
|
|
}
|
|
|
|
// ---------- JSON mirrors of the vector files ----------
|
|
|
|
#[derive(Deserialize)]
|
|
struct OpVector {
|
|
name: String,
|
|
op: OpJson,
|
|
expected_op_id: Option<String>,
|
|
error: Option<String>,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct OpJson {
|
|
alg: u8,
|
|
prior: String,
|
|
edits: Vec<EditJson>,
|
|
author: String,
|
|
intent: String,
|
|
tier: Tier,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct EditJson {
|
|
path: String,
|
|
anchor: AnchorJson,
|
|
replacement_hex: String,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
enum AnchorJson {
|
|
ByteRange { start: u64, end: u64 },
|
|
Hunk { hunk_id: String },
|
|
}
|
|
|
|
impl OpJson {
|
|
fn to_op(&self) -> Result<CodingOp, String> {
|
|
let mut author = [0u8; 32];
|
|
let bytes = hex::decode(&self.author).map_err(|e| e.to_string())?;
|
|
if bytes.len() != 32 {
|
|
return Err("author key must be 32 bytes".into());
|
|
}
|
|
author.copy_from_slice(&bytes);
|
|
let edits = self
|
|
.edits
|
|
.iter()
|
|
.map(|e| {
|
|
Ok(Edit {
|
|
path: e.path.clone(),
|
|
anchor: match &e.anchor {
|
|
AnchorJson::ByteRange { start, end } => Anchor::ByteRange {
|
|
start: *start,
|
|
end: *end,
|
|
},
|
|
AnchorJson::Hunk { hunk_id } => Anchor::Hunk {
|
|
hunk_id: ContentId(hunk_id.clone()),
|
|
},
|
|
},
|
|
replacement: hex::decode(&e.replacement_hex).map_err(|x| x.to_string())?,
|
|
})
|
|
})
|
|
.collect::<Result<Vec<_>, String>>()?;
|
|
Ok(CodingOp {
|
|
alg: self.alg,
|
|
prior: ContentId(self.prior.clone()),
|
|
edits,
|
|
author,
|
|
intent: self.intent.clone(),
|
|
tier: self.tier,
|
|
})
|
|
}
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct SigVector {
|
|
name: String,
|
|
verifying_key: Option<String>,
|
|
message_hex: Option<String>,
|
|
signature: Option<String>,
|
|
/// Present for op-binding cases: the signature preimage is `op_id ‖ prior`.
|
|
op_id: Option<String>,
|
|
prior: Option<String>,
|
|
expected_verify: bool,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct ReceiptVector {
|
|
name: String,
|
|
receipt: ReceiptJson,
|
|
steward: Option<String>,
|
|
endpoint_verifying_key: Option<String>,
|
|
expected_envelope: Option<String>,
|
|
expected_verify: bool,
|
|
error: Option<String>,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct ReceiptJson {
|
|
op_id: String,
|
|
tier: Tier,
|
|
energy: EnergyJson,
|
|
sequence: u64,
|
|
nonce: String,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct EnergyJson {
|
|
measured_uj: Option<u64>,
|
|
estimated_uj: u64,
|
|
provenance: Provenance,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct CascadeVector {
|
|
name: String,
|
|
stages: Vec<Tier>,
|
|
stage_closes: BTreeMap<String, bool>,
|
|
expected_closing_tier: Option<Tier>,
|
|
}
|
|
|
|
fn parse_tier(s: &str) -> Option<Tier> {
|
|
match s {
|
|
"L0" => Some(Tier::L0),
|
|
"L1" => Some(Tier::L1),
|
|
"L2" => Some(Tier::L2),
|
|
"L3" => Some(Tier::L3),
|
|
"L4" => Some(Tier::L4),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
// Map a CargoError to the stable error token used in the vectors.
|
|
fn cargo_error_token(e: &CargoError) -> &'static str {
|
|
match e {
|
|
CargoError::EditsOverlap => "edits_overlap",
|
|
CargoError::EditsMisordered => "edits_misordered",
|
|
CargoError::UnknownAlgorithm(_) => "unknown_algorithm",
|
|
CargoError::NoEdits => "no_edits",
|
|
_ => "other",
|
|
}
|
|
}
|
|
|
|
fn receipt_error_token(e: &ReceiptError) -> &'static str {
|
|
match e {
|
|
ReceiptError::HwShuntRequiresMeasured => "hwshunt_requires_measured",
|
|
ReceiptError::EstimatorMustNotCarryMeasured => "estimator_must_not_carry_measured",
|
|
_ => "other",
|
|
}
|
|
}
|
|
|
|
// ---------- the four suites ----------
|
|
|
|
/// C1 — content addressing. Recompute `op_id`; honor expected errors.
|
|
pub fn verify_coding_ops(json: &str) -> Result<Report, String> {
|
|
let vectors: Vec<OpVector> = serde_json::from_str(json).map_err(|e| e.to_string())?;
|
|
let mut r = Report::default();
|
|
for v in vectors {
|
|
let op = match v.op.to_op() {
|
|
Ok(op) => op,
|
|
Err(e) => {
|
|
r.fail(&v.name, &format!("malformed vector: {e}"));
|
|
continue;
|
|
}
|
|
};
|
|
match (op.op_id(), v.error.as_deref()) {
|
|
// Expected an error and got the right one.
|
|
(Err(e), Some(expected)) => {
|
|
if cargo_error_token(&e) == expected {
|
|
r.pass(&v.name);
|
|
} else {
|
|
r.fail(&v.name, &format!("wrong error: got {}, want {expected}", cargo_error_token(&e)));
|
|
}
|
|
}
|
|
// Expected an error, got an id.
|
|
(Ok(_), Some(expected)) => r.fail(&v.name, &format!("expected error {expected}, got an op_id")),
|
|
// Expected success.
|
|
(Ok(id), None) => match v.expected_op_id {
|
|
Some(expected) if is_placeholder(&expected) => {
|
|
r.skip(&v.name, &format!("op_id pending regen (computed {id})"));
|
|
}
|
|
Some(expected) => {
|
|
if id.as_str() == expected {
|
|
r.pass(&v.name);
|
|
} else {
|
|
r.fail(&v.name, &format!("op_id mismatch: got {id}, want {expected}"));
|
|
}
|
|
}
|
|
None => r.fail(&v.name, "no expected_op_id and no error"),
|
|
},
|
|
(Err(e), None) => r.fail(&v.name, &format!("unexpected error: {e}")),
|
|
}
|
|
}
|
|
Ok(r)
|
|
}
|
|
|
|
/// C2 — signing. Real Ed25519 known-answer cases run; op-binding cases whose
|
|
/// signature is a placeholder are skipped pending regeneration.
|
|
pub fn verify_signatures(json: &str) -> Result<Report, String> {
|
|
let vectors: Vec<SigVector> = serde_json::from_str(json).map_err(|e| e.to_string())?;
|
|
let mut r = Report::default();
|
|
for v in vectors {
|
|
let (Some(vk_hex), Some(sig_hex)) = (&v.verifying_key, &v.signature) else {
|
|
r.skip(&v.name, "no key/signature");
|
|
continue;
|
|
};
|
|
if is_placeholder(sig_hex) {
|
|
r.skip(&v.name, "signature pending regen");
|
|
continue;
|
|
}
|
|
let ok = (|| -> Result<bool, String> {
|
|
let vk_bytes: [u8; 32] = hex::decode(vk_hex)
|
|
.map_err(|e| e.to_string())?
|
|
.try_into()
|
|
.map_err(|_| "vk len".to_string())?;
|
|
let sig_bytes: [u8; 64] = hex::decode(sig_hex)
|
|
.map_err(|e| e.to_string())?
|
|
.try_into()
|
|
.map_err(|_| "sig len".to_string())?;
|
|
// KAT cases verify a raw message; op-binding cases verify over the
|
|
// preimage `op_id ‖ prior` (joule-code-cargo's signing preimage).
|
|
let msg: Vec<u8> = if let Some(msg_hex) = &v.message_hex {
|
|
hex::decode(msg_hex).map_err(|e| e.to_string())?
|
|
} else if let (Some(op_id), Some(prior)) = (&v.op_id, &v.prior) {
|
|
let mut pre = op_id.as_bytes().to_vec();
|
|
pre.extend_from_slice(prior.as_bytes());
|
|
pre
|
|
} else {
|
|
return Err("vector has neither message_hex nor op_id+prior".into());
|
|
};
|
|
let vk = VerifyingKey::from_bytes(&vk_bytes).map_err(|e| e.to_string())?;
|
|
Ok(vk.verify(&msg, &Signature::from_bytes(&sig_bytes)).is_ok())
|
|
})();
|
|
match ok {
|
|
Ok(got) if got == v.expected_verify => r.pass(&v.name),
|
|
Ok(got) => r.fail(&v.name, &format!("verify={got}, expected {}", v.expected_verify)),
|
|
Err(e) => r.fail(&v.name, &e),
|
|
}
|
|
}
|
|
Ok(r)
|
|
}
|
|
|
|
/// C3 — receipt. The provenance↔measured coupling runs for real; positive
|
|
/// signature reproduction is skipped pending regeneration.
|
|
pub fn verify_receipts(json: &str) -> Result<Report, String> {
|
|
let vectors: Vec<ReceiptVector> = serde_json::from_str(json).map_err(|e| e.to_string())?;
|
|
let mut r = Report::default();
|
|
for v in vectors {
|
|
let energy = Energy {
|
|
measured_uj: v.receipt.energy.measured_uj,
|
|
estimated_uj: v.receipt.energy.estimated_uj,
|
|
provenance: v.receipt.energy.provenance,
|
|
};
|
|
match (energy.check_provenance(), v.error.as_deref()) {
|
|
(Err(e), Some(expected)) => {
|
|
if receipt_error_token(&e) == expected {
|
|
r.pass(&v.name);
|
|
} else {
|
|
r.fail(&v.name, &format!("wrong error: got {}, want {expected}", receipt_error_token(&e)));
|
|
}
|
|
}
|
|
(Ok(()), Some(expected)) => r.fail(&v.name, &format!("expected error {expected}, got valid")),
|
|
(Err(e), None) => r.fail(&v.name, &format!("unexpected provenance error: {e}")),
|
|
(Ok(()), None) => {
|
|
// Coupling holds; verify the JCR-1 COSE_Sign1 envelope for real.
|
|
let Some(env_hex) = v.expected_envelope.as_deref() else {
|
|
r.fail(&v.name, "positive vector missing expected_envelope");
|
|
continue;
|
|
};
|
|
if is_placeholder(env_hex) {
|
|
r.skip(&v.name, "coupling OK; envelope pending regen");
|
|
continue;
|
|
}
|
|
match verify_receipt_envelope(&v, energy, env_hex) {
|
|
Ok(ok) if ok == v.expected_verify => r.pass(&v.name),
|
|
Ok(ok) => r.fail(&v.name, &format!("verify={ok}, expected {}", v.expected_verify)),
|
|
Err(e) => r.fail(&v.name, &e),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Ok(r)
|
|
}
|
|
|
|
/// Reconstruct the [`Receipt`] from a vector and verify its JCR-1 COSE_Sign1
|
|
/// envelope against the endpoint key + steward (SPEC §6).
|
|
fn verify_receipt_envelope(v: &ReceiptVector, energy: Energy, env_hex: &str) -> Result<bool, String> {
|
|
let steward = v.steward.as_deref().ok_or("missing steward")?;
|
|
let vk_hex = v
|
|
.endpoint_verifying_key
|
|
.as_deref()
|
|
.ok_or("missing endpoint_verifying_key")?;
|
|
let vk_bytes: [u8; 32] = hex::decode(vk_hex)
|
|
.map_err(|e| e.to_string())?
|
|
.try_into()
|
|
.map_err(|_| "endpoint key len".to_string())?;
|
|
let envelope = hex::decode(env_hex).map_err(|e| e.to_string())?;
|
|
let nonce: [u8; 8] = hex::decode(&v.receipt.nonce)
|
|
.map_err(|e| e.to_string())?
|
|
.try_into()
|
|
.map_err(|_| "nonce len".to_string())?;
|
|
let receipt = Receipt {
|
|
op_id: ContentId(v.receipt.op_id.clone()),
|
|
tier: v.receipt.tier,
|
|
energy,
|
|
sequence: v.receipt.sequence,
|
|
nonce,
|
|
};
|
|
Ok(receipt.verify_envelope(&envelope, steward, &vk_bytes).is_ok())
|
|
}
|
|
|
|
/// C4 — cascade. Fully checkable: select the closing tier under
|
|
/// stop-at-first-close and compare to the vector.
|
|
pub fn verify_cascade(json: &str) -> Result<Report, String> {
|
|
let vectors: Vec<CascadeVector> = serde_json::from_str(json).map_err(|e| e.to_string())?;
|
|
let mut r = Report::default();
|
|
for v in vectors {
|
|
let closes = |t: Tier| -> bool {
|
|
v.stage_closes
|
|
.iter()
|
|
.any(|(k, &c)| parse_tier(k) == Some(t) && c)
|
|
};
|
|
let got = Cascade::select_closing_tier(&v.stages, closes);
|
|
if got == v.expected_closing_tier {
|
|
r.pass(&v.name);
|
|
} else {
|
|
r.fail(
|
|
&v.name,
|
|
&format!("closing tier got {got:?}, expected {:?}", v.expected_closing_tier),
|
|
);
|
|
}
|
|
}
|
|
Ok(r)
|
|
}
|
|
|
|
// ---------- C5: the JCP-gated cascade ----------
|
|
|
|
#[derive(Deserialize)]
|
|
struct GatedStageVector {
|
|
tier: String,
|
|
closes: bool,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct GatedExpect {
|
|
closing_tiers: Vec<Option<String>>,
|
|
spent_uj: u64,
|
|
verdicts: Vec<String>,
|
|
deny_reasons: Vec<String>,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct GatedVector {
|
|
name: String,
|
|
stages: Vec<GatedStageVector>,
|
|
granted_actions: Vec<String>,
|
|
budget_uj: u64,
|
|
runs: u32,
|
|
expected: GatedExpect,
|
|
}
|
|
|
|
/// Scripted stage for C5: closes (or not) at its tier.
|
|
struct Scripted {
|
|
tier: Tier,
|
|
closes: bool,
|
|
}
|
|
|
|
impl joule_code_cascade::Stage for Scripted {
|
|
fn tier(&self) -> Tier {
|
|
self.tier
|
|
}
|
|
fn try_resolve(
|
|
&self,
|
|
_req: &joule_code_cascade::Request,
|
|
) -> Option<joule_code_cascade::Resolution> {
|
|
if !self.closes {
|
|
return None;
|
|
}
|
|
Some(joule_code_cascade::Resolution {
|
|
op: None,
|
|
answer: Some(format!("closed at {:?}", self.tier)),
|
|
receipt: Receipt {
|
|
op_id: ContentId::of_bytes(b"c5-op"),
|
|
tier: self.tier,
|
|
energy: Energy {
|
|
measured_uj: None,
|
|
estimated_uj: joule_code_jcp::tier_cost_estimate(self.tier),
|
|
provenance: Provenance::Estimator,
|
|
},
|
|
sequence: 0,
|
|
nonce: [0; 8],
|
|
},
|
|
})
|
|
}
|
|
}
|
|
|
|
/// C5 — the JCP-gated cascade (SPEC §5.1). Runs the REAL jcp runtime: per
|
|
/// vector, issue a grant naming `granted_actions` with `budget_uj` over the
|
|
/// fixed fixture identities, wrap the scripted stages with the gate, run the
|
|
/// cascade `runs` times, and compare closing tiers, total debit, and the
|
|
/// sealed receipt verdicts (deny reasons included — a denial is evidence, not
|
|
/// a silent skip).
|
|
///
|
|
/// Fixture constants (third-party implementations must use the same):
|
|
/// issuer key = `[1; 32]`, agent key = `[2; 32]`, steward key = `[7; 32]`,
|
|
/// steward did = "did:example:joule-code",
|
|
/// resource = ContentId::of_bytes(b"repo:joule-code"),
|
|
/// request = { prior: "tree-hash", intent: "rename the function" },
|
|
/// per-tier costs = `tier_cost_estimate` (SPEC §1.1 class markers).
|
|
pub fn verify_gated_cascade(json: &str) -> Result<Report, String> {
|
|
use ed25519_dalek::SigningKey;
|
|
use jcp_schema::{Capability, Did, Grant};
|
|
|
|
let vectors: Vec<GatedVector> = serde_json::from_str(json).map_err(|e| e.to_string())?;
|
|
let mut r = Report::default();
|
|
let issuer = SigningKey::from_bytes(&[1; 32]);
|
|
let agent = SigningKey::from_bytes(&[2; 32]);
|
|
let steward_sk = SigningKey::from_bytes(&[7; 32]);
|
|
let steward = "did:example:joule-code";
|
|
let steward_vk = steward_sk.verifying_key().to_bytes();
|
|
let tree = jcp_schema::ContentId::of_bytes(b"repo:joule-code");
|
|
|
|
'vectors: for v in vectors {
|
|
let grant = Grant {
|
|
issuer: Did::from_verifying_key(&issuer.verifying_key()),
|
|
subject: Did::from_verifying_key(&agent.verifying_key()),
|
|
capability: Capability::new(tree, v.granted_actions.iter().map(String::as_str)),
|
|
budget_uj: v.budget_uj,
|
|
budget_funds: None,
|
|
caveats: Vec::new(),
|
|
expiry: 0,
|
|
parent: None,
|
|
delegate_depth: None,
|
|
nonce: [0; 16],
|
|
};
|
|
let chain = match jcp_grant::issue(&issuer, grant) {
|
|
Ok(sg) => vec![sg],
|
|
Err(e) => {
|
|
r.fail(&v.name, &format!("grant issue: {e:?}"));
|
|
continue;
|
|
}
|
|
};
|
|
let gate = joule_code_jcp::JcpGate::new(
|
|
chain,
|
|
Did::from_verifying_key(&agent.verifying_key()),
|
|
steward_sk.clone(),
|
|
steward,
|
|
);
|
|
let mut stages: Vec<Box<dyn joule_code_cascade::Stage>> = Vec::new();
|
|
for s in &v.stages {
|
|
let Some(tier) = parse_tier(&s.tier.to_uppercase()) else {
|
|
r.fail(&v.name, &format!("bad tier {:?}", s.tier));
|
|
continue 'vectors;
|
|
};
|
|
stages.push(Box::new(gate.wrap(Scripted { tier, closes: s.closes })));
|
|
}
|
|
let cascade = Cascade::new(stages);
|
|
let req = joule_code_cascade::Request {
|
|
prior: "tree-hash".into(),
|
|
intent: "rename the function".into(),
|
|
};
|
|
|
|
let mut closings: Vec<Option<String>> = Vec::new();
|
|
for _ in 0..v.runs {
|
|
closings.push(
|
|
cascade
|
|
.resolve(&req)
|
|
.map(|res| format!("{:?}", res.receipt.tier).to_lowercase()),
|
|
);
|
|
}
|
|
let expected_closings: Vec<Option<String>> = v
|
|
.expected
|
|
.closing_tiers
|
|
.iter()
|
|
.map(|t| t.as_ref().map(|s| s.to_lowercase()))
|
|
.collect();
|
|
if closings != expected_closings {
|
|
r.fail(&v.name, &format!("closings got {closings:?}, want {expected_closings:?}"));
|
|
continue;
|
|
}
|
|
if gate.spent() != v.expected.spent_uj {
|
|
r.fail(&v.name, &format!("spent got {}, want {}", gate.spent(), v.expected.spent_uj));
|
|
continue;
|
|
}
|
|
let mut verdicts = Vec::new();
|
|
let mut deny_reasons = Vec::new();
|
|
for sealed in gate.receipts() {
|
|
match jcp_receipt::extract(&sealed, steward, &steward_vk) {
|
|
Ok(rec) => {
|
|
verdicts.push(rec.verdict.clone());
|
|
if let Some(reason) = rec.deny_reason {
|
|
deny_reasons.push(reason);
|
|
}
|
|
}
|
|
Err(e) => {
|
|
r.fail(&v.name, &format!("receipt extract: {e:?}"));
|
|
continue 'vectors;
|
|
}
|
|
}
|
|
}
|
|
if verdicts != v.expected.verdicts {
|
|
r.fail(&v.name, &format!("verdicts got {verdicts:?}, want {:?}", v.expected.verdicts));
|
|
continue;
|
|
}
|
|
if deny_reasons != v.expected.deny_reasons {
|
|
r.fail(&v.name, &format!("deny reasons got {deny_reasons:?}, want {:?}", v.expected.deny_reasons));
|
|
continue;
|
|
}
|
|
r.pass(&v.name);
|
|
}
|
|
Ok(r)
|
|
}
|
|
|
|
/// Verify a whole pack directory (the five suite files).
|
|
pub fn verify_pack(dir: &Path) -> Result<Report, String> {
|
|
let mut report = Report::default();
|
|
type Suite = (&'static str, fn(&str) -> Result<Report, String>);
|
|
let suites: &[Suite] = &[
|
|
("coding_op_vectors.json", verify_coding_ops),
|
|
("signature_vectors.json", verify_signatures),
|
|
("receipt_vectors.json", verify_receipts),
|
|
("cascade_vectors.json", verify_cascade),
|
|
("gated_cascade_vectors.json", verify_gated_cascade),
|
|
];
|
|
for (file, f) in suites {
|
|
let path = dir.join(file);
|
|
report.lines.push(format!("[{file}]"));
|
|
let json = std::fs::read_to_string(&path)
|
|
.map_err(|e| format!("{}: {e}", path.display()))?;
|
|
report.absorb(f(&json)?);
|
|
}
|
|
Ok(report)
|
|
}
|