mirror of
https://github.com/luxfi/consensus.git
synced 2026-09-24 00:26:58 +00:00
The corpus froze a 47-byte key and both a 95- and a 97-byte proof, but no 49-byte key — so the one width a port could get wrong was the one the standard did not carry. The clause is a width equality, not a lower bound: a decoder that guarded only the short side reads the first 48 bytes of a longer buffer and checks a proof against a PREFIX of what the registrant published. That is a second spelling of an already-seated key, and two spellings of one key is two seats and two votes on one signature. The case is generated by the oracle and self-checked against pop.Verify, like every other row, and lands as an ENCODING refusal before any pairing. Go already held it as a unit assertion and Rust held its own; Rust's is dropped here, because a case the frozen corpus carries for all three does not also want a private restatement that has to be kept in step. Co-authored-by: Hanzo Dev <[email protected]>
85 lines
3.2 KiB
Rust
85 lines
3.2 KiB
Rust
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
//! Rust conforms to the Go oracle, byte for byte.
|
|
//!
|
|
//! `tests/vectors/pop.json` is the frozen corpus from `luxfi/conformance`,
|
|
//! generated by the Go `validator/pop` and self-checked against its verifier.
|
|
//! Every case carries the exact node, key and proof bytes and the accept/reject
|
|
//! the Go verifier returned. This asserts the Rust `pop::verify` returns the
|
|
//! same answer AND, on a reject, the same error class — so a certificate that
|
|
//! verifies in Go verifies here, and one Go refuses is refused here for the same
|
|
//! reason.
|
|
|
|
use lux_consensus::pop::{self, NodeId, PopError};
|
|
|
|
// A tiny JSON reach into the vector, without a serde dependency: the file is a
|
|
// flat, machine-generated object, one case per element.
|
|
fn field<'a>(obj: &'a str, key: &str) -> &'a str {
|
|
let k = format!("\"{key}\": \"");
|
|
let p = obj.find(&k).map(|i| i + k.len()).unwrap_or_else(|| {
|
|
panic!("case has no {key}: {obj}");
|
|
});
|
|
&obj[p..p + obj[p..].find('"').unwrap()]
|
|
}
|
|
|
|
fn unhex(s: &str) -> Vec<u8> {
|
|
(0..s.len())
|
|
.step_by(2)
|
|
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
|
|
.collect()
|
|
}
|
|
|
|
// The three error classes, spelled exactly as the Go oracle publishes them, so a
|
|
// mismatch names which clause diverged.
|
|
fn class(e: Option<PopError>) -> &'static str {
|
|
match e {
|
|
None => "",
|
|
Some(PopError::Key) => "pop: public key is not a canonical compressed BLS12-381 G1 point",
|
|
Some(PopError::Proof) => "pop: proof is not a canonical compressed BLS12-381 G2 point",
|
|
Some(PopError::Possession) => "pop: proof does not bind this node to this key",
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn rust_matches_the_go_pop_oracle_case_for_case() {
|
|
let raw = include_str!("vectors/pop.json");
|
|
|
|
// Split into case objects on the boundary between array elements. The
|
|
// generator writes one object per case; each contains a "name".
|
|
let cases: Vec<&str> = raw.split("\"name\":").skip(1).collect();
|
|
assert!(cases.len() >= 8, "vector shrank to {} cases", cases.len());
|
|
|
|
let mut checked = 0;
|
|
for chunk in cases {
|
|
let name = {
|
|
let s = chunk.trim_start();
|
|
let s = &s[s.find('"').unwrap() + 1..];
|
|
&s[..s.find('"').unwrap()]
|
|
};
|
|
let node_hex = field(chunk, "node");
|
|
let key = unhex(field(chunk, "key"));
|
|
let proof = unhex(field(chunk, "proof"));
|
|
let accepted = chunk.contains("\"accepted\": true");
|
|
|
|
let node_bytes = unhex(node_hex);
|
|
assert_eq!(node_bytes.len(), 20, "{name}: node is not 20 bytes");
|
|
let mut node: NodeId = [0u8; 20];
|
|
node.copy_from_slice(&node_bytes);
|
|
|
|
let got = pop::verify(&node, &key, &proof);
|
|
assert_eq!(
|
|
got.is_ok(),
|
|
accepted,
|
|
"{name}: Rust says accepted={}, Go said accepted={accepted}",
|
|
got.is_ok(),
|
|
);
|
|
if !accepted {
|
|
let want = field(chunk, "error");
|
|
assert_eq!(class(got.err()), want, "{name}: error class diverged");
|
|
}
|
|
checked += 1;
|
|
}
|
|
assert_eq!(checked, 14, "expected 14 frozen cases, checked {checked}");
|
|
}
|