Files
zeekay c95d0192e5 The X-chain reads and writes the wire a schema states
chains/schema/xchain.zap says what an X transaction, an X block and each fx
primitive are — the same offsets the chain stated in Rust, and the same ones
the Go chain writes. zapgen turns that into the accessors, so no shape is
spelled out twice.

The runtime goes with them. xvm's own 896-line copy of the reader was byte-
identical to quantumvm's, which is already gone; both now call the emitted
one in chains/rust/zap, so the reader an X node uses is the reader a Go peer
uses.

What is left in wire/ is what an envelope MEANS and not what it is: the two
bytes that name a primitive's family and its shape, the walk over a packed
run of self-delimiting messages, and the gates that decide whether a
threshold over an address set is a quorum anyone could reach. shapes.rs went
from 1019 lines to 474 and containers.rs from 471 to 367 by losing the
offsets, not the rules.

The bytes did not move. The Go-captured golden wires still match, and
tests/corpus_bytes.rs now goes back through the BUILDER for every X vector in
the shared corpus — a transaction re-encoded from the values that were read,
a block rebuilt from those transactions — and compares the result to the hex
the Go chain generated. 19 transactions and 6 blocks; the 10 that carry a
message and nothing else come back byte for byte.

The other nine say something worth stating rather than skipping. A ZAP
message carries its own length, so the corpus deliberately holds vectors with
junk appended past it and one with a byte changed inside padding no field
covers. All are expected to parse and none is what a writer writes, so the
test proves what is true of a writer: it writes the message, it writes the
same message every time, and where a vector IS the message it is the same
bytes. A rewrite longer than its vector, or one that disagrees anywhere
inside the message, fails.

290 unit tests, 12 golden vectors, 7 node tests and the corpus byte test all
pass.
2026-09-06 13:54:25 -07:00

149 lines
6.3 KiB
Rust

