extensions/telemetry-aggregation.md: four measures over session metrics (Prio3SumVec, Prio3Sum, Prio3Histogram), the DAP task's task_info layout and id, the floor of fifty reports each aggregator enforces, the WQS1 statement each aggregator signs, the WQA1 receipt the collector signs, verification, and a room's cell. delivery-telemetry: a class and what it reveals for every CMCD key the mapping writes (§3.4), delivery-only Request mode (§4.3), §8.1 stated as facts with no consent gate, §8.4 pointing to aggregation. live-metering §4.7/§6.5: a room may aggregate its QoE instead of sending logs. wai-rs: HPKE base mode (RFC 9180 A.1.1), DAP-20 messages, a reference client, leader, helper and collector, the receipt verifier, and the corpus generator; live-conformance/aggregate/vectors.json from fixed seeds. Co-Authored-By: Claude Opus 5.5 <[email protected]>
146 lines
8.5 KiB
Rust
146 lines
8.5 KiB
Rust
//! `wai.telemetry.aggregate` (extensions/telemetry-aggregation.md): the committed corpus
|
|
//! `live-conformance/aggregate/vectors.json` regenerates byte for byte from its fixed seeds, the
|
|
//! Prio3 test vectors of `draft-irtf-cfrg-vdaf-22` vendored beside it pass and are the files
|
|
//! as published, every receipt, cell and delivery-only report gets the outcome it states, every
|
|
//! reason the extension adds refuses a vector, the room's aggregated cell is the plaintext cell,
|
|
//! the extension's §11 lists exactly the reasons the reference reports, and delivery-telemetry
|
|
//! §3.4 classes exactly the keys the reference classes.
|
|
#![cfg(feature = "telemetry_aggregate")]
|
|
|
|
use std::collections::BTreeSet;
|
|
use std::path::PathBuf;
|
|
|
|
use sha2::{Digest, Sha256};
|
|
use wai::telemetry::cmcd::key_class;
|
|
use wai::telemetry_aggregate::{corpus, vdaf_vectors, Reason};
|
|
|
|
fn wai_dir() -> PathBuf {
|
|
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..")
|
|
}
|
|
|
|
fn read(p: &str) -> String {
|
|
std::fs::read_to_string(wai_dir().join(p)).unwrap_or_else(|e| panic!("{p}: {e}"))
|
|
}
|
|
|
|
fn committed() -> String {
|
|
read("live-conformance/aggregate/vectors.json")
|
|
}
|
|
|
|
#[test]
|
|
fn the_committed_corpus_regenerates_from_its_seeds() {
|
|
assert!(committed() == corpus::generate(), "live-conformance/aggregate/vectors.json differs from `wai_telemetry_aggregate_vectors gen`");
|
|
let mut files: Vec<String> = std::fs::read_dir(wai_dir().join("live-conformance/aggregate")).unwrap().flatten().map(|e| e.file_name().to_string_lossy().into_owned()).collect();
|
|
files.sort();
|
|
assert_eq!(files, ["README.md", "vdaf-22", "vectors.json"]);
|
|
}
|
|
|
|
/// The draft's vectors, as published at the commit its [TestVectors] reference names: each
|
|
/// file's SHA-256, so that an edit to one is seen.
|
|
const VDAF_FILES: [(&str, &str); 20] = [
|
|
("Prio3Count_0.json", "0701925266d136d90df37dc7e447d91c885ecf2e26ba62ec98811283fd56a4d3"),
|
|
("Prio3Count_1.json", "801b983009c1d262a5bf47154b25007392d865810f38a9da5733addd58c57eb7"),
|
|
("Prio3Count_2.json", "67d5535501d5be79ae06766cd9e94cce8e83fb54a7925ff3a0404338064c8a59"),
|
|
("Prio3Count_bad_gadget_poly.json", "e060c4e267342a84f09b3a27331ac713fff38547f1228c239199cb173059981d"),
|
|
("Prio3Count_bad_helper_seed.json", "b6181bc26554796048fe91c7e2405dd084fadfab1fbc5b531948b46a02ea4e4f"),
|
|
("Prio3Count_bad_meas_share.json", "29fe7518bb85c270783198e924eedcb981267b84577d7dd8b0c4de5fe478efdc"),
|
|
("Prio3Count_bad_wire_seed.json", "4d593e7e5128b8b439b51743ffe74fcc9cfa41d487cefe35da29dde89eb85875"),
|
|
("Prio3Histogram_0.json", "991bafe94fe1725fb4954395cc94d975526626e812ed19db88f120eed5efdd2c"),
|
|
("Prio3Histogram_1.json", "65225dc0b8dff810e8a61630db401ccb82b04b58dc75ae7410ab28327353fc4a"),
|
|
("Prio3Histogram_2.json", "f022b9d68ad4612c8b9270afedcb8e3d86b4e039902c4e90a2ce85278d9a05af"),
|
|
("Prio3Histogram_bad_helper_jr_blind.json", "8c8f4362444ce0a634136c05c0e4466df81679a8d6a5a34f7fd6d775c170b8a8"),
|
|
("Prio3Histogram_bad_leader_jr_blind.json", "cb1c86be8ca9a08692eaae67a8dc1b590f5a85c66eb7bcc09da43a0c3a6ba0b2"),
|
|
("Prio3Histogram_bad_public_share.json", "b27f48e8157a3da7c5fce1f523899931c861f94562096ca5c57299a0d48a2b4e"),
|
|
("Prio3Histogram_bad_verifier_message.json", "b91124fe933c12deb1c62d251cf795f93a6dccf765bc9d6af5390f3cb5e7820b"),
|
|
("Prio3SumVec_0.json", "93a4ae011b547a0915a6099e731660058f1140c537af71e2f0100e61ffa82f9c"),
|
|
("Prio3SumVec_1.json", "cfaf8bf26280c399026009be710d0f196fb48d9c44d2bc7094df7ff593fbd588"),
|
|
("Prio3Sum_0.json", "07c448e3690c8392a8d89c2cc0c298b876f4e59f1220cc49c5b0b28d43bc7965"),
|
|
("Prio3Sum_1.json", "2b4239d0c5e2ba8f9c64f3e51dbcc3fcd1fb7d4f21cdd352d42d4916828e2808"),
|
|
("Prio3Sum_2.json", "4d1b5cd33a5aaee952dc1e8aecc857a1926f92db8d4fdc7031cb032a69ee8ebd"),
|
|
("XofTurboShake128.json", "8f34d9dd964e5320b8db214706dcbf4bf5da4cba95d11a99bd25e73658c6e0ce"),
|
|
];
|
|
|
|
#[test]
|
|
fn the_drafts_prio3_vectors_are_as_published_and_pass() {
|
|
let dir = wai_dir().join("live-conformance/aggregate/vdaf-22");
|
|
let (names, fails) = vdaf_vectors::run_dir(&dir);
|
|
let want: Vec<String> = VDAF_FILES.iter().map(|(n, _)| n.to_string()).collect();
|
|
assert_eq!(names, want);
|
|
for (n, h) in VDAF_FILES {
|
|
let got: String = Sha256::digest(std::fs::read(dir.join(n)).unwrap()).iter().map(|b| format!("{b:02x}")).collect();
|
|
assert_eq!(got, h, "{n} differs from the file as published");
|
|
}
|
|
assert!(fails.is_empty(), "{}", fails.join("\n"));
|
|
}
|
|
|
|
#[test]
|
|
fn every_receipt_cell_and_delivery_only_report_gets_its_stated_outcome() {
|
|
let fails = corpus::verify(&committed());
|
|
assert!(fails.is_empty(), "{}", fails.join("\n"));
|
|
}
|
|
|
|
/// Every reason this extension adds, and every code of another text it reuses, refuses a
|
|
/// vector; the accepted vectors are §12's.
|
|
#[test]
|
|
fn the_corpus_covers_every_reason_and_case() {
|
|
let doc: serde_json::Value = serde_json::from_str(&committed()).unwrap();
|
|
let mut seen: BTreeSet<String> = BTreeSet::new();
|
|
for section in ["receipts", "cells", "delivery_only"] {
|
|
seen.extend(doc[section].as_array().unwrap().iter().filter_map(|v| v["outcome"]["reason"].as_str().map(String::from)));
|
|
}
|
|
for f in doc["flows"].as_array().unwrap() {
|
|
seen.extend(f["job"]["outcomes"].as_array().unwrap().iter().map(|o| o["outcome"].as_str().unwrap().to_string()));
|
|
seen.extend(f["upload_errors"].as_array().unwrap().iter().map(|o| o["error"].as_str().unwrap().to_string()));
|
|
seen.extend(f["refusals"].as_array().unwrap().iter().map(|o| o["outcome"].as_str().unwrap().to_string()));
|
|
}
|
|
for r in Reason::ALL {
|
|
assert!(seen.contains(r.code()), "nothing in the corpus is refused {r}");
|
|
}
|
|
for r in Reason::REUSED {
|
|
assert!(seen.contains(r), "nothing in the corpus is refused {r}");
|
|
}
|
|
let accepted: Vec<&str> = doc["receipts"].as_array().unwrap().iter().filter(|v| v["outcome"]["outcome"] == "accept").map(|v| v["name"].as_str().unwrap()).collect();
|
|
assert_eq!(accepted, ["outcomes", "stalls", "durations", "start_time"]);
|
|
for v in doc["receipts"].as_array().unwrap().iter().take(4) {
|
|
assert!(v["outcome"]["report_count"].as_u64().unwrap() >= 50, "an accepted batch holds at least fifty reports");
|
|
}
|
|
let cell = &doc["cells"][0];
|
|
assert_eq!(cell["outcome"]["outcome"], "accept");
|
|
assert_eq!(cell["outcome"]["cell"], cell["plaintext"], "the room's aggregated cell is the plaintext cell");
|
|
for f in doc["flows"].as_array().unwrap() {
|
|
let rejected: Vec<&str> = f["job"]["outcomes"].as_array().unwrap().iter().map(|o| o["outcome"].as_str().unwrap()).filter(|o| *o != "committed").collect();
|
|
assert!(rejected.contains(&"vdaf_verify_error"), "{}: the out-of-range report's proof fails", f["name"]);
|
|
}
|
|
}
|
|
|
|
/// §11 of the extension lists exactly the codes `Reason::ALL` has, each once, in order.
|
|
#[test]
|
|
fn the_extension_lists_the_reasons() {
|
|
let text = read("extensions/telemetry-aggregation.md");
|
|
let at = text.find("\n## 11. Reasons").expect("a §11");
|
|
let end = text[at + 1..].find("\n## ").map_or(text.len(), |e| at + 1 + e);
|
|
let listed: Vec<String> = text[at..end].lines().filter(|l| l.starts_with("| `")).filter_map(|l| l.split('`').nth(1).map(String::from)).collect();
|
|
let want: Vec<String> = Reason::ALL.iter().map(|r| r.code().to_string()).collect();
|
|
assert_eq!(listed, want, "§11's table and Reason::ALL differ");
|
|
}
|
|
|
|
/// delivery-telemetry §3.4's table states the class of every key §3 and §4 write, as
|
|
/// `key_class` does, and no other.
|
|
#[test]
|
|
fn delivery_telemetry_classes_the_keys_the_reference_does() {
|
|
let text = read("extensions/delivery-telemetry.md");
|
|
let at = text.find("\n§3.4").expect("a §3.4");
|
|
let end = text[at..].find("\n## ").map_or(text.len(), |e| at + e);
|
|
let mut n = 0;
|
|
for l in text[at..end].lines().filter(|l| l.starts_with("| `")) {
|
|
let cells: Vec<&str> = l.split('|').map(str::trim).collect();
|
|
let class = cells[2];
|
|
for k in cells[1].split(", ").map(|k| k.trim_matches('`')) {
|
|
let key = if k.starts_with("wai-") { format!("science.transaction.{k}") } else { k.to_string() };
|
|
assert_eq!(key_class(&key).map(|c| c.name()), Some(class), "§3.4 classes `{k}` {class}");
|
|
n += 1;
|
|
}
|
|
}
|
|
let mapped = ["v", "sid", "sn", "cid", "st", "sf", "ot", "d", "dl", "msd", "bsa", "bsda", "dfa", "ts", "e", "sta", "rc", "ttfb", "ttlb", "url", "cen"];
|
|
let custom = ["b", "tx", "ch", "cap", "ri", "l", "lt", "p", "rcpt", "lc", "lcf", "coh", "u", "pl", "late", "dec", "sw", "co", "cr", "cf", "clm"];
|
|
assert_eq!(n, mapped.len() + custom.len(), "§3.4 classes every key §3 and §4 write, once");
|
|
}
|