Files
zeekayandHanzo Dev c5207ff169 Two blocks named one vector, so emit wrote nothing
The post-quantum addresses were listed twice -- once as a block of explicit
vectors and once by the loop that reaches the same addresses from a table --
so `emit` stopped at "two vectors named MLKEM_EMPTY" and the corpus could
not be regenerated at all. The loop is the general form and stays; the two
work ids are the only rows the block held that it does not produce.

With the corpus regenerating, the enabled set decides these addresses too.
Five of the post-quantum families are linked into the Go binary and named in
no upgrade.json, so all three runtimes now answer ABSENT at them. The three
the chain does enable -- aiMining, ML-KEM, X-Wing -- are served by Go and by
neither port, which is the differential reporting work rather than a price.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-08 00:13:55 -07:00

241 lines
10 KiB
Go

// SPDX-License-Identifier: BSD-3-Clause-Eco
package main
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
)
// Where geth is, for the vectors borrowed from it: the version this module
// requires, as the module cache holds it. Not a checkout beside this one —
// that names whatever the developer has, which on one machine was two patch
// releases behind the requirement and produced a corpus nobody could
// reproduce from the dependency it claims to borrow from. Here the corpus and
// the reference cannot be different versions, because there is only one.
func gethRoot() (string, error) {
out, err := exec.Command("go", "list", "-m", "-f", "{{.Dir}}", "github.com/luxfi/geth").Output()
if err != nil {
return "", fmt.Errorf("locating geth: %w", err)
}
dir := strings.TrimSpace(string(out))
if dir == "" {
return "", errors.New("locating geth: the module is required but not in the cache")
}
return dir, nil
}
func emit(dir string) error {
root, err := gethRoot()
if err != nil {
return err
}
vectors, err := borrowed(root)
if err != nil {
return err
}
vectors = append(vectors, own()...)
var v bytes.Buffer
v.WriteString("# Lux precompile differential corpus.\n")
v.WriteString("# Generated by conformance/precompile from the Go precompiles.\n")
v.WriteString("# Most of it is borrowed: the inputs geth tests itself against are\n")
v.WriteString("# the inputs the other two implementations should survive.\n")
v.WriteString("# V\t<id>\t<address>\t<gas-limit>\t<input-hex>\n")
seen := map[string]bool{}
for _, vec := range vectors {
if seen[vec.ID] {
return fmt.Errorf("two vectors named %s", vec.ID)
}
seen[vec.ID] = true
v.WriteString(vec.Line() + "\n")
}
if err := os.WriteFile(filepath.Join(dir, "precompile_vectors.tsv"), v.Bytes(), 0o644); err != nil {
return err
}
// The expectations are what Go said when it read the vectors back, not a
// second opinion typed beside them.
var e bytes.Buffer
e.WriteString("# What the Go precompiles said when they read the corpus back.\n")
e.WriteString("# R\t<id>\t<status>\t<gas-charged>\t<output-hex>\t<note>\n")
for _, vec := range vectors {
e.WriteString(run(vec).Line() + "\n")
}
if err := os.WriteFile(filepath.Join(dir, "precompile_expected.tsv"), e.Bytes(), 0o644); err != nil {
return err
}
fmt.Printf("%d vectors\n", len(vectors))
return nil
}
// own is what the borrowed vectors do not reach: the addresses geth ships no
// test data for, the boundary between paying and not paying, and the two
// addresses where the three implementations are known to have made different
// decisions.
func own() []Vector {
const plenty = 1 << 20
v := []Vector{
// The three precompiles with no test data of their own. An empty
// input is a defined call to each of them, not a malformed one.
{ID: "SHA256_EMPTY", Address: addr(0x02), Gas: plenty},
{ID: "RIPEMD160_EMPTY", Address: addr(0x03), Gas: plenty},
{ID: "IDENTITY_EMPTY", Address: addr(0x04), Gas: plenty},
{ID: "SHA256_ABC", Address: addr(0x02), Gas: plenty, Input: []byte("abc")},
{ID: "RIPEMD160_ABC", Address: addr(0x03), Gas: plenty, Input: []byte("abc")},
{ID: "IDENTITY_ABC", Address: addr(0x04), Gas: plenty, Input: []byte("abc")},
// Every precompile is asked for an empty input. Half of them refuse
// it and half of them define it, and which half is which is exactly
// the kind of thing three implementations can differ on quietly.
{ID: "ECRECOVER_EMPTY", Address: addr(0x01), Gas: plenty},
{ID: "MODEXP_EMPTY", Address: addr(0x05), Gas: plenty},
{ID: "BN256ADD_EMPTY", Address: addr(0x06), Gas: plenty},
{ID: "BN256MUL_EMPTY", Address: addr(0x07), Gas: plenty},
{ID: "BN256PAIRING_EMPTY", Address: addr(0x08), Gas: plenty},
{ID: "BLAKE2F_EMPTY", Address: addr(0x09), Gas: plenty},
{ID: "KZG_EMPTY", Address: addr(0x0a), Gas: plenty},
{ID: "BLS_G1ADD_EMPTY", Address: addr(0x0b), Gas: plenty},
{ID: "BLS_G1MULTIEXP_EMPTY", Address: addr(0x0c), Gas: plenty},
{ID: "BLS_G2ADD_EMPTY", Address: addr(0x0d), Gas: plenty},
{ID: "BLS_G2MULTIEXP_EMPTY", Address: addr(0x0e), Gas: plenty},
{ID: "BLS_PAIRING_EMPTY", Address: addr(0x0f), Gas: plenty},
{ID: "BLS_MAPG1_EMPTY", Address: addr(0x10), Gas: plenty},
{ID: "BLS_MAPG2_EMPTY", Address: addr(0x11), Gas: plenty},
// Nothing serves these. An implementation that answers is claiming an
// address the others do not, which is a fork whichever way it falls.
// 0x0 is not among them: the dead-address module (LP-0150) is
// registered there and intercepts transfers to it, which is a claim
// the other two runtimes do not make and cannot be settled without a
// chain to read.
{ID: "UNCLAIMED_12", Address: addr(0x12), Gas: plenty},
{ID: "UNCLAIMED_FF", Address: addr(0xff), Gas: plenty},
{ID: "UNCLAIMED_0101", Address: addr(0x01, 0x01), Gas: plenty},
// 0x0300…0003. The inference module is registered here and no
// network enables it: aiInferenceConfig is in no upgrade.json, and
// the AI key the C-chain does enable is aiMiningConfig, at another
// address. So the call is to an empty account, and an implementation
// that answers is serving a precompile the chain does not have.
{ID: "AIVM_GENERATE", Address: addr(0x03, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x03),
Gas: 1 << 20, Input: aivmGenerate(10, 1, 7, 13, 2)},
{ID: "AIVM_BAD_SELECTOR", Address: addr(0x03, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x03),
Gas: 1 << 20, Input: []byte{0x02, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1}},
// The AI mining precompile, at the address its own range starts
// at. It is not the inference one two lines above: 0x0300…0000
// signs work with ML-DSA, prices it by privacy level, and binds
// a work id to a chain so the same work cannot be minted twice
// on two of them. computeWorkId is the part of it that reads no
// chain, and the two rows below are the same device and the same
// nonce under two chain ids — a binding that holds produces two
// different ids, and one that does not produces one id twice.
// The two work ids. Everything else this block used to list -- the
// empty call here and one to each post-quantum address -- is the
// loop below, which reaches the same addresses from one table.
// Listing them twice named two vectors MLKEM_EMPTY and stopped
// `emit` before it wrote anything.
{ID: "AIMINING_WORKID_96369", Address: aiMining, Gas: plenty, Input: workID(96369)},
{ID: "AIMINING_WORKID_200200", Address: aiMining, Gas: plenty, Input: workID(200200)},
// 0x0100. Two implementations are written for this address and
// neither one runs: the Lux secp256r1 module charges 3450 but no
// network enables secp256r1Config, and geth's p256verify charges
// 6900 but it arrives at Osaka, which is past the Cancun every Lux
// chain ends at. A price here is a price no validator charges.
{ID: "P256_EMPTY", Address: addr(0x01, 0x00), Gas: plenty},
{ID: "P256_WRONG_LENGTH", Address: addr(0x01, 0x00), Gas: plenty, Input: make([]byte, 159)},
{ID: "P256_ZEROES", Address: addr(0x01, 0x00), Gas: plenty, Input: make([]byte, 160)},
}
// The addresses outside the stock range that a Lux chain claims. Whether
// all three runtimes claim them is not a question the borrowed vectors can
// reach — geth ships no test data for an address geth does not serve — and
// an address one runtime answers at and another does not is a fork at that
// address whichever way it falls. Two calls each: the empty input, and a
// word of zeroes, so an implementation that serves the address has
// something to be compared on beyond the fact that it is there.
for _, p := range []struct {
name string
address string
}{
{"MINING", addr(0x03, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)},
{"MLKEM", addr(0x01, 0x22, 0x01)},
{"MLDSA", addr(0x01, 0x22, 0x02)},
{"SLHDSA", addr(0x01, 0x22, 0x03)},
{"PULSAR", addr(0x01, 0x22, 0x04)},
{"P3Q", addr(0x01, 0x22, 0x05)},
{"CORONA", addr(0x01, 0x22, 0x06)},
{"XWING", addr(0x22, 0x21)},
} {
v = append(v,
Vector{ID: p.name + "_EMPTY", Address: p.address, Gas: plenty},
Vector{ID: p.name + "_ZEROES", Address: p.address, Gas: plenty, Input: make([]byte, 32)},
)
}
// The gas boundary, for the fixed-price precompiles: exactly the charge,
// and one below it. An implementation that charges a different price
// crosses this boundary in a different place, which shows up here as a
// verdict rather than as a number nobody reads.
for _, b := range []struct {
id string
address string
cost uint64
input []byte
}{
{"ECRECOVER", addr(0x01), 3000, make([]byte, 128)},
{"SHA256", addr(0x02), 72, []byte("abc")},
{"RIPEMD160", addr(0x03), 720, []byte("abc")},
{"IDENTITY", addr(0x04), 18, []byte("abc")},
} {
v = append(v,
Vector{ID: b.id + "_GAS_EXACT", Address: b.address, Gas: b.cost, Input: b.input},
Vector{ID: b.id + "_GAS_SHORT", Address: b.address, Gas: b.cost - 1, Input: b.input},
)
}
return v
}
// The AI mining precompile's address: 0x03 and nineteen zero bytes. Its range
// runs from the FRONT of the twenty, which is what separates it from the stock
// precompiles at the other end.
var aiMining = addr(0x03, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
// workID builds a computeWorkId call: the selector, a device id, a nonce, and
// the chain the work is being claimed on. The device and the nonce are fixed,
// so two calls differing only in the chain id are the whole test of whether the
// id is bound to a chain.
func workID(chain uint64) []byte {
out := []byte{0x06, 0x00, 0x00, 0x00}
for i := range 32 {
out = append(out, byte(i))
}
for i := range 32 {
out = append(out, byte(0xff-i))
}
var id [8]byte
binary.BigEndian.PutUint64(id[:], chain)
return append(out, id[:]...)
}
// aivmGenerate builds the inference precompile's calldata: the generate
// selector, how many tokens to produce, and the prompt — each a big-endian
// uint32, which is also how the answer comes back.
func aivmGenerate(nNew uint32, prompt ...uint32) []byte {
out := []byte{0x01, 0x00, 0x00, 0x00}
be := func(v uint32) { out = append(out, byte(v>>24), byte(v>>16), byte(v>>8), byte(v)) }
be(nNew)
for _, t := range prompt {
be(t)
}
return out
}