mirror of
https://github.com/luxfi/node.git
synced 2026-10-30 15:24:05 +00:00
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.
112 lines
3.9 KiB
Go
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()
|
|
}
|