mirror of
https://github.com/luxfi/node.git
synced 2026-10-30 15:24:05 +00:00
A vote binds the set it was cast under: the validator-set root is inside the signed message. Two nodes that compute it differently do not disagree about a block — they sign different bytes, so each drops the other's votes as unverifiable and neither can say why. It is the first thing a Go, a Rust and a C++ daemon have to agree on before they can be on one network at all. It is implemented four times: chains.SetRoot here, validators::root and Committee::root in the Rust node, validator_set_root in the C++ node. Every test each of them had compares an implementation to a restatement of the spec in the same language — Go's golden re-implements the encoding in Go, Rust's by_hand in Rust, and the C++ constants were computed by a Python script. Three self-consistent implementations is what the vote/v1-against-v2 fork looked like from the inside. conformance/setroot emits the corpus from the live chains.SetRoot and hands it to the two ports through the runner that already exists. The evaluators reach their node by path, the way this Makefile builds the daemons: an evaluator has to answer for the code this checkout would run. Neither restates the encoding; each calls the one function its node uses, so a program that agreed with itself could not be mistaken for agreement. Twelve vectors, each a place two readings of the same sentence part company: an empty set commits to zero and not to the hash of nothing; one set in two orders is one root; node ids either side of 0x80 sort unsigned; the weight is hashed and does not fit a double; the key is the uncompressed one, and the length prefix in front of it is what keeps two members from running together. The definition is exported for this. It is not private knowledge — every other implementation of a Lux validator has to produce the same 32 bytes or be ignored — and a corpus generated from a copy of it would prove nothing. The reply that publishes a set now says once what it means. The wire carries one weight per validator and the root hashes Light, and the reader here filled in Weight and left Light at zero: a set rebuilt from the document looked right and committed to different bytes than the network did. GetValidatorsAtReply.Set is that reading, in one place, and the corpus's own test holds the document to the set it was published from.
210 lines
5.9 KiB
Go
210 lines
5.9 KiB
Go
// SPDX-License-Identifier: BSD-3-Clause-Eco
|
|
|
|
// The Go side of the cross-language chain differential: it writes the corpus,
|
|
// and it is one of the three evaluators that read it.
|
|
//
|
|
// Two verbs:
|
|
//
|
|
// gen emit <dir> build the corpus from the Go chains and write
|
|
// <dir>/vectors.tsv and <dir>/expected.tsv
|
|
// gen eval [-chain L] <vectors> [reps]
|
|
// read a corpus and print this implementation's
|
|
// verdicts; with a repeat count, do the same work
|
|
// that many times and time it, and with a chain
|
|
// letter answer only that chain
|
|
//
|
|
// `emit` writes expected.tsv by running `eval` over the vectors it just built,
|
|
// so the recorded expectations are not a second opinion typed beside the
|
|
// bytes — they are what the Go chain said when it read them back.
|
|
//
|
|
// This module depends on `luxfi/node`, and nothing else in node2 does. That is
|
|
// deliberate and it is the only place it is allowed: the reference has to be
|
|
// the reference. It is a separate Go module for exactly that reason, so it
|
|
// cannot reach the node's own dependency graph.
|
|
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
func main() {
|
|
if len(os.Args) < 2 {
|
|
usage()
|
|
}
|
|
switch os.Args[1] {
|
|
case "emit":
|
|
dir := "../corpus"
|
|
if len(os.Args) > 2 {
|
|
dir = os.Args[2]
|
|
}
|
|
if err := emit(dir); err != nil {
|
|
fmt.Fprintln(os.Stderr, "emit:", err)
|
|
os.Exit(1)
|
|
}
|
|
case "eval":
|
|
// The differential asks this one program for every chain, because it
|
|
// is the reference for all of them. The benchmark asks for one chain
|
|
// at a time: it times this against ports that are one chain each, and
|
|
// a whole corpus is not a time to put beside a sixth of one.
|
|
args := os.Args[2:]
|
|
chain := ""
|
|
if len(args) > 1 && args[0] == "-chain" {
|
|
chain, args = args[1], args[2:]
|
|
}
|
|
path := "../corpus/vectors.tsv"
|
|
if len(args) > 0 {
|
|
path = args[0]
|
|
}
|
|
if err := eval(path, chain, repeatsArg(args, 1)); err != nil {
|
|
fmt.Fprintln(os.Stderr, "eval:", err)
|
|
os.Exit(1)
|
|
}
|
|
default:
|
|
usage()
|
|
}
|
|
}
|
|
|
|
// repeatsArg reads the optional repeat count at args[i]. Absent, it is 0:
|
|
// evaluate the corpus once and say nothing about how long it took, which is
|
|
// what the differential asks for and what it has always got.
|
|
func repeatsArg(args []string, i int) int {
|
|
if len(args) <= i {
|
|
return 0
|
|
}
|
|
n, err := strconv.Atoi(args[i])
|
|
if err != nil || n < 1 {
|
|
usage()
|
|
}
|
|
return n
|
|
}
|
|
|
|
// What each chain letter names, in the words the ports call themselves on
|
|
// their own timing line — so a row for one chain in three languages reads as
|
|
// three rows about one thing.
|
|
var chainNames = map[string]string{
|
|
"P": "platformvm",
|
|
"X": "xvm",
|
|
"Q": "quantumvm",
|
|
"Z": "zkvm",
|
|
"D": "dexvm",
|
|
"F": "fhevm",
|
|
}
|
|
|
|
func usage() {
|
|
fmt.Fprintln(os.Stderr, "usage: gen emit <corpus-dir> | gen eval [-chain P|X|Q|Z|D|F] <vectors.tsv> [repeats]")
|
|
os.Exit(2)
|
|
}
|
|
|
|
func emit(dir string) error {
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
return err
|
|
}
|
|
vectors := buildCorpus()
|
|
|
|
var v strings.Builder
|
|
v.WriteString("# Lux chain differential corpus.\n")
|
|
v.WriteString("# Generated by conformance/gen from the Go P-chain and X-chain.\n")
|
|
v.WriteString("# V\t<id>\t<chain>\t<op>\t<wire-hex>\n")
|
|
for _, vec := range vectors {
|
|
v.WriteString(vec.String())
|
|
v.WriteByte('\n')
|
|
}
|
|
if err := os.WriteFile(filepath.Join(dir, "vectors.tsv"), []byte(v.String()), 0o644); err != nil {
|
|
return err
|
|
}
|
|
|
|
var e strings.Builder
|
|
e.WriteString("# What the Go chains said when they read the corpus back.\n")
|
|
e.WriteString("# R\t<id>\t<parse>\t<kind>\t<hash>\t<syntactic>\t<exec>\t<note>\n")
|
|
for _, vec := range vectors {
|
|
e.WriteString(evaluate(vec).String())
|
|
e.WriteByte('\n')
|
|
}
|
|
if err := os.WriteFile(filepath.Join(dir, "expected.tsv"), []byte(e.String()), 0o644); err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Printf("%d vectors -> %s\n", len(vectors), dir)
|
|
return nil
|
|
}
|
|
|
|
// eval prints this implementation's verdict for every vector in the corpus,
|
|
// or — given a chain letter — for that chain's vectors and no others. The
|
|
// filter runs before the clock starts, so what is timed is the answering.
|
|
//
|
|
// A repeat count asks for the same work to be done that many times and for the
|
|
// elapsed time of THAT WORK to be reported: the corpus is read before the
|
|
// clock starts and the verdicts are printed after it stops, so what the clock
|
|
// covers is parsing, verification and execution and nothing else. The verdicts
|
|
// are kept rather than dropped, so a round cannot be optimised away, and they
|
|
// are printed once however many rounds ran — the runner's input does not change
|
|
// because someone asked for a time.
|
|
//
|
|
// The timing line goes to stderr, where the runner does not read:
|
|
//
|
|
// B <impl> <vectors> <repeats> <seconds>
|
|
func eval(path, chain string, repeats int) error {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
|
|
vectors, err := readVectors(f)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
name := "go"
|
|
if chain != "" {
|
|
vm, ok := chainNames[chain]
|
|
if !ok {
|
|
return fmt.Errorf("no chain is filed under %q", chain)
|
|
}
|
|
name = "go/" + vm
|
|
kept := vectors[:0]
|
|
for _, v := range vectors {
|
|
if v.Chain == chain {
|
|
kept = append(kept, v)
|
|
}
|
|
}
|
|
// A chain the corpus has nothing for would time an empty loop and
|
|
// report it as a speed.
|
|
if len(kept) == 0 {
|
|
return fmt.Errorf("the corpus holds no %s vector", chain)
|
|
}
|
|
vectors = kept
|
|
}
|
|
|
|
rounds := repeats
|
|
if rounds == 0 {
|
|
rounds = 1
|
|
}
|
|
out := make([]Result, len(vectors))
|
|
start := time.Now()
|
|
for r := 0; r < rounds; r++ {
|
|
for i := range vectors {
|
|
out[i] = evaluate(vectors[i])
|
|
}
|
|
}
|
|
elapsed := time.Since(start)
|
|
|
|
w := bufio.NewWriter(os.Stdout)
|
|
for i := range out {
|
|
fmt.Fprintln(w, out[i].String())
|
|
}
|
|
if err := w.Flush(); err != nil {
|
|
return err
|
|
}
|
|
if repeats > 0 {
|
|
fmt.Fprintf(os.Stderr, "B\t%s\t%d\t%d\t%.6f\n", name, len(vectors), repeats, elapsed.Seconds())
|
|
}
|
|
return nil
|
|
}
|