open-standards/wai/wai-rs/tests/conform_result.rs
Transaction Science 4cdae31baf conformance-results: conform-result/1 in the standard, its corpus and a second verifier
extensions/conformance-results.md specifies the record a conformance run signs: its
members (§2) and corpus digest (§2.1), its signing input (the domain
`wai:conform-result` and 0x01, then the record without `sig` in RFC 8785 form) and
Ed25519 signature under SPEC §7's strict rule (§3), what a verifier checks and in what
order (§4: it reads, its version, its members, its signature, its summary, its verdict)
and the codes of each refusal (§5). conform-result/1 is registered as a document kind
(registry export and REGISTRY.md regenerated); SPEC §7 points to it.

wai-rs gains the module conform_result (feature conform_result, in quickstart): sign,
check and verdict, the corpus digest, and the corpus of §7, whose outcomes are stated
from the text and asserted before anything is written. `wai conform` signs with it and
`wai verify` checks with it. conform-result-conformance/ holds accepted records and
refused ones (a case flipped, the corpus digest changed, the summary alone changed, a
bad signature, S past L, a small-order key, a summary or verdict that disagrees though
signed, an unknown version, members missing, added or named twice, a fractional count,
a digest out of form, bytes that are not JSON); wai_conform_result_vectors generates
and verifies it, and conform_result_verify.py, written from the text and sharing no code
with wai-rs, reaches every outcome and fails the corpus with each check of §4 left out.
`wai conform` gains the verifier suite conform-result.

`wai breaking` leaves out an explicit list of corpus files that record a measured run
(the format probes' external decodes, the relay's recorded metering run), each with
its reason, and says so in a note; the vectors built from them stay compared.

A command `wai conform` runs is put in a process group of its own, so a timed-out
command's children are stopped too.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 11:17:05 -04:00

85 lines
4 KiB
Rust

//! Conformance results (extensions/conformance-results.md): the committed corpus
//! (`conform-result-conformance/vectors.json`) regenerates byte for byte, wai-rs gives every
//! vector its stated outcome, every reason of §5 refuses a vector, §5 lists exactly the
//! reference's codes, and a record survives no change after it is signed.
#![cfg(feature = "conform_result")]
use serde_json::{Value, json};
use wai::conform_result::{Reason, check, corpus_document, sign, verdict};
fn wai_dir() -> std::path::PathBuf {
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap().to_path_buf()
}
#[test]
fn the_committed_corpus_regenerates_byte_for_byte() {
let committed = std::fs::read_to_string(wai_dir().join("conform-result-conformance/vectors.json")).expect("the corpus is committed");
assert!(committed == corpus_document(), "conform-result-conformance/vectors.json differs from what wai_conform_result_vectors gen writes");
}
#[test]
fn every_vector_reaches_its_stated_outcome_and_every_reason_is_raised() {
let doc: Value = serde_json::from_str(&corpus_document()).unwrap();
let vs = doc["vectors"].as_array().unwrap();
let mut seen = std::collections::BTreeSet::new();
for v in vs {
let got = verdict(v["record"].as_str().unwrap().as_bytes());
assert_eq!(got, v["expected"].as_str().unwrap(), "{}", v["name"]);
if let Some(r) = got.split("reason=").nth(1) {
seen.insert(r.to_string());
}
}
for r in Reason::ALL {
assert!(seen.contains(r.code()), "no vector is refused with {}", r.code());
}
assert!(vs.iter().any(|v| v["expected"] == "accept conformant=true") && vs.iter().any(|v| v["expected"] == "accept conformant=false"));
}
#[test]
fn section_5_lists_exactly_the_reference_codes() {
let text = std::fs::read_to_string(wai_dir().join("extensions/conformance-results.md")).unwrap();
let section = text.split("\n## 5. Reasons").nth(1).expect("§5").split("\n## ").next().unwrap();
let listed: std::collections::BTreeSet<String> =
section.lines().filter_map(|l| l.strip_prefix("| `")).filter_map(|l| l.split('`').next()).map(String::from).collect();
let codes: std::collections::BTreeSet<String> = Reason::ALL.iter().map(|r| r.code().to_string()).collect();
assert_eq!(listed, codes);
}
/// A record signed, then each leaf of it changed in turn: every change is refused, at the
/// signature or before it.
#[test]
fn no_change_after_signing_survives() {
let key = ed25519_dalek::SigningKey::from_bytes(&[7u8; 32]);
let doc: Value = serde_json::from_str(&corpus_document()).unwrap();
let valid: Value = serde_json::from_str(doc["vectors"][0]["record"].as_str().unwrap()).unwrap();
let mut r = valid.as_object().unwrap().clone();
r.remove("sig");
let r = sign(r, &key).unwrap();
let text = serde_json::to_string(&Value::Object(r.clone())).unwrap();
assert!(check(text.as_bytes()).is_ok());
fn leaves(v: &Value, path: Vec<String>, out: &mut Vec<Vec<String>>) {
match v {
Value::Object(m) => m.iter().for_each(|(k, x)| leaves(x, [path.clone(), vec![k.clone()]].concat(), out)),
Value::Array(a) => a.iter().enumerate().for_each(|(i, x)| leaves(x, [path.clone(), vec![i.to_string()]].concat(), out)),
_ => out.push(path),
}
}
let mut paths = Vec::new();
leaves(&Value::Object(r.clone()), vec![], &mut paths);
for p in paths.iter().filter(|p| p[0] != "sig") {
let mut t = Value::Object(r.clone());
let mut at = &mut t;
for k in p {
at = if let Ok(i) = k.parse::<usize>() { &mut at[i] } else { &mut at[k.as_str()] };
}
*at = match at.clone() {
Value::String(s) => json!(format!("{s}0")),
Value::Bool(b) => json!(!b),
Value::Number(n) => json!(n.as_u64().unwrap() + 1),
Value::Null => json!("x"),
other => other,
};
let got = check(serde_json::to_string(&t).unwrap().as_bytes());
assert!(got.as_ref().is_err_and(|e| e.step <= 4), "{p:?} changed: {got:?}");
}
}