// SPDX-License-Identifier: BSD-3-Clause-Eco
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
//! Every X vector in the shared corpus, written back byte for byte.
//!
//! The corpus was generated by the Go chain, so a vector's hex is what Go
//! wrote. Reading one and writing it again has to reproduce it exactly: this
//! chain's writer and Go's are the same writer or they are not. That is what
//! makes the corpus a check on the WRITE path and not only on the read path —
//! a reader can agree about every field of bytes it could never have produced.
//!
//! `Tx::parse` and `Block::parse` are byte-preserving: each keeps the bytes
//! that arrived, so asking either for its bytes would prove nothing. These
//! tests go back through the BUILDER — the transaction is re-encoded from the
//! values that were read, and the block is rebuilt from those transactions.
//!
//! TWO KINDS OF VECTOR CARRY BYTES NOBODY WROTE. A ZAP message says how long
//! it is, so the corpus deliberately includes vectors with junk appended past
//! that length (`…_TRAIL_1`, `…_TRAIL_4`) and one with a byte changed inside
//! the padding no field covers (`X_EDGE_CORRUPT_TAIL`). All of them are
//! expected to PARSE — their corpus verdict is `ok` — and none of them is
//! what this chain would write. So the claim proved here is the one that is
//! true of a writer: what it writes is the message, it writes the same
//! message every time, and where a vector holds a message and nothing else,
//! the two are the same bytes.
use std::path::PathBuf;
use lux_xvm::block::Block;
use lux_xvm::txs::Tx;
use lux_xvm::wire::containers;
fn corpus() -> String {
let p = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../../conformance/corpus/vectors.tsv");
std::fs::read_to_string(&p).unwrap_or_else(|e| panic!("{}: {e}", p.display()))
}
fn unhex(s: &str) -> Option<Vec<u8>> {
if s == "-" || s.len() % 2 != 0 {
return None;
}
(0..s.len() / 2)
.map(|i| u8::from_str_radix(&s[i * 2..i * 2 + 2], 16).ok())
.collect()
}
/// The bytes this chain would write for a transaction it just read.
fn rewritten(tx: &Tx) -> Vec<u8> {
let creds: Vec<Vec<u8>> = tx.creds.iter().map(|c| c.bytes()).collect();
containers::new_signed(&tx.unsigned.bytes(), &creds)
}
/// What a rewrite proved about the vector it came from.
#[derive(PartialEq, Eq, Debug)]
enum Same {
/// The vector is the message, to the byte.
Exactly,
/// The vector is the message plus bytes past its end.
UpToATail,
/// The vector is the message's length with a byte the reader never looks
/// at set to something else.
UpToAByteNoFieldCovers,
}
/// Compare a rewrite to the vector it came from, and say how far they agree.
///
/// A rewrite SHORTER than the vector can only be the vector's message with the
/// junk dropped; anything else — a different length, or a disagreement inside
/// the message — is the bytes moving, and fails here.
fn agreement(id: &str, again: &[u8], vector: &[u8]) -> Same {
assert!(
again.len() <= vector.len(),
"{id}: the rewrite is LONGER than the vector — the bytes moved"
);
let (message, tail) = vector.split_at(again.len());
let differ: Vec<usize> = (0..again.len()).filter(|&i| again[i] != message[i]).collect();
match (differ.as_slice(), tail.is_empty()) {
([], true) => Same::Exactly,
([], false) => Same::UpToATail,
// One byte, the last one the message occupies: the padding the corrupt
// vector names. Two would be a format difference, not a corrupt byte.
([i], true) if *i == again.len() - 1 => Same::UpToAByteNoFieldCovers,
_ => panic!("{id}: bytes moved at {differ:?}"),
}
}
#[test]
fn every_x_vector_is_written_back_exactly_as_it_arrived() {
let text = corpus();
let (mut blocks, mut txs, mut exact) = (0, 0, 0);
for line in text.lines() {
if line.is_empty() || line.starts_with('#') {
continue;
}
let f: Vec<&str> = line.split('\t').collect();
if f.len() < 5 || f[0] != "V" || f[2] != "X" {
continue;
}
let (id, op) = (f[1], f[3]);
let Some(bytes) = unhex(f[4]) else { continue };
match op {
"tx" => {
let Ok(tx) = Tx::parse(&bytes) else { continue };
let again = rewritten(&tx);
if agreement(id, &again, &bytes) == Same::Exactly {
exact += 1;
}
// Whatever the vector carried, the writer settles on one
// answer: writing what was written changes nothing.
let once_more = rewritten(&Tx::parse(&again).expect("the rewrite parses"));
assert_eq!(once_more, again, "{id}: writing twice is not writing once");
txs += 1;
}
"block" => {
let Ok(b) = Block::parse(&bytes) else { continue };
// Every transaction in the block is re-encoded first, so the
// block is rebuilt out of bytes this chain wrote rather than
// out of the ones it was handed.
let mut rebuilt = Vec::with_capacity(b.txs().len());
for tx in b.txs() {
let mut t = Tx::parse(&rewritten(tx)).expect("rewritten tx parses");
t.creds.clone_from(&tx.creds);
rebuilt.push(t);
}
let again =
Block::new(b.parent(), b.height(), b.timestamp(), b.merkle_root(), rebuilt)
.expect("rebuild");
if agreement(id, again.bytes(), &bytes) == Same::Exactly {
exact += 1;
// A block's id is over its bytes, so identical bytes have
// to be identically named.
assert_eq!(again.id(), b.id(), "{id}: block id moved");
}
blocks += 1;
}
_ => {}
}
}
// A corpus that stopped naming X vectors would pass every assertion above.
assert!(txs > 0, "no X transaction vector was read back");
assert!(blocks > 0, "no X block vector was read back");
println!(
"{txs} X transactions and {blocks} X blocks read back; \
{exact} of them are the message and nothing else, written back byte for byte"
);
}