mirror of
https://github.com/luxfi/node.git
synced 2026-10-30 15:24:05 +00:00
Q is the post-quantum lane of the parallel-witness finality model (LP-020), and it was unported in both non-Go runtimes. This is the port, against the node's own seam: `lux_node::vm` is named, never restated, and an id is `lux_consensus::finality::Id`, so a block this chain builds is named the way the node that certifies it names it. Two signatures live here and they are not the same thing. A transaction stamp is one validator's ML-DSA-65 signature over one transaction, bound to the moment it was made, and it rides beside the transaction on the wire. A round witness is a quorum of the committee signing one block, and it is deliberately NOT a field of the block: the block id is the hash of the block's own bytes, so a signature inside it would give two honest nodes two ids for one block. The wire is canonical or nothing. Parse re-encodes what it decoded and refuses anything that does not come back byte-identical, because a ZAP message declares its own size and its own root offset — padding after the content, a relocated root and a root pointed at the header all decode to the same block under different names. The chain is on a disk, not in a map. `Store::commit` takes the whole batch, so the block, its height index entry and the tip pointer move together or not at all; `store::Log` extends that across a crash, dropping a torn tail whole. A read that FAILED is reported as a failure rather than as an empty chain, which is what keeps one transient error from committing genesis over a live tip. Q-Chain sells no blockspace: `issue_tx` refuses every amount (LP-0130 §6), and the chain advances only through consensus-internal aggregation. Held to the reference, not to itself. `tests/vectors/q_reference.json` is five vectors whose every byte came out of the Go chain — its encoder's wires and its parser's verdicts, ids included — and `src/wire.rs` asserts the Go encoder's own bytes for a preimage and an unsigned envelope as constants. `src/bin/conformance.rs` answers the cross-language corpus in the runner's own format; over the four Q vectors, Go and Rust agree on every compared field, including this chain's genesis id. 118 tests, no stubs. Two places are narrower than Go and say so rather than faking: ML-DSA-44 and -87 cannot be signed under here because libluxcrypto exposes only -65 (asking is an error, never a silent downgrade), and batch verification is CPU-parallel because the accelerator's ML-DSA kernel is reachable from Go only. `/bin/` in .gitignore is anchored to the repo root: unanchored it also swallowed `chains/rust/*/src/bin/`, which is where the differential's Rust evaluators live.
185 lines
6.5 KiB
Rust
185 lines
6.5 KiB
Rust
// SPDX-License-Identifier: BSD-3-Clause-Eco
|
|
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
|
|
|
//! The Q-Chain evaluator for the differential harness.
|
|
//!
|
|
//! Two sources, one evaluator.
|
|
//!
|
|
//! `tests/vectors/q_reference.json` is this chain's own golden file, and every
|
|
//! byte of it came out of the GO chain — the wires are what
|
|
//! `chains/quantumvm`'s encoder writes and the verdicts are what its parser
|
|
//! answers, ids included. It travels with this crate, so the Rust side can be
|
|
//! held to the reference with nothing else checked out. Divergence here FAILS.
|
|
//!
|
|
//! `conformance/corpus/chain_differential.json` is the shared corpus the
|
|
//! cross-language harness reads. This evaluator prints a `RESULT` line per Q
|
|
//! vector for `conformance/harness_runner.py` to compare against the other
|
|
//! runtimes. When the corpus is not checked out, that half prints `skipped` and
|
|
//! says why — a corpus that is not there is not a pass.
|
|
//!
|
|
//! The verdict always comes from the chain. The vector's `expected_action`
|
|
//! chooses which OPERATION to run; nothing here ever reads the expected result
|
|
//! before deciding, which is the only way a differential test means anything.
|
|
|
|
use lux_quantumvm::ids;
|
|
use lux_quantumvm::wire;
|
|
|
|
use serde::Deserialize;
|
|
use std::path::{Path, PathBuf};
|
|
|
|
#[derive(Deserialize)]
|
|
struct Vector {
|
|
id: String,
|
|
#[serde(default)]
|
|
chain: String,
|
|
wire_hex: String,
|
|
expected_action: String,
|
|
#[serde(default)]
|
|
go_expectation: Expectation,
|
|
}
|
|
|
|
#[derive(Deserialize, Default)]
|
|
struct Expectation {
|
|
#[serde(default)]
|
|
status: String,
|
|
#[serde(default)]
|
|
tx_id_hex: String,
|
|
#[serde(default)]
|
|
block_id_hex: String,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct Corpus {
|
|
vectors: Vec<Vector>,
|
|
}
|
|
|
|
fn unhex(s: &str) -> Vec<u8> {
|
|
(0..s.len() / 2)
|
|
.map(|i| u8::from_str_radix(&s[i * 2..i * 2 + 2], 16).unwrap())
|
|
.collect()
|
|
}
|
|
|
|
/// What this chain answers for one vector: a status, a detail, and the id it
|
|
/// named the thing by (empty when it named nothing).
|
|
fn evaluate(action: &str, bytes: &[u8]) -> (&'static str, String, String) {
|
|
match action {
|
|
"block_accept" | "block_reject" | "block_wire" => match wire::parse_block(bytes) {
|
|
Ok(block) => (
|
|
"ACCEPTED",
|
|
format!(
|
|
"block height {} over {} transactions",
|
|
block.height,
|
|
block.txs.len()
|
|
),
|
|
ids::hex(&block.id()),
|
|
),
|
|
Err(e) => ("REJECTED", e.to_string(), String::new()),
|
|
},
|
|
// A transaction arrives in one of two shapes: the envelope that rides
|
|
// in a block, or the signature preimage the envelope wraps. Try the
|
|
// envelope first, because it is the shape a peer sends.
|
|
_ => match wire::parse_tx_envelope(bytes) {
|
|
Ok(tx) => (
|
|
"ACCEPTED",
|
|
format!("envelope, nonce {}", tx.nonce),
|
|
ids::hex(&tx.id()),
|
|
),
|
|
Err(envelope_err) => match wire::parse_tx_body(bytes) {
|
|
Ok(tx) => (
|
|
"ACCEPTED",
|
|
format!("preimage, nonce {}", tx.nonce),
|
|
ids::hex(&tx.id()),
|
|
),
|
|
Err(_) => ("REJECTED", envelope_err.to_string(), String::new()),
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
fn manifest(rel: &str) -> PathBuf {
|
|
Path::new(env!("CARGO_MANIFEST_DIR")).join(rel)
|
|
}
|
|
|
|
/// The Go reference, byte for byte and verdict for verdict.
|
|
#[test]
|
|
fn the_chain_answers_what_the_go_chain_answers() {
|
|
let path = manifest("tests/vectors/q_reference.json");
|
|
let raw = std::fs::read_to_string(&path)
|
|
.unwrap_or_else(|e| panic!("the golden file travels with this crate: {path:?}: {e}"));
|
|
let vectors: Vec<Vector> = serde_json::from_str(&raw).expect("golden file");
|
|
assert!(!vectors.is_empty(), "an empty golden file is not a pass");
|
|
|
|
println!("\n=== RUST QUANTUMVM vs THE GO REFERENCE ===");
|
|
for v in &vectors {
|
|
let bytes = unhex(&v.wire_hex);
|
|
let (status, detail, id) = evaluate(&v.expected_action, &bytes);
|
|
println!("{:<24} {status:<9} {detail}", v.id);
|
|
|
|
assert_eq!(
|
|
status, v.go_expectation.status,
|
|
"{}: Rust says {status} ({detail}), the Go chain says {}",
|
|
v.id, v.go_expectation.status
|
|
);
|
|
|
|
// A verdict that agrees while naming a different id is two chains that
|
|
// both accepted and disagree about WHAT they accepted.
|
|
let want = if v.go_expectation.block_id_hex.is_empty() {
|
|
&v.go_expectation.tx_id_hex
|
|
} else {
|
|
&v.go_expectation.block_id_hex
|
|
};
|
|
if !want.is_empty() {
|
|
assert_eq!(&id, want, "{}: the id differs from the Go chain's", v.id);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The shared corpus, as the cross-language harness reads it.
|
|
#[test]
|
|
fn evaluate_the_differential_corpus() {
|
|
let path = manifest("../../../conformance/corpus/chain_differential.json");
|
|
let raw = match std::fs::read_to_string(&path) {
|
|
Ok(raw) => raw,
|
|
Err(e) => {
|
|
// Not a pass, and not a failure of this chain: the corpus is
|
|
// generated into a directory that may not be checked out here.
|
|
println!("skipped: the differential corpus is not at {path:?}: {e}");
|
|
return;
|
|
}
|
|
};
|
|
let corpus: Corpus = serde_json::from_str(&raw).expect("corpus");
|
|
|
|
let mut seen = 0;
|
|
let mut disagree = Vec::new();
|
|
println!("\n=== RUST QUANTUMVM DIFFERENTIAL EVALUATION ===");
|
|
for v in corpus.vectors.iter().filter(|v| v.chain == "Q") {
|
|
seen += 1;
|
|
let bytes = unhex(&v.wire_hex);
|
|
let (status, detail, id) = evaluate(&v.expected_action, &bytes);
|
|
let detail = if id.is_empty() {
|
|
detail
|
|
} else {
|
|
format!("{detail} id={id}")
|
|
};
|
|
// The line the harness reads. Spaces would split the field, so the
|
|
// detail is reported with none.
|
|
println!(
|
|
"RESULT id={} status={status} detail={}",
|
|
v.id,
|
|
detail.replace(' ', "_")
|
|
);
|
|
if !v.go_expectation.status.is_empty() && v.go_expectation.status != status {
|
|
disagree.push(format!(
|
|
"{}: Rust {status} ({detail}), recorded Go {}",
|
|
v.id, v.go_expectation.status
|
|
));
|
|
}
|
|
}
|
|
println!("evaluated {seen} Q vectors");
|
|
assert!(
|
|
disagree.is_empty(),
|
|
"the corpus records a different verdict than this chain reaches:\n {}",
|
|
disagree.join("\n ")
|
|
);
|
|
}
|