Files
zeekay e72a706541 The Q-chain in Rust: a stamp per transaction, a quorum per block, on a disk
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.
2026-09-04 20:41:35 -07:00

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 ")
);
}