Files
zeekay a53415331f Put the three chains' answers side by side
Every port checked itself against Go in isolation, each one choosing its own
cases. That is how a P-chain fork survived: chains/cpp/platformvm executes the
sovereign-L1 plane and chains/rust/platformvm refuses it by name, and nothing
anywhere put those two answers next to each other.

One corpus, built by the Go P-chain and X-chain themselves — all nineteen
P-chain kinds, the five X-chain kinds, the four block kinds, the sovereign-L1
birth path, and the conservation, ordering and bounds edges a syntactic check
is for. Fifty-three vectors, handed to four evaluators (Go, Rust P and X, C++
P and X) that each print the same seven fields and never see each other's
answers. A runner compares strings and understands no chain; a runner that
understood the rules would be a fourth implementation.

Two files, tab-separated, so the C++ evaluator reads what Go and Rust read
without a JSON library entering a chain's dependency graph. Execution is judged
on a chain that holds nothing, which all three stand up identically; what an
empty ledger still separates is a refusal that never looked at the chain, and
that is the shape of a fork.

It fails on main, which is the point. Thirteen vectors disagree, including both
bugs it was built to catch and three that were not on anyone's list — the
sharpest being that a transaction addressed to another network passes the Rust
P-chain's syntactic check.

The generator is the one place in this repo that depends on luxfi/node, which
is what a reference is; it is a separate Go module so it cannot reach node2's
own graph. And .gitignore's bin/ is now /bin/: unanchored it also matched
chains/rust/*/src/bin, which is where two of the evaluators live.
2026-09-04 18:44:10 -07:00

112 lines
3.9 KiB
Go

// SPDX-License-Identifier: BSD-3-Clause-Eco
// The corpus format, and the result format the three evaluators speak.
//
// Both are line-oriented and tab-separated, for one reason: a C++ evaluator
// must read the same file the Go and Rust ones read, without a JSON library
// entering the C++ chain's dependency graph. One format, three readers, no
// translation between them.
package main
import (
"bufio"
"fmt"
"io"
"strings"
)
// A Vector is one thing to evaluate: bytes, and what to do with them.
//
// V <id> <chain> <op> <wire-hex>
//
// chain is P or X. op is one of:
//
// tx — parse the bytes as a signed transaction, verify, execute
// block — parse the bytes as a block
// seam — no bytes; ask the implementation about a decision-seam method
type Vector struct {
ID string
Chain string
Op string
Wire string
}
// A Result is what one implementation says about one vector.
//
// R <id> <parse> <kind> <hash> <syntactic> <exec> <note>
//
// The first five fields after the id are COMPARED across implementations. The
// note is not: it carries the implementation's own words for what happened,
// so a disagreement can be read without opening three debuggers.
type Result struct {
ID string
Parse string // ok | MALFORMED
Kind string // the tx or block kind's canonical name, or -
Hash string // tx id / block id, lowercase hex, or -
Syntactic string // verdict class
Exec string // verdict class
Note string
}
// The verdict vocabulary. Every implementation maps its own error type into
// exactly one of these before printing, because "failed to fetch UTXO",
// `MissingUtxo` and `kUtxoNotFound` are three spellings of one answer and
// comparing the spellings would report a disagreement that is not one.
//
// LEDGER is deliberately coarse: on the empty chain every vector meets, a
// missing UTXO and a missing validator are both just "the chain does not hold
// this", and splitting them would report which lookup each implementation
// happened to reach first as if it were a difference of opinion. What the
// empty chain DOES separate is UNSUPPORTED — a refusal that never looked at
// the chain at all — from everything that tried.
//
// The mapping is per-implementation and deliberately small; the raw words
// survive in the note beside it, so a mapping that flattened a real difference
// is visible to anyone reading the row.
const (
VOK = "OK" // accepted
VMalformed = "MALFORMED" // the bytes are not a transaction
VSyntactic = "SYNTACTIC" // well-formedness: ordering, zero amounts, field limits
VOverflow = "OVERFLOW" // an arithmetic bound was crossed
VLedger = "LEDGER" // the chain does not hold what the tx needs
VAuth = "AUTH" // a credential did not authorise the spend
VWarp = "WARP" // a warp message did not verify
VUnsupported = "UNSUPPORTED" // the implementation refuses this kind by name
VSkipped = "SKIPPED" // not evaluated at this layer (never a pass)
VInternal = "INTERNAL" // the evaluator itself failed
)
const none = "-"
func (v Vector) String() string {
return fmt.Sprintf("V\t%s\t%s\t%s\t%s", v.ID, v.Chain, v.Op, v.Wire)
}
func (r Result) String() string {
note := strings.ReplaceAll(r.Note, "\t", " ")
note = strings.ReplaceAll(note, "\n", " ")
if note == "" {
note = none
}
return fmt.Sprintf("R\t%s\t%s\t%s\t%s\t%s\t%s\t%s",
r.ID, r.Parse, r.Kind, r.Hash, r.Syntactic, r.Exec, note)
}
func readVectors(r io.Reader) ([]Vector, error) {
var out []Vector
s := bufio.NewScanner(r)
s.Buffer(make([]byte, 1<<20), 1<<26)
for s.Scan() {
line := s.Text()
if line == "" || strings.HasPrefix(line, "#") {
continue
}
f := strings.Split(line, "\t")
if len(f) != 5 || f[0] != "V" {
return nil, fmt.Errorf("corpus line is not a vector: %q", line)
}
out = append(out, Vector{ID: f[1], Chain: f[2], Op: f[3], Wire: f[4]})
}
return out, s.Err()
}