mirror of
https://github.com/luxfi/chains.git
synced 2026-09-24 00:26:00 +00:00
evm, oraclevm, relayvm: a vmID nobody compared, and a policy no rollback could reach
The two shims re-exported luxfi/oracle's and luxfi/relay's private vmID literals. constants.OracleVMID and constants.RelayVMID exist, agree with them, and nothing made them agree. This repo builds the plugin binary whose FILENAME is that id, so the shims now name the constant and their tests compare all three; a drift upstream fails here instead of producing a chain no node starts. cevm gets ErrNotLinked, so a caller can tell "this build has no C++ EVM" from "this input is bad". cevm/parallel's sender-recovery step returned a hard error for the first, which in a build without the library is every block, forever, and CEVM_STRICT=0 -- the documented rollback to the Go EVM -- did not reach that line. It now leaves by declineBlock like every other capability refusal. Its receipt gate asked whether a transaction carried calldata. A contract creation with empty init code carries none, so it was receipted as a value transfer: no ContractAddress, and 21000 intrinsic gas against creation's 53000, into the cumulative total the header commits to. ExecuteBlock splits into shape / dispatch / assemble so the parity-critical half needs no library to exercise. strictGPUEVM loses the sync.Once that fixed its answer at the first decline and left both branches unreachable from one process. The native build referenced gpu_execute_block and gpu_free_result, which ABI v6 removed, so nothing behind lux_cevm_native compiled at all. The three plugins print the version the VM reports rather than a second literal beside it, and the vmID the binary must be installed under: a plugin under the wrong filename is invisible and the chain simply never appears. cevm/plugin.go is gone. It located a binary at ~/work/luxcpp/evm/build/bin/cevm, a path that has never existed -- the tree is luxcpp/cevm -- claimed the lux CLI and universe Makefiles used it, which neither does, and returned "cevm" from a function called VMID. Its two tests skipped and asserted nothing. .gitignore's bare `plugin` matched every <vm>/cmd/plugin DIRECTORY, so a new file under one could not be added: git printed a hint and staged nothing. mpcvm's and bridgevm's plugin tests are written, pass, and are not in the repository. Coverage: evm 0.0 -> 85.0, evm/cevm 71.1 -> 100.0, evm/cevm/parallel 0.0 -> 86.1, oraclevm 0.0 -> 90.0, oraclevm/cmd/plugin 0.0 -> 72.2, relayvm/cmd/plugin 0.0 -> 72.2.
This commit is contained in:
+6
-1
@@ -2,5 +2,10 @@ build/
|
||||
|
||||
# stray local VM build artifacts (never commit binaries)
|
||||
/aivm-gov-mcp
|
||||
plugin
|
||||
# Rooted: `go build ./<vm>/cmd/plugin` from here drops a `plugin` binary in the
|
||||
# repo root. Unrooted, this pattern also matched every `<vm>/cmd/plugin`
|
||||
# DIRECTORY, so a new file under one was silently unaddable — `git add` printed
|
||||
# a hint and staged nothing. mpcvm's and bridgevm's plugin tests were written,
|
||||
# pass, and are not in the repository.
|
||||
/plugin
|
||||
.DS_Store
|
||||
|
||||
+17
-7
@@ -31,12 +31,22 @@ func selectExecutionBackend(logger log.Logger) {
|
||||
"cevm-backends", cevm.AvailableBackends(),
|
||||
)
|
||||
for _, h := range cevm.Health() {
|
||||
if h.OK {
|
||||
logger.Info("cevm backend healthy",
|
||||
"backend", h.Name, "probes", h.ProbesRun, "gas", h.GasUsed)
|
||||
continue
|
||||
}
|
||||
logger.Warn("cevm backend not healthy",
|
||||
"backend", h.Name, "probe", h.Probe, "err", h.Err)
|
||||
report(logger, h)
|
||||
}
|
||||
}
|
||||
|
||||
// report says what one cevm backend answered to the health battery.
|
||||
//
|
||||
// It is separate from the selection because it is the only part that depends
|
||||
// on what the health check found: a build with no library linked has exactly
|
||||
// one report and it is never healthy, so a check folded into the loop above
|
||||
// could only ever be read one way.
|
||||
func report(logger log.Logger, h cevm.HealthReport) {
|
||||
if h.OK {
|
||||
logger.Info("cevm backend healthy",
|
||||
"backend", h.Name, "probes", h.ProbesRun, "gas", h.GasUsed)
|
||||
return
|
||||
}
|
||||
logger.Warn("cevm backend not healthy",
|
||||
"backend", h.Name, "probe", h.Probe, "err", h.Err)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
//go:build cgo
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/chains/evm/cevm"
|
||||
"github.com/luxfi/log"
|
||||
)
|
||||
|
||||
// A backend that answered the health battery is reported at info; one that did
|
||||
// not is reported at warn, carrying the probe that failed and the reason.
|
||||
//
|
||||
// The level is the whole point: an operator's alerting reads it, and a backend
|
||||
// that cannot execute a block reported at info is a chain that will not run
|
||||
// with nothing raised about it.
|
||||
func TestAnUnhealthyBackendIsReportedAtWarnWithItsReason(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := log.NewWriter(&buf)
|
||||
|
||||
report(logger, cevm.HealthReport{
|
||||
Backend: cevm.GPUCUDA,
|
||||
Name: "gpu-cuda",
|
||||
OK: false,
|
||||
Probe: "storage",
|
||||
Err: errors.New("kernel did not launch"),
|
||||
})
|
||||
|
||||
got := lines(t, &buf)
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("one report produced %d log lines", len(got))
|
||||
}
|
||||
if lvl := strings.ToLower(str(got[0]["level"])); lvl != "warn" {
|
||||
t.Errorf("an unhealthy backend was logged at %q, want warn", lvl)
|
||||
}
|
||||
for k, want := range map[string]string{"backend": "gpu-cuda", "probe": "storage"} {
|
||||
if str(got[0][k]) != want {
|
||||
t.Errorf("log field %q = %q, want %q", k, str(got[0][k]), want)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(str(got[0]["err"]), "kernel did not launch") {
|
||||
t.Errorf("log does not carry the reason: %v", got[0]["err"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestAHealthyBackendIsReportedAtInfo(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := log.NewWriter(&buf)
|
||||
|
||||
report(logger, cevm.HealthReport{
|
||||
Backend: cevm.CPUParallel,
|
||||
Name: "cpu-parallel",
|
||||
OK: true,
|
||||
ProbesRun: 5,
|
||||
GasUsed: 105000,
|
||||
})
|
||||
|
||||
got := lines(t, &buf)
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("one report produced %d log lines", len(got))
|
||||
}
|
||||
if lvl := strings.ToLower(str(got[0]["level"])); lvl != "info" {
|
||||
t.Errorf("a healthy backend was logged at %q, want info", lvl)
|
||||
}
|
||||
if str(got[0]["backend"]) != "cpu-parallel" {
|
||||
t.Errorf("log names backend %q", got[0]["backend"])
|
||||
}
|
||||
}
|
||||
|
||||
// This build links no native library, so the one report it has says so — and
|
||||
// says the right thing. It used to blame CGo, which is enabled here; an
|
||||
// operator who followed that checked CGO_ENABLED and found it already 1.
|
||||
func TestThisBuildReportsWhyItCannotExecute(t *testing.T) {
|
||||
reports := cevm.Health()
|
||||
if len(reports) == 0 {
|
||||
t.Fatal("Health() said nothing at all")
|
||||
}
|
||||
for _, h := range reports {
|
||||
if h.OK {
|
||||
continue
|
||||
}
|
||||
if !errors.Is(h.Err, cevm.ErrNotLinked) {
|
||||
t.Errorf("backend %q is not healthy and blames %v, want cevm.ErrNotLinked",
|
||||
h.Name, h.Err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func lines(t *testing.T, buf *bytes.Buffer) []map[string]any {
|
||||
t.Helper()
|
||||
var out []map[string]any
|
||||
for _, l := range strings.Split(strings.TrimSpace(buf.String()), "\n") {
|
||||
if l == "" {
|
||||
continue
|
||||
}
|
||||
var m map[string]any
|
||||
if err := json.Unmarshal([]byte(l), &m); err != nil {
|
||||
t.Fatalf("log line %q is not JSON: %v", l, err)
|
||||
}
|
||||
out = append(out, m)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func str(v any) string {
|
||||
s, _ := v.(string)
|
||||
return s
|
||||
}
|
||||
+16
-1
@@ -55,7 +55,22 @@
|
||||
// memory, and the call bridge) without error.
|
||||
package cevm
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ErrNotLinked is what every entry point that needs the native luxcpp library
|
||||
// returns when this binary was built without it — the default build, and any
|
||||
// build on a host lacking the lux-cevm bundle.
|
||||
//
|
||||
// It is a value rather than a message because a caller has to tell it apart
|
||||
// from a bad input, and the two lead to opposite decisions: a build that
|
||||
// cannot execute declines the block to whatever else can, while a block that
|
||||
// cannot be executed is a hard failure. A caller that could only read the
|
||||
// message either treated a missing library as a corrupt block, or — worse —
|
||||
// fell back on a signature the library had rejected.
|
||||
var ErrNotLinked = errors.New("cevm: native EVM not linked (rebuild with CGO_ENABLED=1 -tags=lux_cevm_native)")
|
||||
|
||||
// Backend selects the C++ EVM execution mode.
|
||||
// BlockResult extends BlockResult with the V2 ABI fields: per-tx status
|
||||
|
||||
@@ -2,10 +2,7 @@
|
||||
|
||||
package cevm
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
)
|
||||
import "testing"
|
||||
|
||||
// TestBlockContextChainID is the canonical end-to-end check that the
|
||||
// dispatcher forwards Config.block_context to whichever backend actually
|
||||
@@ -120,14 +117,3 @@ func TestBlockContextChainID(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// encodeBE32 is a helper used by future test extensions that decode RETURN
|
||||
// data once we wire output bytes through BlockResult. Kept here so the
|
||||
// chain id big-endian encoding stays in one place.
|
||||
func encodeBE32(v uint64) [32]byte {
|
||||
var b [32]byte
|
||||
binary.BigEndian.PutUint64(b[24:32], v)
|
||||
return b
|
||||
}
|
||||
|
||||
var _ = encodeBE32 // not yet used; reserved for output-bytes verification
|
||||
|
||||
@@ -206,7 +206,7 @@ func ExecuteBlock(backend Backend, numThreads uint32, txs []Transaction, ctx *Bl
|
||||
}
|
||||
}
|
||||
|
||||
result := C.gpu_execute_block(
|
||||
result := C.gpu_execute_block_v4(
|
||||
&ctxs[0],
|
||||
C.uint32_t(len(ctxs)),
|
||||
C.uint8_t(backend),
|
||||
@@ -218,7 +218,7 @@ func ExecuteBlock(backend Backend, numThreads uint32, txs []Transaction, ctx *Bl
|
||||
codePtr,
|
||||
C.uint32_t(codeSize),
|
||||
)
|
||||
defer C.gpu_free_result(&result)
|
||||
defer C.gpu_free_result_v2(&result)
|
||||
runtime.KeepAlive(ctxs)
|
||||
runtime.KeepAlive(cctxStorage)
|
||||
runtime.KeepAlive(cAccts)
|
||||
|
||||
+12
-6
@@ -12,13 +12,14 @@ import "fmt"
|
||||
// No library linked, so there is no ABI to report.
|
||||
const ABIVersion uint32 = 0
|
||||
|
||||
// AutoDetect returns CPUSequential when built without CGo.
|
||||
// AutoDetect returns CPUSequential: with no library there is no lane to detect.
|
||||
func AutoDetect() Backend { return CPUSequential }
|
||||
|
||||
// AvailableBackends returns CPUSequential only when built without CGo.
|
||||
// AvailableBackends returns CPUSequential alone — the only lane a build with no
|
||||
// library has, and it cannot execute either.
|
||||
func AvailableBackends() []Backend { return []Backend{CPUSequential} }
|
||||
|
||||
// BackendName uses the local Go-side string when CGo is off.
|
||||
// BackendName uses the local Go-side string; there is no library to ask.
|
||||
func BackendName(b Backend) string { return b.String() }
|
||||
|
||||
// LibraryABIVersion returns the Go-side constant when there's no library.
|
||||
@@ -32,7 +33,7 @@ func ExecuteBlock(backend Backend, numThreads uint32, txs []Transaction, ctx *Bl
|
||||
}
|
||||
_ = ctx
|
||||
_ = state
|
||||
return nil, fmt.Errorf("cevm: native EVM not linked, cannot execute transactions (rebuild with CGO_ENABLED=1 -tags=lux_cevm_native)")
|
||||
return nil, fmt.Errorf("cevm: cannot execute %d transactions: %w", len(txs), ErrNotLinked)
|
||||
}
|
||||
|
||||
// HealthProbeResult mirrors the cgo build's struct so consumers see the same
|
||||
@@ -61,12 +62,17 @@ type HealthReport struct {
|
||||
ExecTime float64
|
||||
}
|
||||
|
||||
// Health returns a single non-OK report indicating CGo is disabled.
|
||||
// Health returns a single non-OK report naming the reason nothing can execute.
|
||||
//
|
||||
// The reason used to read "built without CGo", which is wrong on the default
|
||||
// build: cgo is on, and what is missing is the lux_cevm_native tag and the
|
||||
// luxcpp bundle behind it. An operator reading it went and checked
|
||||
// CGO_ENABLED, which was already 1.
|
||||
func Health() []HealthReport {
|
||||
return []HealthReport{{
|
||||
Backend: CPUSequential,
|
||||
Name: CPUSequential.String(),
|
||||
OK: false,
|
||||
Err: fmt.Errorf("cevm: built without CGo, no backends executable"),
|
||||
Err: ErrNotLinked,
|
||||
}}
|
||||
}
|
||||
|
||||
@@ -25,5 +25,5 @@ func BatchRecoverSenders(txs types.Transactions, signer types.Signer) ([]common.
|
||||
return nil, nil
|
||||
}
|
||||
_ = signer
|
||||
return nil, fmt.Errorf("cevm: BatchRecoverSenders requires CGO_ENABLED=1 -tags=lux_cevm_native (libsecp256k1_cpu)")
|
||||
return nil, fmt.Errorf("cevm: BatchRecoverSenders needs libsecp256k1_cpu: %w", ErrNotLinked)
|
||||
}
|
||||
|
||||
+216
-95
@@ -1,11 +1,21 @@
|
||||
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package cevm
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"unsafe"
|
||||
|
||||
// Tests in this file run under both `cgo` and `!cgo` builds. They cover the
|
||||
// parts of the API that don't depend on the C++ library: enum stringers,
|
||||
// constant exports, and the empty-input fast-paths that ExecuteBlock /
|
||||
// ExecuteBlock promise to return without error.
|
||||
"github.com/luxfi/geth/core/types"
|
||||
)
|
||||
|
||||
// Tests in this file run under every build. They cover the parts of the API
|
||||
// that do not depend on the C++ library: the enum stringers, the wire layout
|
||||
// the C ABI reads by memcpy, and — the part that matters — what the package
|
||||
// does when the native library is NOT linked, which is the default build and
|
||||
// the only one that works on a host without the lux-cevm bundle.
|
||||
|
||||
func TestBackendString(t *testing.T) {
|
||||
tests := []struct {
|
||||
@@ -46,107 +56,218 @@ func TestTxStatusString(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestVMID(t *testing.T) {
|
||||
if got := VMID(); got != "cevm" {
|
||||
t.Errorf("VMID() = %q, want %q", got, "cevm")
|
||||
}
|
||||
}
|
||||
// -----------------------------------------------------------------------------
|
||||
// Refusing, rather than pretending to execute
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
func TestPluginPath(t *testing.T) {
|
||||
p := PluginPath()
|
||||
if p == "" {
|
||||
t.Skip("could not determine home directory")
|
||||
}
|
||||
const suffix = "work/luxcpp/evm/build/bin/cevm"
|
||||
if len(p) < len(suffix) {
|
||||
t.Fatalf("PluginPath() too short: %q", p)
|
||||
}
|
||||
got := p[len(p)-len(suffix):]
|
||||
if got != suffix {
|
||||
t.Errorf("PluginPath() suffix = %q, want %q", got, suffix)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginExists(t *testing.T) {
|
||||
// This test just verifies the function runs without panicking.
|
||||
// Whether it returns true or false depends on the build environment.
|
||||
_ = PluginExists()
|
||||
}
|
||||
|
||||
// TestExecuteBlockEmpty pins the contract for a block with no transactions:
|
||||
// no error, no gas, and the ABI the linked library reports.
|
||||
func TestExecuteBlockEmpty(t *testing.T) {
|
||||
result, err := ExecuteBlock(CPUSequential, 0, nil, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("ExecuteBlock(nil): %v", err)
|
||||
}
|
||||
if result.TotalGas != 0 {
|
||||
t.Errorf("TotalGas = %d, want 0 for an empty block", result.TotalGas)
|
||||
}
|
||||
if result.ABIVersion != ABIVersion {
|
||||
t.Errorf("ABIVersion = %d, want %d", result.ABIVersion, ABIVersion)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAvailableBackends_NonEmpty: every build must expose at least one backend.
|
||||
// Under nocgo, that's CPUSequential. Under cgo, it's whatever the loaded
|
||||
// library reports — but never zero.
|
||||
func TestAvailableBackends_NonEmpty(t *testing.T) {
|
||||
bs := AvailableBackends()
|
||||
if len(bs) == 0 {
|
||||
t.Fatal("AvailableBackends() returned empty list")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackendName_NonEmpty(t *testing.T) {
|
||||
for _, b := range []Backend{CPUSequential, CPUParallel, GPUMetal, GPUCUDA} {
|
||||
if BackendName(b) == "" {
|
||||
t.Errorf("BackendName(%d) is empty", int(b))
|
||||
// The one property that matters when the native EVM is not linked: a block
|
||||
// with transactions in it must NOT come back as a result.
|
||||
//
|
||||
// A successful-looking BlockResult here is a block whose transactions were
|
||||
// never applied. The caller writes the state root and the gas, consensus
|
||||
// accepts it, and the chain has diverged from every node that did link the
|
||||
// library — silently, because nothing errored. So the refusal is the
|
||||
// consensus-relevant behaviour, not a convenience.
|
||||
func TestABlockWithTransactionsIsRefusedRatherThanFaked(t *testing.T) {
|
||||
for _, backend := range []Backend{CPUSequential, CPUParallel, GPUMetal, GPUCUDA} {
|
||||
result, err := ExecuteBlock(backend, 4, []Transaction{{GasLimit: 21000}}, nil, nil)
|
||||
if err == nil {
|
||||
t.Fatalf("ExecuteBlock(%s, 1 tx) returned no error; a block that was never "+
|
||||
"executed must not come back as a result", backend)
|
||||
}
|
||||
if result != nil {
|
||||
t.Errorf("ExecuteBlock(%s, 1 tx) returned a result (%+v) alongside its refusal",
|
||||
backend, result)
|
||||
}
|
||||
// The message has to say what to do about it: an operator reading a
|
||||
// bare "not supported" cannot tell a missing build tag from a missing
|
||||
// GPU.
|
||||
if !strings.Contains(err.Error(), "lux_cevm_native") {
|
||||
t.Errorf("ExecuteBlock error %q does not name the build tag that fixes it", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestHealth_AlwaysReturnsReports: Health() must never return nil. Under
|
||||
// nocgo it returns a single non-OK report. Under cgo it returns one report
|
||||
// per available backend.
|
||||
func TestHealth_AlwaysReturnsReports(t *testing.T) {
|
||||
// A block context and a state snapshot do not change the refusal. They are the
|
||||
// V4 arguments; accepting them and then not executing would be the same
|
||||
// divergence with more ceremony.
|
||||
func TestContextAndSnapshotDoNotBuyExecution(t *testing.T) {
|
||||
ctx := &BlockContext{ChainID: 96369, Number: 7}
|
||||
snapshot := []StateAccount{{Nonce: 1}}
|
||||
|
||||
if _, err := ExecuteBlock(GPUCUDA, 8, []Transaction{{GasLimit: 21000}}, ctx, snapshot); err == nil {
|
||||
t.Fatal("ExecuteBlock accepted a block once it was handed a context and a snapshot")
|
||||
}
|
||||
}
|
||||
|
||||
// An empty block is the documented exception: there is nothing to execute, so
|
||||
// there is nothing to get wrong, and the caller gets the ABI the linked library
|
||||
// reports rather than an error it would have to special-case.
|
||||
func TestAnEmptyBlockIsTheOneThingThatCanBeAnswered(t *testing.T) {
|
||||
for _, txs := range [][]Transaction{nil, {}} {
|
||||
result, err := ExecuteBlock(CPUSequential, 0, txs, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("ExecuteBlock(%v): %v", txs, err)
|
||||
}
|
||||
if result == nil {
|
||||
t.Fatal("ExecuteBlock returned neither a result nor an error")
|
||||
}
|
||||
if result.TotalGas != 0 {
|
||||
t.Errorf("TotalGas = %d, want 0 for an empty block", result.TotalGas)
|
||||
}
|
||||
if len(result.GasUsed) != 0 || len(result.Status) != 0 {
|
||||
t.Errorf("empty block reported %d gas entries and %d statuses",
|
||||
len(result.GasUsed), len(result.Status))
|
||||
}
|
||||
if result.ABIVersion != ABIVersion {
|
||||
t.Errorf("ABIVersion = %d, want %d", result.ABIVersion, ABIVersion)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BatchRecoverSenders is the same shape: no transactions is not a failure, and
|
||||
// any transaction is a refusal. Its doc tells the caller to fall back to
|
||||
// per-tx types.Sender, which is only actionable if the caller can tell the two
|
||||
// apart — so the empty case must not error.
|
||||
func TestBatchRecoveryRefusesWorkAndNotEmptiness(t *testing.T) {
|
||||
signer := types.LatestSignerForChainID(nil)
|
||||
|
||||
senders, err := BatchRecoverSenders(nil, signer)
|
||||
if err != nil {
|
||||
t.Fatalf("BatchRecoverSenders(nil) = %v, want no error", err)
|
||||
}
|
||||
if senders != nil {
|
||||
t.Errorf("BatchRecoverSenders(nil) returned %d senders", len(senders))
|
||||
}
|
||||
|
||||
txs := types.Transactions{types.NewTx(&types.LegacyTx{Gas: 21000})}
|
||||
senders, err = BatchRecoverSenders(txs, signer)
|
||||
if err == nil {
|
||||
t.Fatal("BatchRecoverSenders returned no error with the native library unlinked; " +
|
||||
"a caller would read the zero addresses as recovered senders")
|
||||
}
|
||||
if senders != nil {
|
||||
t.Errorf("BatchRecoverSenders returned %d senders alongside its refusal", len(senders))
|
||||
}
|
||||
if !strings.Contains(err.Error(), "lux_cevm_native") {
|
||||
t.Errorf("BatchRecoverSenders error %q does not name the build tag that fixes it", err)
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// What this build reports about itself
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// Every build exposes at least one backend and names it. A caller that reads
|
||||
// an empty list has no lane to dispatch to and no way to say why.
|
||||
func TestThisBuildReportsExactlyTheBackendItCanRun(t *testing.T) {
|
||||
got := AvailableBackends()
|
||||
if len(got) == 0 {
|
||||
t.Fatal("AvailableBackends() returned an empty list")
|
||||
}
|
||||
for _, b := range got {
|
||||
if BackendName(b) == "" {
|
||||
t.Errorf("BackendName(%d) is empty", int(b))
|
||||
}
|
||||
}
|
||||
// AutoDetect must pick from the list it just published, not from the enum.
|
||||
auto := AutoDetect()
|
||||
found := false
|
||||
for _, b := range got {
|
||||
if b == auto {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("AutoDetect() = %s, which is not in AvailableBackends() = %v", auto, got)
|
||||
}
|
||||
}
|
||||
|
||||
// The name is the Backend's own String under this build — there is no library
|
||||
// to ask — so the two cannot drift into two vocabularies for one lane.
|
||||
func TestBackendNameAgreesWithTheStringer(t *testing.T) {
|
||||
for _, b := range []Backend{CPUSequential, CPUParallel, GPUMetal, GPUCUDA, Backend(99)} {
|
||||
if got, want := BackendName(b), b.String(); got != want {
|
||||
t.Errorf("BackendName(%d) = %q, Backend.String() = %q", int(b), got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The ABI the Go module expects and the ABI the loaded library reports are
|
||||
// compared at process start, and a mismatch panics on purpose: a silent skew
|
||||
// produces wrong gas and wrong state roots, which is a consensus fault. With
|
||||
// no library linked there is nothing to compare against, and LibraryABIVersion
|
||||
// must say so by agreeing with the Go-side constant rather than inventing a
|
||||
// number that would pass a check it never made.
|
||||
func TestWithNoLibraryTheReportedABIIsTheGoSideConstant(t *testing.T) {
|
||||
if got := LibraryABIVersion(); got != ABIVersion {
|
||||
t.Fatalf("LibraryABIVersion() = %d, want ABIVersion = %d", got, ABIVersion)
|
||||
}
|
||||
}
|
||||
|
||||
// Health never returns nothing. A caller that reads an empty report cannot
|
||||
// distinguish "every backend is fine" from "nobody looked", and this one runs
|
||||
// at node start-up where that difference decides whether the chain runs.
|
||||
func TestHealthAlwaysAnswersAndSaysWhyWhenItCannotRun(t *testing.T) {
|
||||
reports := Health()
|
||||
if len(reports) == 0 {
|
||||
t.Fatal("Health() returned no reports — even nocgo must report status")
|
||||
t.Fatal("Health() returned no reports — even a build with no library must report status")
|
||||
}
|
||||
for _, r := range reports {
|
||||
if r.Name == "" {
|
||||
t.Errorf("health report for backend %d has no name", int(r.Backend))
|
||||
}
|
||||
if r.OK {
|
||||
continue
|
||||
}
|
||||
if r.Err == nil {
|
||||
t.Errorf("health report %q is not OK and does not say why", r.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestBlockContextLayout asserts the Go BlockContext fields are the right
|
||||
// widths in the right order. The C-side layout assertion lives in
|
||||
// go_bridge.cpp (static_assert sizeof(BlockContext) == sizeof(CBlockContext));
|
||||
// this test catches Go-side reordering before it reaches the C ABI.
|
||||
func TestBlockContextLayout(t *testing.T) {
|
||||
var ctx BlockContext
|
||||
ctx.Origin[0] = 0xAA
|
||||
ctx.Origin[19] = 0xBB
|
||||
ctx.GasPrice = 1
|
||||
ctx.Timestamp = 2
|
||||
ctx.Number = 3
|
||||
ctx.Prevrandao[0] = 0xCC
|
||||
ctx.Prevrandao[31] = 0xDD
|
||||
ctx.GasLimit = 4
|
||||
ctx.ChainID = 96369
|
||||
ctx.BaseFee = 5
|
||||
ctx.BlobBaseFee = 6
|
||||
ctx.Coinbase[0] = 0xEE
|
||||
ctx.Coinbase[19] = 0xFF
|
||||
ctx.BlobHashes[0][0] = 0x11
|
||||
ctx.BlobHashes[7][31] = 0x22
|
||||
ctx.NumBlobHashes = 8
|
||||
// -----------------------------------------------------------------------------
|
||||
// The one struct that crosses the C ABI by memcpy
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// Sanity: the struct round-trips its own fields.
|
||||
if ctx.ChainID != 96369 {
|
||||
t.Errorf("ChainID round-trip failed: got %d", ctx.ChainID)
|
||||
// BlockContext is copied to the C side field-for-field by address, so its Go
|
||||
// layout IS the wire format. Every offset is pinned.
|
||||
//
|
||||
// The previous test here assigned each field and read it back, which is a
|
||||
// property of Go's assignment rather than of the layout: reordering the struct
|
||||
// left it passing. Offsets are what a reorder changes.
|
||||
//
|
||||
// A change to any number below means the C side's CBlockContext must move in
|
||||
// lockstep AND ABIVersion must be bumped on both sides — the mismatch check at
|
||||
// process start is the only thing that catches a skew, and it can only catch a
|
||||
// skew that was declared.
|
||||
func TestBlockContextIsTheWireLayout(t *testing.T) {
|
||||
var c BlockContext
|
||||
|
||||
if got, want := unsafe.Sizeof(c), uintptr(392); got != want {
|
||||
t.Errorf("sizeof(BlockContext) = %d, want %d", got, want)
|
||||
}
|
||||
if ctx.Origin[0] != 0xAA || ctx.Origin[19] != 0xBB {
|
||||
t.Errorf("Origin round-trip failed: got %x..%x", ctx.Origin[0], ctx.Origin[19])
|
||||
}
|
||||
if ctx.NumBlobHashes != 8 {
|
||||
t.Errorf("NumBlobHashes round-trip failed: got %d", ctx.NumBlobHashes)
|
||||
|
||||
for _, f := range []struct {
|
||||
name string
|
||||
off uintptr
|
||||
want uintptr
|
||||
}{
|
||||
{"Origin", unsafe.Offsetof(c.Origin), 0},
|
||||
{"GasPrice", unsafe.Offsetof(c.GasPrice), 24},
|
||||
{"Timestamp", unsafe.Offsetof(c.Timestamp), 32},
|
||||
{"Number", unsafe.Offsetof(c.Number), 40},
|
||||
{"Prevrandao", unsafe.Offsetof(c.Prevrandao), 48},
|
||||
{"GasLimit", unsafe.Offsetof(c.GasLimit), 80},
|
||||
{"ChainID", unsafe.Offsetof(c.ChainID), 88},
|
||||
{"BaseFee", unsafe.Offsetof(c.BaseFee), 96},
|
||||
{"BlobBaseFee", unsafe.Offsetof(c.BlobBaseFee), 104},
|
||||
{"Coinbase", unsafe.Offsetof(c.Coinbase), 112},
|
||||
{"BlobHashes", unsafe.Offsetof(c.BlobHashes), 132},
|
||||
{"NumBlobHashes", unsafe.Offsetof(c.NumBlobHashes), 388},
|
||||
} {
|
||||
if f.off != f.want {
|
||||
t.Errorf("offsetof(BlockContext.%s) = %d, want %d", f.name, f.off, f.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,674 @@
|
||||
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package parallel
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math/big"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/holiman/uint256"
|
||||
|
||||
"github.com/luxfi/chains/evm/cevm"
|
||||
evmparallel "github.com/luxfi/evm/core/parallel"
|
||||
"github.com/luxfi/evm/core/state"
|
||||
"github.com/luxfi/geth/common"
|
||||
"github.com/luxfi/geth/core/rawdb"
|
||||
"github.com/luxfi/geth/core/tracing"
|
||||
"github.com/luxfi/geth/core/types"
|
||||
"github.com/luxfi/geth/core/vm"
|
||||
ethparams "github.com/luxfi/geth/params"
|
||||
)
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// The declining policy
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// CEVM_STRICT decides what a block cevm cannot run does: fail loudly, or fall
|
||||
// through to the Go EVM.
|
||||
//
|
||||
// Both answers were unreachable from one process until now, because the value
|
||||
// was memoized behind a sync.Once at the first decline. That is why this test
|
||||
// can exist: the two branches are the whole policy and each has to be shown.
|
||||
func TestStrictModeIsWhatTheEnvironmentSays(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
set string
|
||||
strict bool
|
||||
}{
|
||||
{"0", false},
|
||||
{"false", false},
|
||||
{"FALSE", false},
|
||||
{"no", false},
|
||||
{"off", false},
|
||||
{" off ", false},
|
||||
{"", true},
|
||||
{"1", true},
|
||||
{"true", true},
|
||||
{"yes", true},
|
||||
{"maybe", true},
|
||||
} {
|
||||
t.Run(tc.set, func(t *testing.T) {
|
||||
t.Setenv(envCEVMStrict, tc.set)
|
||||
if got := strictGPUEVM(); got != tc.strict {
|
||||
t.Fatalf("CEVM_STRICT=%q → strictGPUEVM() = %v, want %v", tc.set, got, tc.strict)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Unset is not the same as set-to-empty for every reader of an environment,
|
||||
// so the default is pinned against a genuinely absent variable too. Strict is
|
||||
// the safe direction: a binary nobody configured must not silently execute
|
||||
// blocks on a backend the operator did not choose.
|
||||
func TestAnUnsetEnvironmentIsStrict(t *testing.T) {
|
||||
t.Setenv(envCEVMStrict, "0")
|
||||
if err := os.Unsetenv(envCEVMStrict); err != nil {
|
||||
t.Fatalf("unset %s: %v", envCEVMStrict, err)
|
||||
}
|
||||
if !strictGPUEVM() {
|
||||
t.Fatal("with CEVM_STRICT unset the fallback is enabled; the default must be strict")
|
||||
}
|
||||
}
|
||||
|
||||
// A declined block is a refusal under strict mode and a fall-through under the
|
||||
// legacy opt-out — and the refusal has to be recognisable, because the caller
|
||||
// distinguishes "cevm cannot" from "this block is bad" by the sentinel.
|
||||
func TestDecliningIsARefusalUnderStrictAndAFallThroughOtherwise(t *testing.T) {
|
||||
t.Setenv(envCEVMStrict, "1")
|
||||
receipts, err := declineBlock("call_or_create_unsupported_v4", 42, 7)
|
||||
if !errors.Is(err, ErrGPUEVMRequired) {
|
||||
t.Fatalf("strict decline = %v, want ErrGPUEVMRequired", err)
|
||||
}
|
||||
if receipts != nil {
|
||||
t.Errorf("strict decline returned %d receipts alongside its refusal", len(receipts))
|
||||
}
|
||||
// An operator reading only the log needs the reason and the position.
|
||||
for _, want := range []string{"call_or_create_unsupported_v4", "block=42", "tx_index=7"} {
|
||||
if !strings.Contains(err.Error(), want) {
|
||||
t.Errorf("decline error %q does not carry %q", err, want)
|
||||
}
|
||||
}
|
||||
|
||||
t.Setenv(envCEVMStrict, "0")
|
||||
receipts, err = declineBlock("call_or_create_unsupported_v4", 42, 7)
|
||||
if err != nil || receipts != nil {
|
||||
t.Fatalf("legacy decline = (%v, %v), want (nil, nil) so the caller falls through",
|
||||
receipts, err)
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// A build with no C++ EVM
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// The executor in a binary that has no cevm to dispatch to must decline, not
|
||||
// hard-fail: the absence of the library is a property of the build, so it
|
||||
// belongs to the policy CEVM_STRICT governs.
|
||||
//
|
||||
// It used to return a bare error from the sender-recovery step, which meant
|
||||
// CEVM_STRICT=0 — the documented emergency rollback to the Go EVM — could not
|
||||
// clear it. In a build without the library that is every block, permanently.
|
||||
func TestABuildWithNoLibraryDeclinesRatherThanHalting(t *testing.T) {
|
||||
e := &Executor{CevmBackend: cevm.GPUCUDA}
|
||||
sdb := newState(t)
|
||||
header := newHeader()
|
||||
txs := types.Transactions{transfer(t, 1, big.NewInt(1))}
|
||||
|
||||
t.Setenv(envCEVMStrict, "0")
|
||||
receipts, err := e.ExecuteBlock(chainConfig(), header, txs, sdb, vm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("with CEVM_STRICT=0 the block must fall through to the Go EVM, got: %v", err)
|
||||
}
|
||||
if receipts != nil {
|
||||
t.Errorf("fall-through returned %d receipts", len(receipts))
|
||||
}
|
||||
|
||||
t.Setenv(envCEVMStrict, "1")
|
||||
if _, err := e.ExecuteBlock(chainConfig(), header, txs, sdb, vm.Config{}); !errors.Is(err, ErrGPUEVMRequired) {
|
||||
t.Fatalf("under strict mode the refusal must be ErrGPUEVMRequired, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// An empty block is nothing to execute, so it falls through before anything
|
||||
// else is asked — no signer, no library, no policy.
|
||||
func TestAnEmptyBlockIsNotThisExecutorsBusiness(t *testing.T) {
|
||||
e := &Executor{}
|
||||
receipts, err := e.ExecuteBlock(chainConfig(), newHeader(), nil, newState(t), vm.Config{})
|
||||
if err != nil || receipts != nil {
|
||||
t.Fatalf("empty block = (%v, %v), want (nil, nil)", receipts, err)
|
||||
}
|
||||
}
|
||||
|
||||
// The lane the executor was configured with is the lane it reports. A registry
|
||||
// that read a different one would dispatch elsewhere than the operator asked.
|
||||
func TestTheExecutorReportsTheLaneItWasGiven(t *testing.T) {
|
||||
for _, b := range []cevm.Backend{cevm.CPUSequential, cevm.CPUParallel, cevm.GPUMetal, cevm.GPUCUDA} {
|
||||
if got := (&Executor{CevmBackend: b}).Backend(); got != b {
|
||||
t.Errorf("Backend() = %s, want %s", got, b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The interface this package exists to satisfy.
|
||||
func TestTheExecutorIsABlockExecutor(t *testing.T) {
|
||||
var _ evmparallel.BlockExecutor = (*Executor)(nil)
|
||||
}
|
||||
|
||||
// This package must not claim luxfi/evm's single executor slot on import.
|
||||
// RegisterExecutor is a plain assignment, and luxfi/evm's own cevmShadowExecutor
|
||||
// takes that slot from an init() under -tags cevm; a second registration would
|
||||
// replace a consensus-gated applier with this one on link order alone.
|
||||
func TestImportingThisPackageRegistersNothing(t *testing.T) {
|
||||
if _, ok := evmparallel.DefaultExecutor().(*Executor); ok {
|
||||
t.Fatal("importing this package installed its Executor as the default; " +
|
||||
"registration is explicit and must stay explicit")
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// The block, in cevm's wire form
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// Every field cevm reads comes from the transaction it was paired with, and the
|
||||
// pairing is positional. A shape that crossed indexes would execute each
|
||||
// transaction as somebody else.
|
||||
func TestShapeCarriesEachTransactionsOwnFields(t *testing.T) {
|
||||
sdb := newState(t)
|
||||
to := common.Address{0x22}
|
||||
code := []byte{0x60, 0x00}
|
||||
sdb.SetCode(to, code, tracing.CodeChangeUnspecified)
|
||||
|
||||
txs := types.Transactions{
|
||||
types.NewTx(&types.LegacyTx{
|
||||
Nonce: 7, To: &to, Value: big.NewInt(1234), Gas: 21000,
|
||||
GasPrice: big.NewInt(5), Data: []byte{0xAB},
|
||||
}),
|
||||
types.NewTx(&types.LegacyTx{Nonce: 8, Value: big.NewInt(1), Gas: 53000, GasPrice: big.NewInt(9)}),
|
||||
}
|
||||
senders := []common.Address{{0x11}, {0x33}}
|
||||
|
||||
got, i := shape(txs, senders, sdb)
|
||||
if i != len(txs) {
|
||||
t.Fatalf("shape declined at index %d; every transaction here is representable", i)
|
||||
}
|
||||
|
||||
if got[0].Nonce != 7 || got[0].Value != 1234 || got[0].GasLimit != 21000 || got[0].GasPrice != 5 {
|
||||
t.Errorf("tx 0 shaped as %+v", got[0])
|
||||
}
|
||||
if !got[0].HasTo || common.Address(got[0].To) != to {
|
||||
t.Errorf("tx 0 lost its recipient: HasTo=%v To=%x", got[0].HasTo, got[0].To)
|
||||
}
|
||||
if common.Address(got[0].From) != senders[0] {
|
||||
t.Errorf("tx 0 sender = %x, want %x", got[0].From, senders[0])
|
||||
}
|
||||
if string(got[0].Code) != string(code) {
|
||||
t.Errorf("tx 0 did not carry the recipient's code: %x", got[0].Code)
|
||||
}
|
||||
if string(got[0].Data) != string([]byte{0xAB}) {
|
||||
t.Errorf("tx 0 calldata = %x", got[0].Data)
|
||||
}
|
||||
|
||||
// A creation has no recipient, so no address and no code to load.
|
||||
if got[1].HasTo {
|
||||
t.Errorf("tx 1 has no recipient but was shaped with one: %x", got[1].To)
|
||||
}
|
||||
if len(got[1].Code) != 0 {
|
||||
t.Errorf("tx 1 has no recipient but carries code: %x", got[1].Code)
|
||||
}
|
||||
if common.Address(got[1].From) != senders[1] {
|
||||
t.Errorf("tx 1 sender = %x, want %x", got[1].From, senders[1])
|
||||
}
|
||||
}
|
||||
|
||||
// cevm.Transaction holds Value as a uint64. A value above 2^64-1 is rare but
|
||||
// legal, and truncating it would execute a transaction other than the one that
|
||||
// was signed — so the block is declined at the index that cannot be carried.
|
||||
func TestAValueTooLargeForTheWireIsDeclinedAtItsIndex(t *testing.T) {
|
||||
huge := new(big.Int).Lsh(big.NewInt(1), 64) // 2^64: one past what fits
|
||||
txs := types.Transactions{
|
||||
types.NewTx(&types.LegacyTx{Value: big.NewInt(1), Gas: 21000, GasPrice: big.NewInt(1)}),
|
||||
types.NewTx(&types.LegacyTx{Value: huge, Gas: 21000, GasPrice: big.NewInt(1)}),
|
||||
}
|
||||
if _, i := shape(txs, []common.Address{{}, {}}, newState(t)); i != 1 {
|
||||
t.Fatalf("shape declined at index %d, want 1 (the transaction whose value overflows)", i)
|
||||
}
|
||||
|
||||
// One below the boundary still fits, so the refusal is about the width and
|
||||
// not about "large".
|
||||
max := new(big.Int).Sub(huge, big.NewInt(1))
|
||||
txs[1] = types.NewTx(&types.LegacyTx{Value: max, Gas: 21000, GasPrice: big.NewInt(1)})
|
||||
got, i := shape(txs, []common.Address{{}, {}}, newState(t))
|
||||
if i != len(txs) {
|
||||
t.Fatalf("shape declined 2^64-1 at index %d; it is exactly representable", i)
|
||||
}
|
||||
if got[1].Value != max.Uint64() {
|
||||
t.Errorf("value = %d, want %d", got[1].Value, max.Uint64())
|
||||
}
|
||||
}
|
||||
|
||||
// A nil gas price is left at zero rather than dereferenced.
|
||||
func TestANilGasPriceIsZeroAndNotAPanic(t *testing.T) {
|
||||
tx := types.NewTx(&types.LegacyTx{Value: big.NewInt(1), Gas: 21000})
|
||||
got, i := shape(types.Transactions{tx}, []common.Address{{}}, newState(t))
|
||||
if i != 1 {
|
||||
t.Fatalf("shape declined at %d", i)
|
||||
}
|
||||
if got[0].GasPrice != 0 {
|
||||
t.Errorf("gas price = %d, want 0", got[0].GasPrice)
|
||||
}
|
||||
}
|
||||
|
||||
// The block context is what the block-level opcodes answer. Each field comes
|
||||
// from the header, and a field read from the wrong place is a block that
|
||||
// executes against someone else's chain, height or time.
|
||||
func TestTheBlockContextIsTheHeader(t *testing.T) {
|
||||
excess := uint64(131072)
|
||||
header := &types.Header{
|
||||
Time: 1717171717,
|
||||
Number: big.NewInt(9_000_001),
|
||||
GasLimit: 15_000_000,
|
||||
BaseFee: big.NewInt(25_000_000_000),
|
||||
ExcessBlobGas: &excess,
|
||||
Coinbase: common.Address{0xC0, 0xFF, 0xEE},
|
||||
MixDigest: common.Hash{0xAB, 0xCD},
|
||||
}
|
||||
config := ðparams.ChainConfig{ChainID: big.NewInt(96369)}
|
||||
|
||||
got := blockContext(config, header)
|
||||
|
||||
if got.ChainID != 96369 {
|
||||
t.Errorf("ChainID = %d, want 96369 — a block executed against the wrong chain id "+
|
||||
"is replayable on another chain", got.ChainID)
|
||||
}
|
||||
if got.Timestamp != header.Time || got.Number != header.Number.Uint64() {
|
||||
t.Errorf("time/number = %d/%d, want %d/%d",
|
||||
got.Timestamp, got.Number, header.Time, header.Number.Uint64())
|
||||
}
|
||||
if got.GasLimit != header.GasLimit {
|
||||
t.Errorf("GasLimit = %d, want %d", got.GasLimit, header.GasLimit)
|
||||
}
|
||||
if got.BaseFee != header.BaseFee.Uint64() {
|
||||
t.Errorf("BaseFee = %d, want %d", got.BaseFee, header.BaseFee.Uint64())
|
||||
}
|
||||
if got.BlobBaseFee != excess {
|
||||
t.Errorf("BlobBaseFee = %d, want %d", got.BlobBaseFee, excess)
|
||||
}
|
||||
if common.Address(got.Coinbase) != header.Coinbase {
|
||||
t.Errorf("Coinbase = %x, want %x", got.Coinbase, header.Coinbase)
|
||||
}
|
||||
if common.Hash(got.Prevrandao) != header.MixDigest {
|
||||
t.Errorf("Prevrandao = %x, want the header's MixDigest %x", got.Prevrandao, header.MixDigest)
|
||||
}
|
||||
}
|
||||
|
||||
// A pre-merge header carries no base fee and no blob gas. Those are absent
|
||||
// values, not zero ones, and reading through the nil pointer would panic on
|
||||
// every historical block.
|
||||
func TestAHeaderWithoutBaseFeeOrBlobGasIsNotADereference(t *testing.T) {
|
||||
header := &types.Header{Number: big.NewInt(1), Time: 1}
|
||||
got := blockContext(ðparams.ChainConfig{ChainID: big.NewInt(1)}, header)
|
||||
if got.BaseFee != 0 || got.BlobBaseFee != 0 {
|
||||
t.Fatalf("absent base fee / blob gas became %d / %d", got.BaseFee, got.BlobBaseFee)
|
||||
}
|
||||
}
|
||||
|
||||
// A base fee wider than uint64 cannot be carried, and is left at zero rather
|
||||
// than truncated into a different fee.
|
||||
func TestABaseFeeTooLargeForTheWireIsNotTruncated(t *testing.T) {
|
||||
header := &types.Header{
|
||||
Number: big.NewInt(1),
|
||||
BaseFee: new(big.Int).Lsh(big.NewInt(1), 64),
|
||||
}
|
||||
if got := blockContext(ðparams.ChainConfig{ChainID: big.NewInt(1)}, header); got.BaseFee != 0 {
|
||||
t.Fatalf("BaseFee = %d; a value that does not fit must not be truncated", got.BaseFee)
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// What comes back
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// cevm answers positionally, so a result of a different length than the block
|
||||
// cannot be matched up. Assembling it anyway would attribute one transaction's
|
||||
// gas and status to another.
|
||||
func TestAResultThatDoesNotMatchTheBlockIsRefused(t *testing.T) {
|
||||
txs := types.Transactions{transfer(t, 1, big.NewInt(1)), transfer(t, 2, big.NewInt(1))}
|
||||
|
||||
for _, r := range []*cevm.BlockResult{
|
||||
{GasUsed: []uint64{21000}, Status: []cevm.TxStatus{cevm.TxOK, cevm.TxOK}},
|
||||
{GasUsed: []uint64{21000, 21000}, Status: []cevm.TxStatus{cevm.TxOK}},
|
||||
{},
|
||||
} {
|
||||
if _, err := assemble(txs, newState(t), r, newHeader()); err == nil {
|
||||
t.Errorf("assemble accepted a result with %d gas entries and %d statuses for %d transactions",
|
||||
len(r.GasUsed), len(r.Status), len(txs))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The V4 kernel says CallNotSupported for the CALL/CREATE family. Mixing
|
||||
// backends mid-block would execute part of it on cevm and part on the Go EVM,
|
||||
// so the whole block is declined at the transaction that could not run.
|
||||
func TestATransactionTheKernelCannotRunDeclinesTheWholeBlock(t *testing.T) {
|
||||
t.Setenv(envCEVMStrict, "0")
|
||||
txs := types.Transactions{transfer(t, 1, big.NewInt(1)), transfer(t, 2, big.NewInt(1))}
|
||||
result := &cevm.BlockResult{
|
||||
GasUsed: []uint64{21000, 0},
|
||||
Status: []cevm.TxStatus{cevm.TxOK, cevm.TxCallNotSupported},
|
||||
}
|
||||
receipts, err := assemble(txs, newState(t), result, newHeader())
|
||||
if err != nil || receipts != nil {
|
||||
t.Fatalf("assemble = (%v, %v), want the block declined", receipts, err)
|
||||
}
|
||||
}
|
||||
|
||||
// The receipt reconstruction only holds where (status, gas_used) determines
|
||||
// the receipt completely. Everything else is declined at the index that broke
|
||||
// it.
|
||||
//
|
||||
// The contract-creation case is the one that used to be missed: the check
|
||||
// asked whether the transaction had calldata, and a creation with empty init
|
||||
// code has none. Its receipt needs a ContractAddress this reconstruction never
|
||||
// computes, and creation is charged 53000 intrinsic gas against a transfer's
|
||||
// 21000 — so the block would have been receipted with the wrong cumulative gas,
|
||||
// which is exactly what the header commits to.
|
||||
func TestOnlyAPlainValueTransferIsReconstructable(t *testing.T) {
|
||||
sdb := newState(t)
|
||||
withCode := common.Address{0x99}
|
||||
sdb.SetCode(withCode, []byte{0x00}, tracing.CodeChangeUnspecified)
|
||||
plain := common.Address{0x11}
|
||||
|
||||
transferTx := types.NewTx(&types.LegacyTx{To: &plain, Value: big.NewInt(1), Gas: 21000})
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
tx *types.Transaction
|
||||
want int
|
||||
}{
|
||||
{"plain transfer", transferTx, 1},
|
||||
{
|
||||
"contract creation with empty init code",
|
||||
types.NewTx(&types.LegacyTx{Value: big.NewInt(1), Gas: 53000}),
|
||||
0,
|
||||
},
|
||||
{
|
||||
"contract creation with init code",
|
||||
types.NewTx(&types.LegacyTx{Value: big.NewInt(0), Gas: 53000, Data: []byte{0x60, 0x00}}),
|
||||
0,
|
||||
},
|
||||
{
|
||||
"call with calldata",
|
||||
types.NewTx(&types.LegacyTx{To: &plain, Gas: 30000, Data: []byte{0x01}}),
|
||||
0,
|
||||
},
|
||||
{
|
||||
"transfer to an account that has code",
|
||||
types.NewTx(&types.LegacyTx{To: &withCode, Value: big.NewInt(1), Gas: 21000}),
|
||||
0,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := firstBeyondValueTransfer(types.Transactions{tc.tx}, sdb); got != tc.want {
|
||||
t.Fatalf("firstBeyondValueTransfer = %d, want %d", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// And in a block, the index reported is the offending one, not zero.
|
||||
txs := types.Transactions{transferTx, transferTx, types.NewTx(&types.LegacyTx{Gas: 53000})}
|
||||
if got := firstBeyondValueTransfer(txs, sdb); got != 2 {
|
||||
t.Fatalf("firstBeyondValueTransfer = %d, want 2", got)
|
||||
}
|
||||
}
|
||||
|
||||
// A block cevm cannot receipt is declined, and under strict mode that refusal
|
||||
// names the transaction responsible.
|
||||
func TestABlockBeyondValueTransferIsDeclinedAtItsIndex(t *testing.T) {
|
||||
t.Setenv(envCEVMStrict, "1")
|
||||
to := common.Address{0x11}
|
||||
txs := types.Transactions{
|
||||
types.NewTx(&types.LegacyTx{To: &to, Value: big.NewInt(1), Gas: 21000}),
|
||||
types.NewTx(&types.LegacyTx{Gas: 53000}), // a creation
|
||||
}
|
||||
result := &cevm.BlockResult{
|
||||
GasUsed: []uint64{21000, 53000},
|
||||
Status: []cevm.TxStatus{cevm.TxOK, cevm.TxOK},
|
||||
}
|
||||
_, err := assemble(txs, newState(t), result, newHeader())
|
||||
if !errors.Is(err, ErrGPUEVMRequired) {
|
||||
t.Fatalf("assemble = %v, want ErrGPUEVMRequired", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "tx_index=1") {
|
||||
t.Errorf("refusal %q does not name the transaction that could not be receipted", err)
|
||||
}
|
||||
}
|
||||
|
||||
// The receipt is the parity-critical seam: the receipt trie hash is in the
|
||||
// header, so every field has to be what the Go EVM would have produced.
|
||||
func TestReceiptsCarryEachTransactionsOwnResult(t *testing.T) {
|
||||
t.Setenv(envCEVMStrict, "1")
|
||||
to := common.Address{0x11}
|
||||
txs := types.Transactions{
|
||||
types.NewTx(&types.LegacyTx{Nonce: 1, To: &to, Value: big.NewInt(1), Gas: 21000}),
|
||||
types.NewTx(&types.LegacyTx{Nonce: 2, To: &to, Value: big.NewInt(2), Gas: 21000}),
|
||||
types.NewTx(&types.DynamicFeeTx{Nonce: 3, To: &to, Value: big.NewInt(3), Gas: 21000}),
|
||||
}
|
||||
result := &cevm.BlockResult{
|
||||
GasUsed: []uint64{21000, 30000, 21000},
|
||||
Status: []cevm.TxStatus{cevm.TxOK, cevm.TxRevert, cevm.TxReturn},
|
||||
}
|
||||
header := newHeader()
|
||||
|
||||
got, err := assemble(txs, newState(t), result, header)
|
||||
if err != nil {
|
||||
t.Fatalf("assemble: %v", err)
|
||||
}
|
||||
if len(got) != len(txs) {
|
||||
t.Fatalf("got %d receipts for %d transactions", len(got), len(txs))
|
||||
}
|
||||
|
||||
// Cumulative gas is what the header's gasUsed is checked against, so it
|
||||
// must be the running sum and not a repeat of the per-tx figure.
|
||||
for i, want := range []uint64{21000, 51000, 72000} {
|
||||
if got[i].CumulativeGasUsed != want {
|
||||
t.Errorf("receipt %d cumulative gas = %d, want %d", i, got[i].CumulativeGasUsed, want)
|
||||
}
|
||||
if got[i].GasUsed != result.GasUsed[i] {
|
||||
t.Errorf("receipt %d gas = %d, want %d", i, got[i].GasUsed, result.GasUsed[i])
|
||||
}
|
||||
}
|
||||
|
||||
// TxOK and TxReturn are both a clean exit; everything else failed. A revert
|
||||
// receipted as successful is a transaction the chain says worked and did not.
|
||||
for i, want := range []uint64{
|
||||
types.ReceiptStatusSuccessful,
|
||||
types.ReceiptStatusFailed,
|
||||
types.ReceiptStatusSuccessful,
|
||||
} {
|
||||
if got[i].Status != want {
|
||||
t.Errorf("receipt %d status = %d for cevm status %s, want %d",
|
||||
i, got[i].Status, result.Status[i], want)
|
||||
}
|
||||
}
|
||||
|
||||
for i, tx := range txs {
|
||||
if got[i].TxHash != tx.Hash() {
|
||||
t.Errorf("receipt %d is for %s, want %s", i, got[i].TxHash, tx.Hash())
|
||||
}
|
||||
if got[i].Type != tx.Type() {
|
||||
t.Errorf("receipt %d type = %d, want %d", i, got[i].Type, tx.Type())
|
||||
}
|
||||
if got[i].TransactionIndex != uint(i) {
|
||||
t.Errorf("receipt %d index = %d", i, got[i].TransactionIndex)
|
||||
}
|
||||
if got[i].BlockNumber.Cmp(header.Number) != 0 {
|
||||
t.Errorf("receipt %d block = %s, want %s", i, got[i].BlockNumber, header.Number)
|
||||
}
|
||||
if len(got[i].Logs) != 0 {
|
||||
t.Errorf("receipt %d carries %d logs; only value transfers reach here", i, len(got[i].Logs))
|
||||
}
|
||||
if got[i].Bloom != (types.Bloom{}) {
|
||||
t.Errorf("receipt %d has a non-empty bloom over no logs", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Every failing cevm status becomes a failed receipt. A status this package
|
||||
// has not heard of must not be read as success.
|
||||
func TestAnUnrecognisedStatusIsNotSuccess(t *testing.T) {
|
||||
to := common.Address{0x11}
|
||||
tx := types.NewTx(&types.LegacyTx{To: &to, Value: big.NewInt(1), Gas: 21000})
|
||||
|
||||
for _, st := range []cevm.TxStatus{
|
||||
cevm.TxRevert, cevm.TxOOG, cevm.TxError, cevm.TxStatus(200),
|
||||
} {
|
||||
result := &cevm.BlockResult{GasUsed: []uint64{21000}, Status: []cevm.TxStatus{st}}
|
||||
got := receipts(types.Transactions{tx}, result, newHeader())
|
||||
if got[0].Status != types.ReceiptStatusFailed {
|
||||
t.Errorf("cevm status %s became receipt status %d, want failed", st, got[0].Status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// The state snapshot
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// The kernel resolves CALL and CREATE targets from this snapshot, so every
|
||||
// address the block touches has to be in it exactly once, carrying the state
|
||||
// the StateDB holds.
|
||||
func TestTheSnapshotHoldsEveryTouchedAccountOnce(t *testing.T) {
|
||||
sdb := newState(t)
|
||||
from, to := common.Address{0x01}, common.Address{0x02}
|
||||
code := []byte{0x60, 0x01, 0x60, 0x02}
|
||||
sdb.SetNonce(from, 9, tracing.NonceChangeUnspecified)
|
||||
sdb.AddBalance(from, uint256.NewInt(1_000_000), tracing.BalanceChangeUnspecified)
|
||||
sdb.SetCode(to, code, tracing.CodeChangeUnspecified)
|
||||
|
||||
txs := []cevm.Transaction{
|
||||
{From: [20]byte(from), To: [20]byte(to), HasTo: true},
|
||||
{From: [20]byte(from), To: [20]byte(to), HasTo: true}, // same pair again
|
||||
{From: [20]byte(from)}, // a creation: no target
|
||||
}
|
||||
|
||||
got := buildStateSnapshot(txs, sdb)
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("snapshot holds %d accounts, want 2 (the caller and the target, deduped)", len(got))
|
||||
}
|
||||
|
||||
byAddr := map[common.Address]cevm.StateAccount{}
|
||||
for _, a := range got {
|
||||
if _, dup := byAddr[common.Address(a.Address)]; dup {
|
||||
t.Fatalf("account %x appears twice", a.Address)
|
||||
}
|
||||
byAddr[common.Address(a.Address)] = a
|
||||
}
|
||||
|
||||
caller := byAddr[from]
|
||||
if caller.Nonce != 9 {
|
||||
t.Errorf("caller nonce = %d, want 9", caller.Nonce)
|
||||
}
|
||||
if caller.Balance != [4]uint64{1_000_000, 0, 0, 0} {
|
||||
t.Errorf("caller balance limbs = %v, want [1000000 0 0 0]", caller.Balance)
|
||||
}
|
||||
if len(caller.Code) != 0 {
|
||||
t.Errorf("an account with no code carries %d bytes of it", len(caller.Code))
|
||||
}
|
||||
|
||||
target := byAddr[to]
|
||||
if string(target.Code) != string(code) {
|
||||
t.Errorf("target code = %x, want %x", target.Code, code)
|
||||
}
|
||||
if common.Hash(target.CodeHash) != sdb.GetCodeHash(to) {
|
||||
t.Errorf("target code hash = %x, want %x", target.CodeHash, sdb.GetCodeHash(to))
|
||||
}
|
||||
}
|
||||
|
||||
// Balance crosses as four little-endian uint64 limbs because that is the
|
||||
// kernel's layout. A balance assembled in the wrong order is a different
|
||||
// number, and the kernel would fund or starve the account by it.
|
||||
func TestTheBalanceLimbsAreLittleEndian(t *testing.T) {
|
||||
sdb := newState(t)
|
||||
addr := common.Address{0x07}
|
||||
|
||||
// A value with a distinct byte in every limb, so a swapped or reversed
|
||||
// order cannot coincide with the right answer.
|
||||
bal := new(uint256.Int)
|
||||
bal.SetBytes32([]byte{
|
||||
0x44, 0, 0, 0, 0, 0, 0, 0x43, // limb 3 (most significant)
|
||||
0x34, 0, 0, 0, 0, 0, 0, 0x33, // limb 2
|
||||
0x24, 0, 0, 0, 0, 0, 0, 0x23, // limb 1
|
||||
0x14, 0, 0, 0, 0, 0, 0, 0x13, // limb 0 (least significant)
|
||||
})
|
||||
sdb.AddBalance(addr, bal, tracing.BalanceChangeUnspecified)
|
||||
|
||||
got := buildStateSnapshot([]cevm.Transaction{{From: [20]byte(addr)}}, sdb)
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("snapshot holds %d accounts, want 1", len(got))
|
||||
}
|
||||
want := [4]uint64{
|
||||
0x1400000000000013,
|
||||
0x2400000000000023,
|
||||
0x3400000000000033,
|
||||
0x4400000000000043,
|
||||
}
|
||||
if got[0].Balance != want {
|
||||
t.Fatalf("balance limbs = %#x, want %#x", got[0].Balance, want)
|
||||
}
|
||||
// And the limbs really are the number: reassembling them gives it back.
|
||||
if reassemble(got[0].Balance).Cmp(bal) != 0 {
|
||||
t.Fatalf("limbs reassemble to %s, want %s", reassemble(got[0].Balance), bal)
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing to snapshot is not an empty snapshot to build. A nil StateDB is the
|
||||
// caller having nothing to read from, which must not be a dereference.
|
||||
func TestThereIsNoSnapshotWithoutTransactionsOrAState(t *testing.T) {
|
||||
if got := buildStateSnapshot(nil, newState(t)); got != nil {
|
||||
t.Errorf("snapshot of no transactions = %v, want nil", got)
|
||||
}
|
||||
if got := buildStateSnapshot([]cevm.Transaction{{}}, nil); got != nil {
|
||||
t.Errorf("snapshot with no state = %v, want nil", got)
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// helpers
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
func newState(t *testing.T) *state.StateDB {
|
||||
t.Helper()
|
||||
sdb, err := state.New(types.EmptyRootHash, state.NewDatabase(rawdb.NewMemoryDatabase()), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("state.New: %v", err)
|
||||
}
|
||||
return sdb
|
||||
}
|
||||
|
||||
func newHeader() *types.Header {
|
||||
return &types.Header{Number: big.NewInt(1), Time: 1, GasLimit: 15_000_000}
|
||||
}
|
||||
|
||||
func chainConfig() *ethparams.ChainConfig {
|
||||
return ðparams.ChainConfig{ChainID: big.NewInt(96369)}
|
||||
}
|
||||
|
||||
// transfer builds a plain value transfer to a fixed recipient.
|
||||
func transfer(t *testing.T, nonce uint64, value *big.Int) *types.Transaction {
|
||||
t.Helper()
|
||||
to := common.Address{0x11}
|
||||
return types.NewTx(&types.LegacyTx{Nonce: nonce, To: &to, Value: value, Gas: 21000})
|
||||
}
|
||||
|
||||
// reassemble rebuilds a uint256 from little-endian limbs.
|
||||
func reassemble(limbs [4]uint64) *uint256.Int {
|
||||
out := new(uint256.Int)
|
||||
for i := 3; i >= 0; i-- {
|
||||
out.Lsh(out, 64)
|
||||
out.Or(out, uint256.NewInt(limbs[i]))
|
||||
}
|
||||
return out
|
||||
}
|
||||
+206
-164
@@ -14,29 +14,42 @@
|
||||
// The implementation lives WITH cevm (not in luxfi/evm) so the
|
||||
// import direction is correct: luxfi/evm declares the interface;
|
||||
// luxfi/chains/evm/cevm/parallel imports luxfi/evm to satisfy it.
|
||||
// Consumers wire it explicitly:
|
||||
//
|
||||
// import _ "github.com/luxfi/chains/evm/cevm/parallel" // registers
|
||||
// # Wiring
|
||||
//
|
||||
// or for explicit control:
|
||||
// Explicit, and it has to stay explicit:
|
||||
//
|
||||
// import (
|
||||
// "github.com/luxfi/evm/core/parallel"
|
||||
// cevmparallel "github.com/luxfi/chains/evm/cevm/parallel"
|
||||
// "github.com/luxfi/chains/evm/cevm"
|
||||
// )
|
||||
// parallel.RegisterExecutor(&cevmparallel.Executor{
|
||||
// CevmBackend: cevm.GPUMetal,
|
||||
// Threads: 0,
|
||||
// })
|
||||
// import (
|
||||
// "github.com/luxfi/evm/core/parallel"
|
||||
// cevmparallel "github.com/luxfi/chains/evm/cevm/parallel"
|
||||
// "github.com/luxfi/chains/evm/cevm"
|
||||
// )
|
||||
// parallel.RegisterExecutor(&cevmparallel.Executor{
|
||||
// CevmBackend: cevm.GPUMetal,
|
||||
// Threads: 0,
|
||||
// })
|
||||
//
|
||||
// Build with: `go build -tags cgo` (the cgo bridge in cevm/cevm_cgo.go
|
||||
// links libcevm.a + libluxgpu).
|
||||
// luxfi/evm/core/parallel holds ONE executor: RegisterExecutor is a plain
|
||||
// assignment to a package variable, so a second call replaces the first with
|
||||
// no complaint. luxfi/evm's own cevmShadowExecutor takes that slot from an
|
||||
// init() when the binary is built with -tags cevm. A package that registered
|
||||
// itself on import would therefore replace a consensus-gated applier with this
|
||||
// one, depending only on link order — so this package has no init().
|
||||
//
|
||||
// Parity contract: every receipt produced here must byte-equal the
|
||||
// receipt produced by Go EVM Block-STM for the same input tuple.
|
||||
// Enforced by parity_test.go in this package.
|
||||
|
||||
// This doc used to advertise `import _ ".../cevm/parallel" // registers` as an
|
||||
// alternative. There is no init() and never was, so that form registers
|
||||
// nothing: a caller following it got the Go EVM and no error saying otherwise.
|
||||
//
|
||||
// # Execution
|
||||
//
|
||||
// Three orthogonal steps, so that only the middle one needs the C++ library:
|
||||
//
|
||||
// shape / blockContext / buildStateSnapshot — the block, in cevm's wire form
|
||||
// cevm.ExecuteBlock — one cgo call
|
||||
// assemble — what comes back, as receipts
|
||||
//
|
||||
// Parity contract: every receipt produced here must byte-equal the receipt
|
||||
// produced by Go EVM Block-STM for the same input tuple.
|
||||
package parallel
|
||||
|
||||
import (
|
||||
@@ -44,7 +57,6 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/luxfi/crypto/backend"
|
||||
evmparallel "github.com/luxfi/evm/core/parallel"
|
||||
@@ -73,32 +85,31 @@ var ErrGPUEVMRequired = errors.New("cevm: GPU EVM cannot execute this block (V4
|
||||
|
||||
const envCEVMStrict = "CEVM_STRICT"
|
||||
|
||||
var (
|
||||
strictOnce sync.Once
|
||||
strictMode bool
|
||||
)
|
||||
|
||||
// strictGPUEVM reports whether the silent Go EVM fallback is disabled.
|
||||
// Read once at first call (not init) so test suites can set the env var
|
||||
// before exercising ExecuteBlock.
|
||||
//
|
||||
// Read on every call. It used to be memoized behind a sync.Once, which fixed
|
||||
// the answer to whatever the first block that declined happened to see — and
|
||||
// which made the two branches unreachable from a single process, so neither
|
||||
// was ever exercised.
|
||||
func strictGPUEVM() bool {
|
||||
strictOnce.Do(func() {
|
||||
v := strings.ToLower(strings.TrimSpace(os.Getenv(envCEVMStrict)))
|
||||
switch v {
|
||||
case "0", "false", "no", "off":
|
||||
strictMode = false
|
||||
default:
|
||||
// Default ON: do NOT silently shadow-execute on Go EVM. The
|
||||
// fallback was the lie that hid the missing GPU CALL/CREATE.
|
||||
strictMode = true
|
||||
}
|
||||
})
|
||||
return strictMode
|
||||
switch strings.ToLower(strings.TrimSpace(os.Getenv(envCEVMStrict))) {
|
||||
case "0", "false", "no", "off":
|
||||
return false
|
||||
default:
|
||||
// Default ON: do NOT silently shadow-execute on Go EVM. The
|
||||
// fallback was the lie that hid the missing GPU CALL/CREATE.
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// declineBlock is the single fallback exit point. It records the
|
||||
// fallback reason for observability, then returns either the strict
|
||||
// sentinel error (default) or the legacy (nil, nil) opt-out result.
|
||||
//
|
||||
// Every "cevm cannot do this" leaves through here. A path that returned its
|
||||
// own error instead would be a refusal CEVM_STRICT=0 cannot clear, which is
|
||||
// the whole point of the flag — an operator flips it to get the Go EVM back
|
||||
// and the block still fails.
|
||||
func declineBlock(reason string, blockNumber, txIndex uint64) ([]*types.Receipt, error) {
|
||||
backend.RecordFallback(backend.FallbackUnsupported, "cevm:"+reason)
|
||||
if strictGPUEVM() {
|
||||
@@ -112,9 +123,6 @@ func declineBlock(reason string, blockNumber, txIndex uint64) ([]*types.Receipt,
|
||||
|
||||
// Executor is a luxfi/evm/core/parallel.BlockExecutor that dispatches
|
||||
// every block to cevm.ExecuteBlock in one cgo call.
|
||||
//
|
||||
// Compile-time interface check; the var line below ensures Executor
|
||||
// satisfies BlockExecutor.
|
||||
type Executor struct {
|
||||
// CevmBackend selects the cevm execution lane:
|
||||
// cevm.CPUSequential — single-threaded CPU baseline (parity reference)
|
||||
@@ -135,10 +143,9 @@ var _ evmparallel.BlockExecutor = (*Executor)(nil)
|
||||
// receipts.
|
||||
//
|
||||
// Returns (nil, nil) — the documented "fall through to sequential"
|
||||
// signal — when the block contains opcodes cevm GPU can't handle yet
|
||||
// (CALL/CREATE family) or non-trivial tx (Data/Code) for which cevm's
|
||||
// V2 ABI doesn't yet expose per-tx logs. On hard errors it returns
|
||||
// the error.
|
||||
// signal — when CEVM_STRICT=0 and the block contains something cevm cannot
|
||||
// run yet. Under the default strict mode those cases return
|
||||
// ErrGPUEVMRequired instead. On hard errors it returns the error.
|
||||
func (e *Executor) ExecuteBlock(
|
||||
config *ethparams.ChainConfig,
|
||||
header *types.Header,
|
||||
@@ -150,149 +157,193 @@ func (e *Executor) ExecuteBlock(
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Stage 1: build cevm.Transaction shape from luxfi/geth Transactions.
|
||||
//
|
||||
// Sender recovery is the dominant cost of Stage 1 — secp256k1 ECDSA
|
||||
// recovery is ~50us per tx in pure Go and dominates block validation
|
||||
// for full-utilization C-Chain blocks. We batch every sender into one
|
||||
// cgo dispatch into the luxcpp/crypto first-party pipeline (see
|
||||
// cevm.BatchRecoverSenders). The batch also primes the per-tx
|
||||
// sigCache via types.CacheSender so any subsequent types.Sender call
|
||||
// is a cache hit.
|
||||
// Sender recovery is the dominant cost of shaping the block — secp256k1
|
||||
// ECDSA recovery is ~50us per tx in pure Go and dominates block validation
|
||||
// for full-utilization C-Chain blocks. Every sender goes into one cgo
|
||||
// dispatch into the luxcpp/crypto first-party pipeline. The batch also
|
||||
// primes the per-tx sigCache via types.CacheSender so any subsequent
|
||||
// types.Sender call is a cache hit.
|
||||
signer := types.MakeSigner(config, header.Number, header.Time)
|
||||
senders, err := cevm.BatchRecoverSenders(txs, signer)
|
||||
if err != nil {
|
||||
if errors.Is(err, cevm.ErrNotLinked) {
|
||||
// This binary has no C++ EVM. That is a property of the build, not
|
||||
// of the block, so it leaves by the one door that consults
|
||||
// CEVM_STRICT. It used to return a hard error from here, which no
|
||||
// CEVM_STRICT=0 could clear: in a build without the library that is
|
||||
// every block, permanently, and the documented rollback to the Go
|
||||
// EVM did not reach this line.
|
||||
return declineBlock("native_evm_not_linked", header.Number.Uint64(), 0)
|
||||
}
|
||||
return nil, fmt.Errorf("cevm: batch sender recovery: %w", err)
|
||||
}
|
||||
cevmTxs := make([]cevm.Transaction, len(txs))
|
||||
for i, tx := range txs {
|
||||
ct := cevm.Transaction{
|
||||
GasLimit: tx.Gas(),
|
||||
Nonce: tx.Nonce(),
|
||||
Data: tx.Data(),
|
||||
}
|
||||
copy(ct.From[:], senders[i].Bytes())
|
||||
if to := tx.To(); to != nil {
|
||||
copy(ct.To[:], to.Bytes())
|
||||
ct.HasTo = true
|
||||
// For real GPU execution the receiver's bytecode must be
|
||||
// loaded so the kernel can interpret it. Skip for CALL /
|
||||
// DELEGATECALL targets — cevm GPU returns status=5 for
|
||||
// nested calls, caught at Stage 4.
|
||||
ct.Code = statedb.GetCode(*to)
|
||||
}
|
||||
// Value + GasPrice are uint64 in cevm.Transaction; transactions
|
||||
// with values exceeding uint64 (rare but legal) cannot run
|
||||
// through this backend. Decline the block.
|
||||
if tx.Value().IsUint64() {
|
||||
ct.Value = tx.Value().Uint64()
|
||||
} else {
|
||||
return declineBlock("value_overflow_uint64", header.Number.Uint64(), uint64(i))
|
||||
}
|
||||
if tx.GasPrice() != nil && tx.GasPrice().IsUint64() {
|
||||
ct.GasPrice = tx.GasPrice().Uint64()
|
||||
}
|
||||
cevmTxs[i] = ct
|
||||
}
|
||||
|
||||
// Stage 2: build the BlockContext.
|
||||
blockCtx := cevm.BlockContext{
|
||||
Timestamp: header.Time,
|
||||
Number: header.Number.Uint64(),
|
||||
GasLimit: header.GasLimit,
|
||||
ChainID: config.ChainID.Uint64(),
|
||||
cevmTxs, i := shape(txs, senders, statedb)
|
||||
if i < len(txs) {
|
||||
return declineBlock("value_overflow_uint64", header.Number.Uint64(), uint64(i))
|
||||
}
|
||||
if header.BaseFee != nil && header.BaseFee.IsUint64() {
|
||||
blockCtx.BaseFee = header.BaseFee.Uint64()
|
||||
}
|
||||
if header.ExcessBlobGas != nil {
|
||||
blockCtx.BlobBaseFee = *header.ExcessBlobGas
|
||||
}
|
||||
copy(blockCtx.Coinbase[:], header.Coinbase.Bytes())
|
||||
// Prevrandao = post-merge MixDigest. Pre-merge headers carry zero
|
||||
// MixDigest; the cevm side treats zero as "not set".
|
||||
copy(blockCtx.Prevrandao[:], header.MixDigest.Bytes())
|
||||
|
||||
// Stage 3: build state snapshot of touched accounts.
|
||||
//
|
||||
// The GPU CALL/CREATE path needs target nonce/balance/code on-device.
|
||||
// Walk every (caller, target) tuple, dedupe addresses, and read the
|
||||
// account data from the StateDB. Pre-V4 the kernel returned
|
||||
// CallNotSupported for every CALL → triggered the V3 → cevm CPU
|
||||
// fallback. With V4 we hand the GPU the data it needs and the call
|
||||
// completes on-device for the LP-108 P5 corpus.
|
||||
blockCtx := blockContext(config, header)
|
||||
snapshot := buildStateSnapshot(cevmTxs, statedb)
|
||||
|
||||
// Stage 4: dispatch.
|
||||
threads := e.Threads
|
||||
if threads == 0 {
|
||||
threads = 1
|
||||
}
|
||||
result, err := cevm.ExecuteBlock(e.CevmBackend, threads, cevmTxs, &blockCtx, snapshot)
|
||||
if err != nil {
|
||||
if errors.Is(err, cevm.ErrNotLinked) {
|
||||
return declineBlock("native_evm_not_linked", header.Number.Uint64(), 0)
|
||||
}
|
||||
return nil, fmt.Errorf("cevm: ExecuteBlock: %w", err)
|
||||
}
|
||||
return assemble(txs, statedb, result, header)
|
||||
}
|
||||
|
||||
// Backend returns the cevm backend lane this Executor dispatches to.
|
||||
func (e *Executor) Backend() cevm.Backend { return e.CevmBackend }
|
||||
|
||||
// shape converts the block's transactions into cevm's wire form, positionally
|
||||
// paired with the recovered senders.
|
||||
//
|
||||
// It returns the index of the first transaction that cannot be represented, or
|
||||
// len(txs) when every one can. cevm.Transaction carries Value and GasPrice as
|
||||
// uint64; a value above 2^64-1 is rare but legal, and truncating it would
|
||||
// execute a transaction other than the one that was signed.
|
||||
func shape(txs types.Transactions, senders []common.Address, statedb *state.StateDB) ([]cevm.Transaction, int) {
|
||||
out := make([]cevm.Transaction, len(txs))
|
||||
for i, tx := range txs {
|
||||
if !tx.Value().IsUint64() {
|
||||
return out, i
|
||||
}
|
||||
ct := cevm.Transaction{
|
||||
GasLimit: tx.Gas(),
|
||||
Nonce: tx.Nonce(),
|
||||
Data: tx.Data(),
|
||||
Value: tx.Value().Uint64(),
|
||||
}
|
||||
copy(ct.From[:], senders[i].Bytes())
|
||||
if to := tx.To(); to != nil {
|
||||
copy(ct.To[:], to.Bytes())
|
||||
ct.HasTo = true
|
||||
// For real GPU execution the receiver's bytecode must be
|
||||
// loaded so the kernel can interpret it.
|
||||
ct.Code = statedb.GetCode(*to)
|
||||
}
|
||||
if tx.GasPrice() != nil && tx.GasPrice().IsUint64() {
|
||||
ct.GasPrice = tx.GasPrice().Uint64()
|
||||
}
|
||||
out[i] = ct
|
||||
}
|
||||
return out, len(txs)
|
||||
}
|
||||
|
||||
// blockContext is the block-level execution context every transaction in the
|
||||
// block sees: what TIMESTAMP, NUMBER, CHAINID, BASEFEE, COINBASE, GASLIMIT,
|
||||
// PREVRANDAO and BLOBBASEFEE answer.
|
||||
func blockContext(config *ethparams.ChainConfig, header *types.Header) cevm.BlockContext {
|
||||
ctx := cevm.BlockContext{
|
||||
Timestamp: header.Time,
|
||||
Number: header.Number.Uint64(),
|
||||
GasLimit: header.GasLimit,
|
||||
ChainID: config.ChainID.Uint64(),
|
||||
}
|
||||
if header.BaseFee != nil && header.BaseFee.IsUint64() {
|
||||
ctx.BaseFee = header.BaseFee.Uint64()
|
||||
}
|
||||
if header.ExcessBlobGas != nil {
|
||||
ctx.BlobBaseFee = *header.ExcessBlobGas
|
||||
}
|
||||
copy(ctx.Coinbase[:], header.Coinbase.Bytes())
|
||||
// Prevrandao = post-merge MixDigest. Pre-merge headers carry zero
|
||||
// MixDigest; the cevm side treats zero as "not set".
|
||||
copy(ctx.Prevrandao[:], header.MixDigest.Bytes())
|
||||
return ctx
|
||||
}
|
||||
|
||||
// assemble turns what cevm returned into receipts, or declines the block.
|
||||
//
|
||||
// It needs no library — only the result — which is why it is separate from the
|
||||
// dispatch: this is the parity-critical half and it has to be exercisable in
|
||||
// a build that cannot execute anything.
|
||||
func assemble(
|
||||
txs types.Transactions,
|
||||
statedb *state.StateDB,
|
||||
result *cevm.BlockResult,
|
||||
header *types.Header,
|
||||
) ([]*types.Receipt, error) {
|
||||
if len(result.GasUsed) != len(txs) || len(result.Status) != len(txs) {
|
||||
return nil, fmt.Errorf("cevm: result length mismatch (gas=%d status=%d txs=%d)",
|
||||
len(result.GasUsed), len(result.Status), len(txs))
|
||||
}
|
||||
|
||||
// Stage 5: detect CallNotSupported. The cevm V4 kernel returns
|
||||
// TxCallNotSupported for CALL/CREATE/DELEGATECALL/STATICCALL
|
||||
// opcodes. The V5 kernel (spec at chains/evm/cevm/V5_ABI.md)
|
||||
// implements these on device; until it lands, decline the block
|
||||
// rather than mix backends mid-block.
|
||||
// The cevm V4 kernel returns TxCallNotSupported for
|
||||
// CALL/CREATE/DELEGATECALL/STATICCALL opcodes. The V5 kernel (spec at
|
||||
// chains/evm/cevm/V5_ABI.md) implements these on device; until it lands,
|
||||
// decline the block rather than mix backends mid-block.
|
||||
for i, st := range result.Status {
|
||||
if st == cevm.TxCallNotSupported {
|
||||
return declineBlock("call_or_create_unsupported_v4", header.Number.Uint64(), uint64(i))
|
||||
}
|
||||
}
|
||||
|
||||
// Stage 6: receipt reconstruction.
|
||||
//
|
||||
// IMPORTANT: this stage is the parity-critical seam. Receipt
|
||||
// fields (Status, CumulativeGasUsed, Bloom, Logs, ContractAddress,
|
||||
// GasUsed, BlockHash, BlockNumber, TransactionIndex, EffectiveGasPrice)
|
||||
// must match Go EVM byte-for-byte. The cevm V2 ABI returns
|
||||
// (gas_used, status); logs/bloom/contract-address must be derived
|
||||
// from the StateDB after cevm writes its state-trie deltas. Until
|
||||
// the cevm V3 ABI exposes per-tx logs (open work-item), we cannot
|
||||
// produce parity-compatible receipts for tx that emit LOG opcodes.
|
||||
//
|
||||
// For the safety of this commit: if any tx is non-trivial (has Data
|
||||
// or Code), fall through to Go EVM. The cevm path is enabled only
|
||||
// for value-transfer-only blocks where receipts are deterministic
|
||||
// from (status, gas_used) alone.
|
||||
allValueTransfer := true
|
||||
for _, tx := range txs {
|
||||
if len(tx.Data()) > 0 {
|
||||
allValueTransfer = false
|
||||
break
|
||||
}
|
||||
if to := tx.To(); to != nil && len(statedb.GetCode(*to)) > 0 {
|
||||
allValueTransfer = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if !allValueTransfer {
|
||||
return declineBlock("non_value_transfer_logs_abi_pending_v5", header.Number.Uint64(), 0)
|
||||
// The cevm V4 ABI returns (gas_used, status) and nothing else, so a
|
||||
// receipt is only reconstructable where (status, gas_used) determines it
|
||||
// completely. That is a plain value transfer and nothing more.
|
||||
if i := firstBeyondValueTransfer(txs, statedb); i < len(txs) {
|
||||
return declineBlock("non_value_transfer_logs_abi_pending_v5", header.Number.Uint64(), uint64(i))
|
||||
}
|
||||
return receipts(txs, result, header), nil
|
||||
}
|
||||
|
||||
receipts := make([]*types.Receipt, len(txs))
|
||||
// firstBeyondValueTransfer returns the index of the first transaction whose
|
||||
// receipt does not follow from (status, gas_used) alone, or len(txs) when
|
||||
// every one does.
|
||||
//
|
||||
// Three things put a transaction beyond that:
|
||||
//
|
||||
// - calldata, which can reach code that emits LOGs — and the V4 ABI carries
|
||||
// no per-tx logs, so the bloom and the log list would be reconstructed as
|
||||
// empty;
|
||||
// - a recipient that has code, for the same reason;
|
||||
// - no recipient at all, which is a contract CREATION. This one used to be
|
||||
// missed: the check asked `len(tx.Data()) > 0`, and a creation with empty
|
||||
// init code answers no. Its receipt needs a ContractAddress that this
|
||||
// reconstruction does not compute, and creation is charged 53000 intrinsic
|
||||
// gas against a transfer's 21000 — so it would have been receipted, with
|
||||
// the wrong gas, into the cumulative total the block header commits to.
|
||||
func firstBeyondValueTransfer(txs types.Transactions, statedb *state.StateDB) int {
|
||||
for i, tx := range txs {
|
||||
to := tx.To()
|
||||
if to == nil || len(tx.Data()) > 0 {
|
||||
return i
|
||||
}
|
||||
if len(statedb.GetCode(*to)) > 0 {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return len(txs)
|
||||
}
|
||||
|
||||
// receipts reconstructs the block's receipts from cevm's per-tx status and gas.
|
||||
//
|
||||
// This is the parity-critical seam: every field must match what the Go EVM
|
||||
// produces for the same transaction, because the receipt trie hash is in the
|
||||
// header. It is only reached for blocks that firstBeyondValueTransfer passed,
|
||||
// where there are no logs to carry and no contract address to compute.
|
||||
func receipts(txs types.Transactions, result *cevm.BlockResult, header *types.Header) []*types.Receipt {
|
||||
out := make([]*types.Receipt, len(txs))
|
||||
cumulativeGas := uint64(0)
|
||||
for i, tx := range txs {
|
||||
gas := result.GasUsed[i]
|
||||
cumulativeGas += gas
|
||||
var receiptStatus uint64
|
||||
status := uint64(types.ReceiptStatusFailed)
|
||||
switch result.Status[i] {
|
||||
case cevm.TxOK, cevm.TxReturn:
|
||||
receiptStatus = types.ReceiptStatusSuccessful
|
||||
default:
|
||||
receiptStatus = types.ReceiptStatusFailed
|
||||
status = types.ReceiptStatusSuccessful
|
||||
}
|
||||
receipts[i] = &types.Receipt{
|
||||
out[i] = &types.Receipt{
|
||||
Type: tx.Type(),
|
||||
Status: receiptStatus,
|
||||
Status: status,
|
||||
CumulativeGasUsed: cumulativeGas,
|
||||
GasUsed: gas,
|
||||
TxHash: tx.Hash(),
|
||||
@@ -302,13 +353,9 @@ func (e *Executor) ExecuteBlock(
|
||||
Bloom: types.Bloom{},
|
||||
}
|
||||
}
|
||||
|
||||
return receipts, nil
|
||||
return out
|
||||
}
|
||||
|
||||
// Backend returns the cevm backend lane this Executor dispatches to.
|
||||
func (e *Executor) Backend() cevm.Backend { return e.CevmBackend }
|
||||
|
||||
// buildStateSnapshot collects every (caller, target) address touched by the
|
||||
// batch and reads its account data from the StateDB. The GPU dispatch hands
|
||||
// this snapshot to the kernel host so OP_CALL / OP_CREATE can resolve
|
||||
@@ -319,9 +366,7 @@ func (e *Executor) Backend() cevm.Backend { return e.CevmBackend }
|
||||
// nonce / balance only.
|
||||
//
|
||||
// Balance encoding: 4×uint64 little-endian limbs (Balance[0] = low 64 bits)
|
||||
// to match the kernel's HostStateAccount layout exactly. uint256 → limbs
|
||||
// is just `Uint64()` per word; geth's uint256.Int already stores in this
|
||||
// order so we copy it verbatim.
|
||||
// to match the kernel's HostStateAccount layout exactly.
|
||||
func buildStateSnapshot(txs []cevm.Transaction, statedb *state.StateDB) []cevm.StateAccount {
|
||||
if len(txs) == 0 || statedb == nil {
|
||||
return nil
|
||||
@@ -336,13 +381,10 @@ func buildStateSnapshot(txs []cevm.Transaction, statedb *state.StateDB) []cevm.S
|
||||
acct := cevm.StateAccount{Nonce: statedb.GetNonce(addr)}
|
||||
copy(acct.Address[:], addr.Bytes())
|
||||
if bal := statedb.GetBalance(addr); bal != nil {
|
||||
// uint256.Int is stored as little-endian uint64 limbs:
|
||||
// Uint64() returns word 0; words 1-3 require array
|
||||
// access. The geth API exposes the raw words via .Bytes32
|
||||
// (BE) — we read the LE uint64 limbs through that.
|
||||
// uint256.Int is stored as little-endian uint64 limbs, and the
|
||||
// kernel's HostStateAccount.balance[] expects the same order.
|
||||
// Bytes32 hands them over big-endian, so read each limb back out.
|
||||
b32 := bal.Bytes32()
|
||||
// b32 is big-endian; convert to little-endian limb layout
|
||||
// matching the kernel's HostStateAccount.balance[].
|
||||
for i := 0; i < 4; i++ {
|
||||
var w uint64
|
||||
for j := 0; j < 8; j++ {
|
||||
|
||||
+45
-17
@@ -1,14 +1,24 @@
|
||||
// Lux EVM plugin — C-Chain EVM with all precompiles enabled.
|
||||
// Command evm serves the C-Chain EVM to a Lux node, with every precompile
|
||||
// linked in.
|
||||
//
|
||||
// Every precompile is explicitly imported. No umbrella packages.
|
||||
// Genesis determines which are active at which block/timestamp.
|
||||
// Every precompile is explicitly imported. No umbrella packages. Genesis
|
||||
// determines which are active at which block/timestamp.
|
||||
//
|
||||
// The binary's FILENAME must be the CB58 of constants.ContractVMID
|
||||
// (mgj786NP7uDwBCcq6YwThhaN8FLyybkCa4zBWTQbNgmK6k9A6) — that is how the node's
|
||||
// plugin registry resolves a vmID from a CreateChainTx to an implementation.
|
||||
// A binary installed under any other name is invisible and the chain silently
|
||||
// never starts, which is why `version` prints the id it must be installed as:
|
||||
// the fact an operator needs is in the place an operator already looks.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/luxfi/constants"
|
||||
"github.com/luxfi/evm/plugin/evm"
|
||||
"github.com/luxfi/log"
|
||||
"github.com/luxfi/sys/ulimit"
|
||||
@@ -25,12 +35,12 @@ import (
|
||||
_ "github.com/luxfi/precompile/vrf" // 0x3213 ECVRF verify (RFC 9381)
|
||||
|
||||
// ── Post-Quantum (FIPS 203/204/205) — LP-4200 unified block ──────────
|
||||
_ "github.com/luxfi/precompile/mlkem" // 0x012201 ML-KEM (FIPS 203 — Module-LWE KEM)
|
||||
_ "github.com/luxfi/precompile/mldsa" // 0x012202 ML-DSA (FIPS 204 — Module-LWE signature)
|
||||
_ "github.com/luxfi/precompile/slhdsa" // 0x012203 SLH-DSA (FIPS 205 — hash-based signature)
|
||||
_ "github.com/luxfi/precompile/pulsar" // 0x012204 Pulsar (Module-LWE threshold FIPS 204)
|
||||
_ "github.com/luxfi/precompile/p3q" // 0x012205 P3Q (strict-PQ STARK / FRI / cSHAKE256)
|
||||
_ "github.com/luxfi/precompile/corona" // 0x012206 Corona (Ring-LWE threshold)
|
||||
_ "github.com/luxfi/precompile/mldsa" // 0x012202 ML-DSA (FIPS 204 — Module-LWE signature)
|
||||
_ "github.com/luxfi/precompile/mlkem" // 0x012201 ML-KEM (FIPS 203 — Module-LWE KEM)
|
||||
_ "github.com/luxfi/precompile/p3q" // 0x012205 P3Q (strict-PQ STARK / FRI / cSHAKE256)
|
||||
_ "github.com/luxfi/precompile/pulsar" // 0x012204 Pulsar (Module-LWE threshold FIPS 204)
|
||||
_ "github.com/luxfi/precompile/slhdsa" // 0x012203 SLH-DSA (FIPS 205 — hash-based signature)
|
||||
_ "github.com/luxfi/precompile/xwing" // 0x2221 X-Wing hybrid KEM (X25519+ML-KEM)
|
||||
|
||||
// ── Hashing / ZK Curves ─────────────────────────────
|
||||
@@ -84,16 +94,27 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
versionStr := fmt.Sprintf("Lux-EVM/1.0.0 [node=%s, rpcchainvm=%d]", version.Current, version.RPCChainVMProtocol)
|
||||
os.Exit(run(context.Background(), os.Args[1:], os.Stdout, os.Stderr))
|
||||
}
|
||||
|
||||
if len(os.Args) > 1 && os.Args[1] == "version" {
|
||||
fmt.Println(versionStr)
|
||||
os.Exit(0)
|
||||
// run is main with the exit lifted out, so what the plugin does is reachable
|
||||
// from a test and only the exit itself is not.
|
||||
func run(ctx context.Context, args []string, out, errOut io.Writer) int {
|
||||
if len(args) > 0 && args[0] == "version" {
|
||||
// Every number here is read from the package that declares it. The EVM
|
||||
// version is luxfi/evm's own — this line used to carry a literal
|
||||
// "1.0.0" beside it, and two declarations of one version disagree the
|
||||
// first time either is bumped, with the one an operator reads being
|
||||
// this one. The rpcchainvm protocol comes from the same place the node
|
||||
// reads it, because a plugin whose protocol differs from the node's is
|
||||
// refused at the handshake and this line is where that is diagnosed.
|
||||
fmt.Fprintf(out, "evm/%s %s [node=%s, rpcchainvm=%d]\n",
|
||||
evm.Version, constants.ContractVMID, version.Current, version.RPCChainVMProtocol)
|
||||
return 0
|
||||
}
|
||||
|
||||
if err := ulimit.Set(ulimit.DefaultFDLimit, log.Root()); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to set fd limit: %s\n", err)
|
||||
os.Exit(1)
|
||||
return fail(errOut, err)
|
||||
}
|
||||
|
||||
// Resolve the execution backend before serving. With cgo this links
|
||||
@@ -101,8 +122,15 @@ func main() {
|
||||
// with -tags cevm; GoEVM otherwise). Without cgo this stays on GoEVM.
|
||||
selectExecutionBackend(log.Root())
|
||||
|
||||
if err := rpc.Serve(context.Background(), log.Root(), &evm.VM{}); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "rpc.Serve error: %s\n", err)
|
||||
os.Exit(1)
|
||||
if err := rpc.Serve(ctx, log.Root(), &evm.VM{}); err != nil {
|
||||
return fail(errOut, err)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// fail reports why the plugin is not serving and gives the node a non-zero
|
||||
// exit. A plugin that dies quietly presents as a chain that never starts.
|
||||
func fail(w io.Writer, err error) int {
|
||||
fmt.Fprintf(w, "evm plugin: %s\n", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/constants"
|
||||
"github.com/luxfi/evm/core/parallel"
|
||||
"github.com/luxfi/evm/plugin/evm"
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/log"
|
||||
"github.com/luxfi/version"
|
||||
"github.com/luxfi/vm/rpc/runtime"
|
||||
)
|
||||
|
||||
// canonicalVMIDCB58 is C-Chain's vmID in the encoding the node's plugin
|
||||
// registry uses. The binary this package builds MUST be installed under
|
||||
// exactly this filename; the registry resolves a CreateChainTx's vmID to an
|
||||
// implementation by looking for a file with this name in the plugin directory.
|
||||
//
|
||||
// Written out literally rather than computed, so that a change to the vmID —
|
||||
// for any reason, including an "obviously equivalent" refactor — fails here
|
||||
// instead of silently producing a chain no node can start.
|
||||
const canonicalVMIDCB58 = "mgj786NP7uDwBCcq6YwThhaN8FLyybkCa4zBWTQbNgmK6k9A6"
|
||||
|
||||
// C-Chain's id is the one in luxfi/constants and nothing else. A vmID is an
|
||||
// immutable one-way door: baked into the CreateChainTx at genesis, stored by
|
||||
// the P-Chain forever, and the filename the plugin is installed under.
|
||||
func TestTheVMIDIsCanonicalAndStable(t *testing.T) {
|
||||
if constants.ContractVMID != constants.EVMID {
|
||||
t.Fatalf("ContractVMID = %s but EVMID = %s; C-Chain has one id",
|
||||
constants.ContractVMID, constants.EVMID)
|
||||
}
|
||||
if want := (ids.ID{'e', 'v', 'm'}); constants.ContractVMID != want {
|
||||
t.Fatalf("ContractVMID bytes = %v, want %v", constants.ContractVMID[:4], want[:4])
|
||||
}
|
||||
if got := constants.ContractVMID.String(); got != canonicalVMIDCB58 {
|
||||
t.Fatalf("ContractVMID CB58 = %q, want %q", got, canonicalVMIDCB58)
|
||||
}
|
||||
}
|
||||
|
||||
// The `version` subcommand prints the id the binary must be INSTALLED as, not
|
||||
// just the software version.
|
||||
//
|
||||
// The two facts fail differently and an operator needs both. A wrong software
|
||||
// version starts a chain that misbehaves; a wrong FILENAME starts nothing at
|
||||
// all — the node's registry resolves a CreateChainTx's vmID by looking for a
|
||||
// file with that name, so a correctly built plugin under the wrong name is
|
||||
// invisible, and the symptom is a chain that never appears with no error
|
||||
// anywhere.
|
||||
func TestVersionNamesTheIdTheBinaryMustBeInstalledAs(t *testing.T) {
|
||||
var out, errOut bytes.Buffer
|
||||
if code := run(context.Background(), []string{"version"}, &out, &errOut); code != 0 {
|
||||
t.Fatalf("version exited %d, want 0; stderr=%q", code, errOut.String())
|
||||
}
|
||||
if !strings.Contains(out.String(), canonicalVMIDCB58) {
|
||||
t.Errorf("version output %q does not name the vmID the plugin must be installed as (%s)",
|
||||
out.String(), canonicalVMIDCB58)
|
||||
}
|
||||
if errOut.Len() != 0 {
|
||||
t.Errorf("version wrote to stderr: %q", errOut.String())
|
||||
}
|
||||
}
|
||||
|
||||
// The EVM version printed is the VM's own, not a literal beside it. This used
|
||||
// to print a hardcoded "Lux-EVM/1.0.0" while luxfi/evm carried its own
|
||||
// plugin/evm.Version; the two agreed only by never being looked at together.
|
||||
func TestTheVersionIsTheVMsOwn(t *testing.T) {
|
||||
want, err := (&evm.VM{}).Version(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("VM.Version: %v", err)
|
||||
}
|
||||
|
||||
var out, errOut bytes.Buffer
|
||||
if code := run(context.Background(), []string{"version"}, &out, &errOut); code != 0 {
|
||||
t.Fatalf("version exited %d, want 0; stderr=%q", code, errOut.String())
|
||||
}
|
||||
if !strings.Contains(out.String(), want) {
|
||||
t.Errorf("version output %q does not carry the VM's own version %q", out.String(), want)
|
||||
}
|
||||
}
|
||||
|
||||
// The node and rpcchainvm numbers are read from the same package the node
|
||||
// reads them from. A plugin whose rpcchainvm protocol differs from the node's
|
||||
// is refused at the handshake with nothing to inspect but this line.
|
||||
func TestVersionNamesTheProtocolTheNodeMustMatch(t *testing.T) {
|
||||
var out, errOut bytes.Buffer
|
||||
if code := run(context.Background(), []string{"version"}, &out, &errOut); code != 0 {
|
||||
t.Fatalf("version exited %d, want 0", code)
|
||||
}
|
||||
got := out.String()
|
||||
if !strings.Contains(got, version.Current.String()) {
|
||||
t.Errorf("version output %q does not name the node version %s", got, version.Current)
|
||||
}
|
||||
if !strings.Contains(got, "rpcchainvm=") {
|
||||
t.Errorf("version output %q does not name the rpcchainvm protocol", got)
|
||||
}
|
||||
}
|
||||
|
||||
// Anything that is not `version` serves, and serving needs a node. Started by
|
||||
// hand there is no engine address in the environment, so the plugin says why
|
||||
// it is not serving and exits non-zero.
|
||||
//
|
||||
// The exit code is the load-bearing half. A plugin that fails to serve and
|
||||
// exits 0 looks to the node's runtime like a plugin that ran and finished, so
|
||||
// the chain never starts and nothing reports a failure — the same silent
|
||||
// absence a misnamed binary produces.
|
||||
func TestAPluginWithNoNodeToServeSaysSoAndFails(t *testing.T) {
|
||||
t.Setenv(runtime.EngineAddressKey, "")
|
||||
|
||||
var out, errOut bytes.Buffer
|
||||
if code := run(context.Background(), nil, &out, &errOut); code != 1 {
|
||||
t.Fatalf("run with no engine address exited %d, want 1", code)
|
||||
}
|
||||
if !strings.Contains(errOut.String(), "evm plugin:") {
|
||||
t.Errorf("stderr %q does not name the plugin as the source of the failure", errOut.String())
|
||||
}
|
||||
if !strings.Contains(errOut.String(), runtime.EngineAddressKey) {
|
||||
t.Errorf("stderr %q does not name the missing environment variable %q",
|
||||
errOut.String(), runtime.EngineAddressKey)
|
||||
}
|
||||
}
|
||||
|
||||
// An empty argument list and an argument that is not `version` take the same
|
||||
// path: there is one subcommand, and everything else is "serve".
|
||||
func TestOnlyVersionIsASubcommand(t *testing.T) {
|
||||
t.Setenv(runtime.EngineAddressKey, "")
|
||||
|
||||
for _, args := range [][]string{nil, {}, {"serve"}, {"Version"}, {"--version"}} {
|
||||
var out, errOut bytes.Buffer
|
||||
if code := run(context.Background(), args, &out, &errOut); code != 1 {
|
||||
t.Errorf("run(%q) exited %d, want 1 (it must have tried to serve)", args, code)
|
||||
}
|
||||
if out.Len() != 0 {
|
||||
t.Errorf("run(%q) wrote %q to stdout; only `version` reports on stdout", args, out.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fail is the one place a refusal is reported, so the message names the plugin
|
||||
// and the code is non-zero whatever went wrong.
|
||||
func TestEveryRefusalIsAttributedAndNonZero(t *testing.T) {
|
||||
var w bytes.Buffer
|
||||
if code := fail(&w, errors.New("disk fell off")); code != 1 {
|
||||
t.Fatalf("fail() = %d, want 1", code)
|
||||
}
|
||||
if got := w.String(); got != "evm plugin: disk fell off\n" {
|
||||
t.Fatalf("fail() wrote %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// Selecting the backend leaves exactly one lane active, and it is one this
|
||||
// build actually has. A binary that resolved to a backend it cannot run would
|
||||
// execute nothing and report a name.
|
||||
func TestSelectingTheBackendLeavesARunnableLaneActive(t *testing.T) {
|
||||
selectExecutionBackend(log.Root())
|
||||
|
||||
active := parallel.ActiveBackend()
|
||||
available := parallel.AvailableBackends()
|
||||
if len(available) == 0 {
|
||||
t.Fatal("no execution backend is available; the EVM cannot run a block")
|
||||
}
|
||||
for _, b := range available {
|
||||
if b == active {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatalf("active backend %q is not among the available ones %q", active, available)
|
||||
}
|
||||
|
||||
// Selection is idempotent: the node calls it once at start-up, but a value
|
||||
// that changed on a second call would mean the answer depends on when it was
|
||||
// asked.
|
||||
func TestSelectingTheBackendTwiceGivesTheSameLane(t *testing.T) {
|
||||
selectExecutionBackend(log.Root())
|
||||
first := parallel.ActiveBackend()
|
||||
selectExecutionBackend(log.Root())
|
||||
if second := parallel.ActiveBackend(); second != first {
|
||||
t.Fatalf("backend moved from %q to %q between two identical calls", first, second)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package oraclevm
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/accel"
|
||||
"github.com/luxfi/log"
|
||||
)
|
||||
|
||||
// A host built around nothing defers its failure to whichever method the
|
||||
// operator calls first, arbitrarily far from the mistake. NewAccelHost refuses
|
||||
// at the point the mistake is made.
|
||||
func TestNoVMIsRefusedAtConstruction(t *testing.T) {
|
||||
h, err := NewAccelHost(nil)
|
||||
if !errors.Is(err, ErrNilVM) {
|
||||
t.Fatalf("NewAccelHost(nil) error = %v, want ErrNilVM", err)
|
||||
}
|
||||
if h != nil {
|
||||
t.Fatalf("NewAccelHost(nil) returned a host (%p) alongside its refusal", h)
|
||||
}
|
||||
}
|
||||
|
||||
// The host wraps the VM it was given and holds a session for it. The embedded
|
||||
// *VM is the promoted field every method of the VM reaches through, so a host
|
||||
// that dropped it would answer for a different VM than the operator built.
|
||||
func TestTheHostHoldsTheVMItWasGiven(t *testing.T) {
|
||||
vm := newVM(t)
|
||||
|
||||
h, err := NewAccelHost(vm)
|
||||
if err != nil {
|
||||
t.Fatalf("NewAccelHost: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = h.Close() })
|
||||
|
||||
if h.VM != vm {
|
||||
t.Fatalf("host wraps %p, want the VM it was given, %p", h.VM, vm)
|
||||
}
|
||||
if h.Session() == nil {
|
||||
t.Fatal("Session() = nil; the host exists to carry one")
|
||||
}
|
||||
// The session is named for the chain, so per-VM GPU accounting attributes
|
||||
// its work to O-Chain and not to whichever VM allocated first.
|
||||
if got := h.Session().ID(); got != "oraclevm" {
|
||||
t.Errorf("session id = %q, want %q", got, "oraclevm")
|
||||
}
|
||||
}
|
||||
|
||||
// Options reach the session. Without this, NewAccelHost's variadic parameter
|
||||
// could be dropped on the floor and every caller would silently get the
|
||||
// documented default instead of what it asked for.
|
||||
func TestOptionsReachTheSession(t *testing.T) {
|
||||
h, err := NewAccelHost(newVM(t), accel.WithPriority(accel.PriorityHigh))
|
||||
if err != nil {
|
||||
t.Fatalf("NewAccelHost: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = h.Close() })
|
||||
|
||||
if got := h.Session().Priority(); got != accel.PriorityHigh {
|
||||
t.Errorf("session priority = %v, want %v", got, accel.PriorityHigh)
|
||||
}
|
||||
|
||||
// And the documented default when nothing is asked for.
|
||||
d, err := NewAccelHost(newVM(t))
|
||||
if err != nil {
|
||||
t.Fatalf("NewAccelHost: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = d.Close() })
|
||||
|
||||
if got := d.Session().Priority(); got != accel.PriorityNormal {
|
||||
t.Errorf("default session priority = %v, want %v", got, accel.PriorityNormal)
|
||||
}
|
||||
}
|
||||
|
||||
// Close releases the session and leaves the VM alone: the host owns the GPU
|
||||
// session, not the chain.
|
||||
func TestCloseReleasesTheSessionAndNotTheVM(t *testing.T) {
|
||||
vm := newVM(t)
|
||||
|
||||
h, err := NewAccelHost(vm)
|
||||
if err != nil {
|
||||
t.Fatalf("NewAccelHost: %v", err)
|
||||
}
|
||||
if err := h.Close(); err != nil {
|
||||
t.Fatalf("Close: %v", err)
|
||||
}
|
||||
if !h.Session().IsClosed() {
|
||||
t.Fatal("Close returned nil but the session is still open")
|
||||
}
|
||||
if h.VM != vm {
|
||||
t.Fatal("Close dropped the wrapped VM")
|
||||
}
|
||||
}
|
||||
|
||||
// A host with no session — the zero value, reachable through composite literal
|
||||
// construction — closes without dereferencing it. Nothing to release is not an
|
||||
// error.
|
||||
func TestClosingAHostWithNoSessionIsNotAnError(t *testing.T) {
|
||||
var h AccelHost
|
||||
if err := h.Close(); err != nil {
|
||||
t.Fatalf("Close() on a host with no session = %v, want nil", err)
|
||||
}
|
||||
if h.Session() != nil {
|
||||
t.Fatal("Session() on a host with no session returned one")
|
||||
}
|
||||
}
|
||||
|
||||
// newVM builds an O-Chain VM through the Factory the node uses.
|
||||
func newVM(t *testing.T) *VM {
|
||||
t.Helper()
|
||||
|
||||
raw, err := (&Factory{}).New(log.Root())
|
||||
if err != nil {
|
||||
t.Fatalf("Factory.New: %v", err)
|
||||
}
|
||||
return raw.(*VM)
|
||||
}
|
||||
+48
-19
@@ -1,41 +1,70 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// Command plugin serves the O-Chain VM to a Lux node.
|
||||
//
|
||||
// The binary's FILENAME must be the CB58 of oraclevm.VMID
|
||||
// (r5m1ujrmXxVcQetG3CQfuDLHp2RHKh6vCDaFgBRQfUcTZh7eS) — that is how the node's
|
||||
// plugin registry resolves a vmID from a CreateChainTx to an implementation.
|
||||
// A binary installed under any other name is invisible and the chain silently
|
||||
// never starts, which is why `version` prints the id it must be installed as:
|
||||
// the fact an operator needs is in the place an operator already looks.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/luxfi/chains/oraclevm"
|
||||
"github.com/luxfi/log"
|
||||
ovm "github.com/luxfi/chains/oraclevm"
|
||||
"github.com/luxfi/sys/ulimit"
|
||||
"github.com/luxfi/vm/chain"
|
||||
"github.com/luxfi/vm/rpc"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) > 1 && os.Args[1] == "version" {
|
||||
fmt.Println("Oracle-VM/1.0.0")
|
||||
os.Exit(0)
|
||||
os.Exit(run(context.Background(), os.Args[1:], os.Stdout, os.Stderr))
|
||||
}
|
||||
|
||||
// run is main with the exit lifted out, so what the plugin does is reachable
|
||||
// from a test and only the exit itself is not.
|
||||
func run(ctx context.Context, args []string, out, errOut io.Writer) int {
|
||||
// Built through the same vms.Factory the node's registry uses. oraclevm
|
||||
// asserts *VM is a chain.ChainVM at compile time and its own test asserts
|
||||
// the Factory produces one, so the conversion here cannot be wrong by the
|
||||
// time this compiles.
|
||||
raw, err := (&oraclevm.Factory{}).New(log.Root())
|
||||
if err != nil {
|
||||
return fail(errOut, err)
|
||||
}
|
||||
vm := raw.(chain.ChainVM)
|
||||
|
||||
if len(args) > 0 && args[0] == "version" {
|
||||
// The version comes from the VM, not from a second copy of the number
|
||||
// here. Two declarations of one version disagree the first time either
|
||||
// is bumped, and the one an operator reads is this one.
|
||||
v, err := vm.Version(ctx)
|
||||
if err != nil {
|
||||
return fail(errOut, err)
|
||||
}
|
||||
fmt.Fprintf(out, "oraclevm/%s %s\n", v, oraclevm.VMID)
|
||||
return 0
|
||||
}
|
||||
|
||||
if err := ulimit.Set(ulimit.DefaultFDLimit, log.Root()); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to set fd limit: %s\n", err)
|
||||
os.Exit(1)
|
||||
return fail(errOut, err)
|
||||
}
|
||||
|
||||
f := &ovm.Factory{}
|
||||
raw, err := f.New(log.Root())
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "factory error: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
vm := raw.(chain.ChainVM)
|
||||
if err := rpc.Serve(context.Background(), log.Root(), vm); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "rpc.Serve error: %s\n", err)
|
||||
os.Exit(1)
|
||||
if err := rpc.Serve(ctx, log.Root(), vm); err != nil {
|
||||
return fail(errOut, err)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// fail reports why the plugin is not serving and gives the node a non-zero
|
||||
// exit. A plugin that dies quietly presents as a chain that never starts.
|
||||
func fail(w io.Writer, err error) int {
|
||||
fmt.Fprintf(w, "oraclevm plugin: %s\n", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/chains/oraclevm"
|
||||
"github.com/luxfi/log"
|
||||
"github.com/luxfi/vm/chain"
|
||||
"github.com/luxfi/vm/rpc/runtime"
|
||||
)
|
||||
|
||||
// The `version` subcommand prints the id the binary must be INSTALLED as, not
|
||||
// just the software version.
|
||||
//
|
||||
// The two facts fail differently and an operator needs both. A wrong software
|
||||
// version starts a chain that misbehaves; a wrong FILENAME starts nothing at
|
||||
// all — the node's registry resolves a CreateChainTx's vmID by looking for a
|
||||
// file with that name, so a correctly built plugin under the wrong name is
|
||||
// invisible, and the symptom is a chain that never appears with no error
|
||||
// anywhere. Printing the id here puts it where an operator already looks.
|
||||
func TestVersionNamesTheIdTheBinaryMustBeInstalledAs(t *testing.T) {
|
||||
var out, errOut bytes.Buffer
|
||||
if code := run(context.Background(), []string{"version"}, &out, &errOut); code != 0 {
|
||||
t.Fatalf("version exited %d, want 0; stderr=%q", code, errOut.String())
|
||||
}
|
||||
got := out.String()
|
||||
if !strings.Contains(got, oraclevm.VMID.String()) {
|
||||
t.Errorf("version output %q does not name the vmID %s the plugin must be installed as",
|
||||
got, oraclevm.VMID)
|
||||
}
|
||||
if errOut.Len() != 0 {
|
||||
t.Errorf("version wrote to stderr: %q", errOut.String())
|
||||
}
|
||||
}
|
||||
|
||||
// The version printed is the VM's own answer, not a literal beside it. The
|
||||
// plugin used to print a hardcoded "Oracle-VM/1.0.0" while the VM reported its
|
||||
// own number from luxfi/oracle; bumping either left the two disagreeing, and
|
||||
// the one an operator reads is the plugin's.
|
||||
func TestTheVersionIsTheVMsOwn(t *testing.T) {
|
||||
raw, err := (&oraclevm.Factory{}).New(log.Root())
|
||||
if err != nil {
|
||||
t.Fatalf("Factory.New: %v", err)
|
||||
}
|
||||
want, err := raw.(chain.ChainVM).Version(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("VM.Version: %v", err)
|
||||
}
|
||||
|
||||
var out, errOut bytes.Buffer
|
||||
if code := run(context.Background(), []string{"version"}, &out, &errOut); code != 0 {
|
||||
t.Fatalf("version exited %d, want 0; stderr=%q", code, errOut.String())
|
||||
}
|
||||
if !strings.Contains(out.String(), want) {
|
||||
t.Errorf("version output %q does not carry the VM's own version %q", out.String(), want)
|
||||
}
|
||||
}
|
||||
|
||||
// Anything that is not `version` serves, and serving needs a node. Started by
|
||||
// hand there is no engine address in the environment, so the plugin says why it
|
||||
// is not serving and exits non-zero.
|
||||
//
|
||||
// The exit code is the load-bearing half. A plugin that fails to serve and
|
||||
// exits 0 looks to the node's runtime like a plugin that ran and finished, so
|
||||
// the chain never starts and nothing reports a failure — the same silent
|
||||
// absence a misnamed binary produces.
|
||||
func TestAPluginWithNoNodeToServeSaysSoAndFails(t *testing.T) {
|
||||
t.Setenv(runtime.EngineAddressKey, "")
|
||||
|
||||
var out, errOut bytes.Buffer
|
||||
if code := run(context.Background(), nil, &out, &errOut); code != 1 {
|
||||
t.Fatalf("run with no engine address exited %d, want 1", code)
|
||||
}
|
||||
if !strings.Contains(errOut.String(), "oraclevm plugin:") {
|
||||
t.Errorf("stderr %q does not name the plugin as the source of the failure", errOut.String())
|
||||
}
|
||||
if !strings.Contains(errOut.String(), runtime.EngineAddressKey) {
|
||||
t.Errorf("stderr %q does not name the missing environment variable %q",
|
||||
errOut.String(), runtime.EngineAddressKey)
|
||||
}
|
||||
}
|
||||
|
||||
// An empty argument list and an argument that is not `version` take the same
|
||||
// path: there is one subcommand, and everything else is "serve".
|
||||
func TestOnlyVersionIsASubcommand(t *testing.T) {
|
||||
t.Setenv(runtime.EngineAddressKey, "")
|
||||
|
||||
for _, args := range [][]string{nil, {}, {"serve"}, {"Version"}, {"--version"}} {
|
||||
var out, errOut bytes.Buffer
|
||||
if code := run(context.Background(), args, &out, &errOut); code != 1 {
|
||||
t.Errorf("run(%q) exited %d, want 1 (it must have tried to serve)", args, code)
|
||||
}
|
||||
if out.Len() != 0 {
|
||||
t.Errorf("run(%q) wrote %q to stdout; only `version` reports on stdout", args, out.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fail is the one place a refusal is reported, so the message names the plugin
|
||||
// and the code is non-zero whatever went wrong.
|
||||
func TestEveryRefusalIsAttributedAndNonZero(t *testing.T) {
|
||||
var w bytes.Buffer
|
||||
if code := fail(&w, errors.New("disk fell off")); code != 1 {
|
||||
t.Fatalf("fail() = %d, want 1", code)
|
||||
}
|
||||
if got := w.String(); got != "oraclevm plugin: disk fell off\n" {
|
||||
t.Fatalf("fail() wrote %q", got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package oraclevm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/database/memdb"
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/log"
|
||||
"github.com/luxfi/node/vms/types/fee"
|
||||
"github.com/luxfi/runtime"
|
||||
vmcore "github.com/luxfi/vm"
|
||||
"github.com/luxfi/vm/chain"
|
||||
)
|
||||
|
||||
// feegate.go states that a caller constructing a VM through this shim
|
||||
// "transitively gets the NoUserTxPolicy{}". That is a claim about a value in
|
||||
// another repo, held here only in prose.
|
||||
//
|
||||
// luxfi/oracle's own feegate test builds `&VM{feePolicy: fee.NoUserTxPolicy{}}`
|
||||
// — it assigns the field it then reads, so it passes unchanged if Initialize
|
||||
// stops pinning the policy. This starts from the Factory the node uses, runs
|
||||
// the real Initialize, and reads the policy back. It is the shim's claim, so
|
||||
// it is the shim's test.
|
||||
func TestAVMBuiltThroughThisShimTakesNoUserTransactions(t *testing.T) {
|
||||
vm := start(t)
|
||||
|
||||
policy := vm.FeePolicy()
|
||||
if policy == nil {
|
||||
t.Fatal("FeePolicy() = nil; a chain that declares no policy is a chain with no declared cost")
|
||||
}
|
||||
if _, ok := policy.(fee.NoUserTxPolicy); !ok {
|
||||
t.Fatalf("FeePolicy() = %T, want fee.NoUserTxPolicy — O-Chain is service-only, "+
|
||||
"operator observations arrive through consensus and there is no user mempool", policy)
|
||||
}
|
||||
if err := fee.Validate(policy); err != nil {
|
||||
t.Fatalf("the node's boot gate rejects O-Chain's own declaration: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// An uninitialized VM has not declared anything, and the shim's claim is about
|
||||
// initialized VMs only. Pinned so the test above cannot be read as "any *VM
|
||||
// refuses" — it is Initialize that pins the policy, and that is the statement.
|
||||
func TestAnUninitializedVMHasNoPolicy(t *testing.T) {
|
||||
raw, err := (&Factory{}).New(log.Root())
|
||||
if err != nil {
|
||||
t.Fatalf("Factory.New: %v", err)
|
||||
}
|
||||
if policy := raw.(*VM).FeePolicy(); policy != nil {
|
||||
t.Fatalf("a VM that never ran Initialize reports policy %T; the policy is "+
|
||||
"pinned by Initialize and nowhere else", policy)
|
||||
}
|
||||
}
|
||||
|
||||
// start builds a VM the way the node does — through the Factory — and runs
|
||||
// Initialize against an in-memory database.
|
||||
func start(t *testing.T) *VM {
|
||||
t.Helper()
|
||||
|
||||
raw, err := (&Factory{}).New(log.Root())
|
||||
if err != nil {
|
||||
t.Fatalf("Factory.New: %v", err)
|
||||
}
|
||||
vm := raw.(*VM)
|
||||
|
||||
init := vmcore.Init{
|
||||
Runtime: &runtime.Runtime{
|
||||
ChainID: ids.GenerateTestID(),
|
||||
Log: log.NewNoOpLogger(),
|
||||
},
|
||||
DB: memdb.New(),
|
||||
ToEngine: make(chan vmcore.Message, 1),
|
||||
}
|
||||
if err := vm.Initialize(context.Background(), init); err != nil {
|
||||
t.Fatalf("Initialize: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = vm.Shutdown(context.Background()) })
|
||||
|
||||
// Initialize is the ChainVM contract's entry point, so what it returns must
|
||||
// still be the thing the plugin serves.
|
||||
var _ chain.ChainVM = vm
|
||||
return vm
|
||||
}
|
||||
+19
-2
@@ -5,7 +5,9 @@
|
||||
package oraclevm
|
||||
|
||||
import (
|
||||
"github.com/luxfi/constants"
|
||||
oraclevm "github.com/luxfi/oracle/vm"
|
||||
"github.com/luxfi/vm/chain"
|
||||
)
|
||||
|
||||
// Re-exported public surface.
|
||||
@@ -44,5 +46,20 @@ type (
|
||||
HealthReply = oraclevm.HealthReply
|
||||
)
|
||||
|
||||
// VMID is the canonical O-Chain VM identifier.
|
||||
var VMID = oraclevm.VMID
|
||||
// VMID identifies O-Chain. It is constants.OracleVMID and nothing else.
|
||||
//
|
||||
// A vmID is an immutable one-way door: it is baked into the CreateChainTx at
|
||||
// genesis, it is the plugin binary's filename, and it is what the P-Chain
|
||||
// stores forever. So it is named from the one source of truth rather than
|
||||
// re-exported from luxfi/oracle, which declares it as a private literal
|
||||
// (`ids.ID{'o','r','a','c','l','e','v','m'}`) that agrees with constants by
|
||||
// coincidence and nothing else. This repo builds the plugin binary, so this
|
||||
// repo is where the two must be proven to agree: TestVMID_IsCanonicalAndStable
|
||||
// compares all three.
|
||||
var VMID = constants.OracleVMID
|
||||
|
||||
// Factory.New satisfies the node's vms.Factory and so returns interface{}.
|
||||
// What the plugin serves is a chain.ChainVM, and this is where that is
|
||||
// established — a build error here rather than a type assertion that fails at
|
||||
// the moment an operator starts the chain.
|
||||
var _ chain.ChainVM = (*VM)(nil)
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package oraclevm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/constants"
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/log"
|
||||
upstream "github.com/luxfi/oracle/vm"
|
||||
"github.com/luxfi/vm/chain"
|
||||
)
|
||||
|
||||
// canonicalVMIDCB58 is O-Chain's vmID in the encoding the node's plugin
|
||||
// registry uses. The plugin binary MUST be installed under exactly this
|
||||
// filename; the registry resolves a CreateChainTx's vmID to an implementation
|
||||
// by looking for a file with this name in the plugin directory.
|
||||
//
|
||||
// Written out literally rather than computed, so that a change to the vmID —
|
||||
// for any reason, including an "obviously equivalent" refactor — fails here
|
||||
// instead of silently producing a chain no node can start.
|
||||
const canonicalVMIDCB58 = "r5m1ujrmXxVcQetG3CQfuDLHp2RHKh6vCDaFgBRQfUcTZh7eS"
|
||||
|
||||
// The three declarations of O-Chain's vmID must be one value.
|
||||
//
|
||||
// luxfi/oracle declares its own private literal, `ids.ID{'o','r','a','c','l',
|
||||
// 'e','v','m'}` at vm/factory.go. luxfi/constants declares OracleVMID. They
|
||||
// agree today and nothing in either repo makes them. This repo builds the
|
||||
// plugin binary whose FILENAME is that id, so this is the layer where the
|
||||
// agreement is checked; if luxfi/oracle's literal ever moves, a node would
|
||||
// look for a plugin under one name while genesis named the other, and the
|
||||
// chain would simply never appear.
|
||||
func TestVMID_IsCanonicalAndStable(t *testing.T) {
|
||||
if VMID != constants.OracleVMID {
|
||||
t.Fatalf("VMID = %s, want constants.OracleVMID = %s", VMID, constants.OracleVMID)
|
||||
}
|
||||
if VMID != upstream.VMID {
|
||||
t.Fatalf("VMID = %s, but luxfi/oracle/vm.VMID = %s\n"+
|
||||
"The upstream private literal has drifted from constants.OracleVMID.\n"+
|
||||
"The plugin binary this repo builds is named after constants; the VM\n"+
|
||||
"inside it believes the other value.", VMID, upstream.VMID)
|
||||
}
|
||||
if got := VMID.String(); got != canonicalVMIDCB58 {
|
||||
t.Fatalf("VMID CB58 = %q, want %q\n"+
|
||||
"A vmID cannot change after a chain is created with it. If this is an\n"+
|
||||
"intentional pre-launch change, every declaration must move together:\n"+
|
||||
" luxfi/constants vm_ids.go OracleVMID\n"+
|
||||
" luxfi/oracle vm/factory.go VMID\n"+
|
||||
" chains/oraclevm oraclevm.go, this test",
|
||||
got, canonicalVMIDCB58)
|
||||
}
|
||||
if VMID == ids.Empty {
|
||||
t.Fatal("VMID is the empty id")
|
||||
}
|
||||
}
|
||||
|
||||
// The bytes themselves, spelled out: an ASCII name left-padded into 32 bytes.
|
||||
func TestVMID_Bytes(t *testing.T) {
|
||||
want := ids.ID{'o', 'r', 'a', 'c', 'l', 'e', 'v', 'm'}
|
||||
if VMID != want {
|
||||
t.Fatalf("VMID bytes = %v, want %v", VMID[:10], want[:10])
|
||||
}
|
||||
}
|
||||
|
||||
// O-Chain and R-Chain are different chains, so their ids are different values.
|
||||
// The two shims are near-copies of each other; a copy that kept the wrong id
|
||||
// would still compile, still pass a byte test if that were copied too, and
|
||||
// produce two plugins racing for one filename.
|
||||
func TestVMID_IsNotAnotherChains(t *testing.T) {
|
||||
for name, other := range map[string]ids.ID{
|
||||
"RelayVMID": constants.RelayVMID,
|
||||
"BridgeVMID": constants.BridgeVMID,
|
||||
"MPCVMID": constants.MPCVMID,
|
||||
"IdentityVMID": constants.IdentityVMID,
|
||||
} {
|
||||
if VMID == other {
|
||||
t.Fatalf("oraclevm.VMID equals constants.%s (%s)", name, other)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// What the Factory builds is what the plugin serves.
|
||||
//
|
||||
// Factory.New satisfies the node's vms.Factory, so it returns interface{} and
|
||||
// the value's real type is checked nowhere at compile time. oraclevm.go
|
||||
// asserts *VM implements chain.ChainVM; this asserts the Factory actually
|
||||
// produces a *VM. Together they are the whole claim, and the plugin needs no
|
||||
// runtime check of its own.
|
||||
func TestFactoryBuildsTheVMThePluginServes(t *testing.T) {
|
||||
raw, err := (&Factory{}).New(log.Root())
|
||||
if err != nil {
|
||||
t.Fatalf("Factory.New: %v", err)
|
||||
}
|
||||
vm, ok := raw.(*VM)
|
||||
if !ok {
|
||||
t.Fatalf("Factory.New built a %T, want *oraclevm.VM", raw)
|
||||
}
|
||||
if vm == nil {
|
||||
t.Fatal("Factory.New built a nil *VM")
|
||||
}
|
||||
if _, ok := raw.(chain.ChainVM); !ok {
|
||||
t.Fatalf("Factory.New built a %T, which is not a chain.ChainVM — "+
|
||||
"the plugin cannot serve it", raw)
|
||||
}
|
||||
}
|
||||
|
||||
// A VM reports its own version, and the plugin prints that rather than a
|
||||
// second copy of the number. Two declarations of one version disagree the
|
||||
// first time either is bumped, and the one an operator reads is the plugin's.
|
||||
func TestTheVMReportsAVersion(t *testing.T) {
|
||||
raw, err := (&Factory{}).New(log.Root())
|
||||
if err != nil {
|
||||
t.Fatalf("Factory.New: %v", err)
|
||||
}
|
||||
v, err := raw.(chain.ChainVM).Version(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("Version: %v", err)
|
||||
}
|
||||
if v == "" {
|
||||
t.Fatal("the VM reports an empty version; the plugin would print none")
|
||||
}
|
||||
if want := upstream.Version.String(); v != want {
|
||||
t.Fatalf("VM version = %q, want luxfi/oracle/vm.Version = %q", v, want)
|
||||
}
|
||||
}
|
||||
+48
-19
@@ -1,41 +1,70 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// Command plugin serves the R-Chain VM to a Lux node.
|
||||
//
|
||||
// The binary's FILENAME must be the CB58 of relayvm.VMID
|
||||
// (sP6dLqrrBR9w3soP18fbJ3YzZecZdD7DDdfH2cFhhLq7Hy9bz) — that is how the node's
|
||||
// plugin registry resolves a vmID from a CreateChainTx to an implementation.
|
||||
// A binary installed under any other name is invisible and the chain silently
|
||||
// never starts, which is why `version` prints the id it must be installed as:
|
||||
// the fact an operator needs is in the place an operator already looks.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/luxfi/chains/relayvm"
|
||||
"github.com/luxfi/log"
|
||||
rvm "github.com/luxfi/chains/relayvm"
|
||||
"github.com/luxfi/sys/ulimit"
|
||||
"github.com/luxfi/vm/chain"
|
||||
"github.com/luxfi/vm/rpc"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) > 1 && os.Args[1] == "version" {
|
||||
fmt.Println("Relay-VM/1.0.0")
|
||||
os.Exit(0)
|
||||
os.Exit(run(context.Background(), os.Args[1:], os.Stdout, os.Stderr))
|
||||
}
|
||||
|
||||
// run is main with the exit lifted out, so what the plugin does is reachable
|
||||
// from a test and only the exit itself is not.
|
||||
func run(ctx context.Context, args []string, out, errOut io.Writer) int {
|
||||
// Built through the same vms.Factory the node's registry uses. relayvm
|
||||
// asserts *VM is a chain.ChainVM at compile time and its own test asserts
|
||||
// the Factory produces one, so the conversion here cannot be wrong by the
|
||||
// time this compiles.
|
||||
raw, err := (&relayvm.Factory{}).New(log.Root())
|
||||
if err != nil {
|
||||
return fail(errOut, err)
|
||||
}
|
||||
vm := raw.(chain.ChainVM)
|
||||
|
||||
if len(args) > 0 && args[0] == "version" {
|
||||
// The version comes from the VM, not from a second copy of the number
|
||||
// here. Two declarations of one version disagree the first time either
|
||||
// is bumped, and the one an operator reads is this one.
|
||||
v, err := vm.Version(ctx)
|
||||
if err != nil {
|
||||
return fail(errOut, err)
|
||||
}
|
||||
fmt.Fprintf(out, "relayvm/%s %s\n", v, relayvm.VMID)
|
||||
return 0
|
||||
}
|
||||
|
||||
if err := ulimit.Set(ulimit.DefaultFDLimit, log.Root()); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to set fd limit: %s\n", err)
|
||||
os.Exit(1)
|
||||
return fail(errOut, err)
|
||||
}
|
||||
|
||||
f := &rvm.Factory{}
|
||||
raw, err := f.New(log.Root())
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "factory error: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
vm := raw.(chain.ChainVM)
|
||||
if err := rpc.Serve(context.Background(), log.Root(), vm); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "rpc.Serve error: %s\n", err)
|
||||
os.Exit(1)
|
||||
if err := rpc.Serve(ctx, log.Root(), vm); err != nil {
|
||||
return fail(errOut, err)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// fail reports why the plugin is not serving and gives the node a non-zero
|
||||
// exit. A plugin that dies quietly presents as a chain that never starts.
|
||||
func fail(w io.Writer, err error) int {
|
||||
fmt.Fprintf(w, "relayvm plugin: %s\n", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/chains/relayvm"
|
||||
"github.com/luxfi/log"
|
||||
"github.com/luxfi/vm/chain"
|
||||
"github.com/luxfi/vm/rpc/runtime"
|
||||
)
|
||||
|
||||
// The `version` subcommand prints the id the binary must be INSTALLED as, not
|
||||
// just the software version.
|
||||
//
|
||||
// The two facts fail differently and an operator needs both. A wrong software
|
||||
// version starts a chain that misbehaves; a wrong FILENAME starts nothing at
|
||||
// all — the node's registry resolves a CreateChainTx's vmID by looking for a
|
||||
// file with that name, so a correctly built plugin under the wrong name is
|
||||
// invisible, and the symptom is a chain that never appears with no error
|
||||
// anywhere. Printing the id here puts it where an operator already looks.
|
||||
func TestVersionNamesTheIdTheBinaryMustBeInstalledAs(t *testing.T) {
|
||||
var out, errOut bytes.Buffer
|
||||
if code := run(context.Background(), []string{"version"}, &out, &errOut); code != 0 {
|
||||
t.Fatalf("version exited %d, want 0; stderr=%q", code, errOut.String())
|
||||
}
|
||||
got := out.String()
|
||||
if !strings.Contains(got, relayvm.VMID.String()) {
|
||||
t.Errorf("version output %q does not name the vmID %s the plugin must be installed as",
|
||||
got, relayvm.VMID)
|
||||
}
|
||||
if errOut.Len() != 0 {
|
||||
t.Errorf("version wrote to stderr: %q", errOut.String())
|
||||
}
|
||||
}
|
||||
|
||||
// The version printed is the VM's own answer, not a literal beside it. The
|
||||
// plugin used to print a hardcoded "Relay-VM/1.0.0" while the VM answered with
|
||||
// its own "1.0.0" from luxfi/relay; the two agreed by coincidence, and the
|
||||
// first bump of either would have left the one an operator reads — the
|
||||
// plugin's — wrong.
|
||||
func TestTheVersionIsTheVMsOwn(t *testing.T) {
|
||||
raw, err := (&relayvm.Factory{}).New(log.Root())
|
||||
if err != nil {
|
||||
t.Fatalf("Factory.New: %v", err)
|
||||
}
|
||||
want, err := raw.(chain.ChainVM).Version(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("VM.Version: %v", err)
|
||||
}
|
||||
|
||||
var out, errOut bytes.Buffer
|
||||
if code := run(context.Background(), []string{"version"}, &out, &errOut); code != 0 {
|
||||
t.Fatalf("version exited %d, want 0; stderr=%q", code, errOut.String())
|
||||
}
|
||||
if !strings.Contains(out.String(), want) {
|
||||
t.Errorf("version output %q does not carry the VM's own version %q", out.String(), want)
|
||||
}
|
||||
}
|
||||
|
||||
// Anything that is not `version` serves, and serving needs a node. Started by
|
||||
// hand there is no engine address in the environment, so the plugin says why it
|
||||
// is not serving and exits non-zero.
|
||||
//
|
||||
// The exit code is the load-bearing half. A plugin that fails to serve and
|
||||
// exits 0 looks to the node's runtime like a plugin that ran and finished, so
|
||||
// the chain never starts and nothing reports a failure — the same silent
|
||||
// absence a misnamed binary produces.
|
||||
func TestAPluginWithNoNodeToServeSaysSoAndFails(t *testing.T) {
|
||||
t.Setenv(runtime.EngineAddressKey, "")
|
||||
|
||||
var out, errOut bytes.Buffer
|
||||
if code := run(context.Background(), nil, &out, &errOut); code != 1 {
|
||||
t.Fatalf("run with no engine address exited %d, want 1", code)
|
||||
}
|
||||
if !strings.Contains(errOut.String(), "relayvm plugin:") {
|
||||
t.Errorf("stderr %q does not name the plugin as the source of the failure", errOut.String())
|
||||
}
|
||||
if !strings.Contains(errOut.String(), runtime.EngineAddressKey) {
|
||||
t.Errorf("stderr %q does not name the missing environment variable %q",
|
||||
errOut.String(), runtime.EngineAddressKey)
|
||||
}
|
||||
}
|
||||
|
||||
// An empty argument list and an argument that is not `version` take the same
|
||||
// path: there is one subcommand, and everything else is "serve".
|
||||
func TestOnlyVersionIsASubcommand(t *testing.T) {
|
||||
t.Setenv(runtime.EngineAddressKey, "")
|
||||
|
||||
for _, args := range [][]string{nil, {}, {"serve"}, {"Version"}, {"--version"}} {
|
||||
var out, errOut bytes.Buffer
|
||||
if code := run(context.Background(), args, &out, &errOut); code != 1 {
|
||||
t.Errorf("run(%q) exited %d, want 1 (it must have tried to serve)", args, code)
|
||||
}
|
||||
if out.Len() != 0 {
|
||||
t.Errorf("run(%q) wrote %q to stdout; only `version` reports on stdout", args, out.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fail is the one place a refusal is reported, so the message names the plugin
|
||||
// and the code is non-zero whatever went wrong.
|
||||
func TestEveryRefusalIsAttributedAndNonZero(t *testing.T) {
|
||||
var w bytes.Buffer
|
||||
if code := fail(&w, errors.New("disk fell off")); code != 1 {
|
||||
t.Fatalf("fail() = %d, want 1", code)
|
||||
}
|
||||
if got := w.String(); got != "relayvm plugin: disk fell off\n" {
|
||||
t.Fatalf("fail() wrote %q", got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package relayvm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/database/memdb"
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/log"
|
||||
"github.com/luxfi/node/vms/types/fee"
|
||||
"github.com/luxfi/runtime"
|
||||
vmcore "github.com/luxfi/vm"
|
||||
"github.com/luxfi/vm/chain"
|
||||
)
|
||||
|
||||
// feegate.go states that a caller constructing a VM through this shim
|
||||
// "transitively gets the NoUserTxPolicy{}". That is a claim about a value in
|
||||
// another repo, held here only in prose.
|
||||
//
|
||||
// luxfi/relay's own feegate test builds `&VM{feePolicy: fee.NoUserTxPolicy{}}`
|
||||
// — it assigns the field it then reads, so it passes unchanged if Initialize
|
||||
// stops pinning the policy. This starts from the Factory the node uses, runs
|
||||
// the real Initialize, and reads the policy back. It is the shim's claim, so
|
||||
// it is the shim's test.
|
||||
func TestAVMBuiltThroughThisShimTakesNoUserTransactions(t *testing.T) {
|
||||
vm := start(t)
|
||||
|
||||
policy := vm.FeePolicy()
|
||||
if policy == nil {
|
||||
t.Fatal("FeePolicy() = nil; a chain that declares no policy is a chain with no declared cost")
|
||||
}
|
||||
if _, ok := policy.(fee.NoUserTxPolicy); !ok {
|
||||
t.Fatalf("FeePolicy() = %T, want fee.NoUserTxPolicy — R-Chain is service-only, "+
|
||||
"relayed messages arrive through consensus and there is no user mempool", policy)
|
||||
}
|
||||
if err := fee.Validate(policy); err != nil {
|
||||
t.Fatalf("the node's boot gate rejects R-Chain's own declaration: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// An uninitialized VM has not declared anything, and the shim's claim is about
|
||||
// initialized VMs only. Pinned so the test above cannot be read as "any *VM
|
||||
// refuses" — it is Initialize that pins the policy, and that is the statement.
|
||||
func TestAnUninitializedVMHasNoPolicy(t *testing.T) {
|
||||
raw, err := (&Factory{}).New(log.Root())
|
||||
if err != nil {
|
||||
t.Fatalf("Factory.New: %v", err)
|
||||
}
|
||||
if policy := raw.(*VM).FeePolicy(); policy != nil {
|
||||
t.Fatalf("a VM that never ran Initialize reports policy %T; the policy is "+
|
||||
"pinned by Initialize and nowhere else", policy)
|
||||
}
|
||||
}
|
||||
|
||||
// start builds a VM the way the node does — through the Factory — and runs
|
||||
// Initialize against an in-memory database.
|
||||
func start(t *testing.T) *VM {
|
||||
t.Helper()
|
||||
|
||||
raw, err := (&Factory{}).New(log.Root())
|
||||
if err != nil {
|
||||
t.Fatalf("Factory.New: %v", err)
|
||||
}
|
||||
vm := raw.(*VM)
|
||||
|
||||
genesis, err := json.Marshal(Genesis{})
|
||||
if err != nil {
|
||||
t.Fatalf("marshal genesis: %v", err)
|
||||
}
|
||||
init := vmcore.Init{
|
||||
Runtime: &runtime.Runtime{
|
||||
ChainID: ids.GenerateTestID(),
|
||||
Log: log.NewNoOpLogger(),
|
||||
},
|
||||
DB: memdb.New(),
|
||||
Genesis: genesis,
|
||||
ToEngine: make(chan vmcore.Message, 1),
|
||||
}
|
||||
if err := vm.Initialize(context.Background(), init); err != nil {
|
||||
t.Fatalf("Initialize: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = vm.Shutdown(context.Background()) })
|
||||
|
||||
// Initialize is the ChainVM contract's entry point, so what it returns must
|
||||
// still be the thing the plugin serves.
|
||||
var _ chain.ChainVM = vm
|
||||
return vm
|
||||
}
|
||||
+50
-33
@@ -5,48 +5,65 @@
|
||||
package relayvm
|
||||
|
||||
import (
|
||||
"github.com/luxfi/constants"
|
||||
relayvm "github.com/luxfi/relay/vm"
|
||||
"github.com/luxfi/vm/chain"
|
||||
)
|
||||
|
||||
// Re-exported public surface — type aliases keep existing import paths working.
|
||||
type (
|
||||
Config = relayvm.Config
|
||||
Channel = relayvm.Channel
|
||||
Message = relayvm.Message
|
||||
MessageReceipt = relayvm.MessageReceipt
|
||||
VM = relayvm.VM
|
||||
Genesis = relayvm.Genesis
|
||||
SignedReceipt = relayvm.SignedReceipt
|
||||
ReceiptCommit = relayvm.ReceiptCommit
|
||||
Service = relayvm.Service
|
||||
Factory = relayvm.Factory
|
||||
DestNonceKey = relayvm.DestNonceKey
|
||||
RelayVertex = relayvm.RelayVertex
|
||||
Config = relayvm.Config
|
||||
Channel = relayvm.Channel
|
||||
Message = relayvm.Message
|
||||
MessageReceipt = relayvm.MessageReceipt
|
||||
VM = relayvm.VM
|
||||
Genesis = relayvm.Genesis
|
||||
SignedReceipt = relayvm.SignedReceipt
|
||||
ReceiptCommit = relayvm.ReceiptCommit
|
||||
Service = relayvm.Service
|
||||
Factory = relayvm.Factory
|
||||
DestNonceKey = relayvm.DestNonceKey
|
||||
RelayVertex = relayvm.RelayVertex
|
||||
)
|
||||
|
||||
// Service args/replies — re-exported.
|
||||
type (
|
||||
OpenChannelArgs = relayvm.OpenChannelArgs
|
||||
OpenChannelReply = relayvm.OpenChannelReply
|
||||
GetChannelArgs = relayvm.GetChannelArgs
|
||||
ChannelReply = relayvm.ChannelReply
|
||||
GetChannelReply = relayvm.GetChannelReply
|
||||
CloseChannelArgs = relayvm.CloseChannelArgs
|
||||
CloseChannelReply = relayvm.CloseChannelReply
|
||||
ListChannelsArgs = relayvm.ListChannelsArgs
|
||||
ListChannelsReply = relayvm.ListChannelsReply
|
||||
SendMessageArgs = relayvm.SendMessageArgs
|
||||
SendMessageReply = relayvm.SendMessageReply
|
||||
GetMessageArgs = relayvm.GetMessageArgs
|
||||
MessageReply = relayvm.MessageReply
|
||||
GetMessageReply = relayvm.GetMessageReply
|
||||
ReceiveMessageArgs = relayvm.ReceiveMessageArgs
|
||||
ReceiveMessageReply = relayvm.ReceiveMessageReply
|
||||
GetVerifiedMessageArgs = relayvm.GetVerifiedMessageArgs
|
||||
OpenChannelArgs = relayvm.OpenChannelArgs
|
||||
OpenChannelReply = relayvm.OpenChannelReply
|
||||
GetChannelArgs = relayvm.GetChannelArgs
|
||||
ChannelReply = relayvm.ChannelReply
|
||||
GetChannelReply = relayvm.GetChannelReply
|
||||
CloseChannelArgs = relayvm.CloseChannelArgs
|
||||
CloseChannelReply = relayvm.CloseChannelReply
|
||||
ListChannelsArgs = relayvm.ListChannelsArgs
|
||||
ListChannelsReply = relayvm.ListChannelsReply
|
||||
SendMessageArgs = relayvm.SendMessageArgs
|
||||
SendMessageReply = relayvm.SendMessageReply
|
||||
GetMessageArgs = relayvm.GetMessageArgs
|
||||
MessageReply = relayvm.MessageReply
|
||||
GetMessageReply = relayvm.GetMessageReply
|
||||
ReceiveMessageArgs = relayvm.ReceiveMessageArgs
|
||||
ReceiveMessageReply = relayvm.ReceiveMessageReply
|
||||
GetVerifiedMessageArgs = relayvm.GetVerifiedMessageArgs
|
||||
GetVerifiedMessageReply = relayvm.GetVerifiedMessageReply
|
||||
HealthArgs = relayvm.HealthArgs
|
||||
HealthReply = relayvm.HealthReply
|
||||
HealthArgs = relayvm.HealthArgs
|
||||
HealthReply = relayvm.HealthReply
|
||||
)
|
||||
|
||||
// VMID is the canonical R-Chain VM identifier.
|
||||
var VMID = relayvm.VMID
|
||||
// VMID identifies R-Chain. It is constants.RelayVMID and nothing else.
|
||||
//
|
||||
// A vmID is an immutable one-way door: it is baked into the CreateChainTx at
|
||||
// genesis, it is the plugin binary's filename, and it is what the P-Chain
|
||||
// stores forever. So it is named from the one source of truth rather than
|
||||
// re-exported from luxfi/relay, which declares it as a private literal
|
||||
// (`ids.ID{'r','e','l','a','y','v','m'}`) that agrees with constants by
|
||||
// coincidence and nothing else. This repo builds the plugin binary, so this
|
||||
// repo is where the two must be proven to agree: TestVMID_IsCanonicalAndStable
|
||||
// compares all three.
|
||||
var VMID = constants.RelayVMID
|
||||
|
||||
// Factory.New satisfies the node's vms.Factory and so returns interface{}.
|
||||
// What the plugin serves is a chain.ChainVM, and this is where that is
|
||||
// established — a build error here rather than a type assertion that fails at
|
||||
// the moment an operator starts the chain.
|
||||
var _ chain.ChainVM = (*VM)(nil)
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package relayvm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/constants"
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/log"
|
||||
upstream "github.com/luxfi/relay/vm"
|
||||
"github.com/luxfi/vm/chain"
|
||||
)
|
||||
|
||||
// canonicalVMIDCB58 is R-Chain's vmID in the encoding the node's plugin
|
||||
// registry uses. The plugin binary MUST be installed under exactly this
|
||||
// filename; the registry resolves a CreateChainTx's vmID to an implementation
|
||||
// by looking for a file with this name in the plugin directory.
|
||||
//
|
||||
// Written out literally rather than computed, so that a change to the vmID —
|
||||
// for any reason, including an "obviously equivalent" refactor — fails here
|
||||
// instead of silently producing a chain no node can start.
|
||||
const canonicalVMIDCB58 = "sP6dLqrrBR9w3soP18fbJ3YzZecZdD7DDdfH2cFhhLq7Hy9bz"
|
||||
|
||||
// The three declarations of R-Chain's vmID must be one value.
|
||||
//
|
||||
// luxfi/relay declares its own private literal, `ids.ID{'r','e','l','a','y',
|
||||
// 'v','m'}` at vm/factory.go. luxfi/constants declares RelayVMID. They agree
|
||||
// today and nothing in either repo makes them. This repo builds the plugin
|
||||
// binary whose FILENAME is that id, so this is the layer where the agreement
|
||||
// is checked; if luxfi/relay's literal ever moves, a node would look for a
|
||||
// plugin under one name while genesis named the other, and the chain would
|
||||
// simply never appear.
|
||||
func TestVMID_IsCanonicalAndStable(t *testing.T) {
|
||||
if VMID != constants.RelayVMID {
|
||||
t.Fatalf("VMID = %s, want constants.RelayVMID = %s", VMID, constants.RelayVMID)
|
||||
}
|
||||
if VMID != upstream.VMID {
|
||||
t.Fatalf("VMID = %s, but luxfi/relay/vm.VMID = %s\n"+
|
||||
"The upstream private literal has drifted from constants.RelayVMID.\n"+
|
||||
"The plugin binary this repo builds is named after constants; the VM\n"+
|
||||
"inside it believes the other value.", VMID, upstream.VMID)
|
||||
}
|
||||
if got := VMID.String(); got != canonicalVMIDCB58 {
|
||||
t.Fatalf("VMID CB58 = %q, want %q\n"+
|
||||
"A vmID cannot change after a chain is created with it. If this is an\n"+
|
||||
"intentional pre-launch change, every declaration must move together:\n"+
|
||||
" luxfi/constants vm_ids.go RelayVMID\n"+
|
||||
" luxfi/relay vm/factory.go VMID\n"+
|
||||
" chains/relayvm relayvm.go, this test",
|
||||
got, canonicalVMIDCB58)
|
||||
}
|
||||
if VMID == ids.Empty {
|
||||
t.Fatal("VMID is the empty id")
|
||||
}
|
||||
}
|
||||
|
||||
// The bytes themselves, spelled out: an ASCII name left-padded into 32 bytes.
|
||||
func TestVMID_Bytes(t *testing.T) {
|
||||
want := ids.ID{'r', 'e', 'l', 'a', 'y', 'v', 'm'}
|
||||
if VMID != want {
|
||||
t.Fatalf("VMID bytes = %v, want %v", VMID[:9], want[:9])
|
||||
}
|
||||
}
|
||||
|
||||
// R-Chain and O-Chain are different chains, so their ids are different values.
|
||||
// The two shims are near-copies of each other; a copy that kept the wrong id
|
||||
// would still compile, still pass its own byte test if that were copied too,
|
||||
// and produce two plugins racing for one filename.
|
||||
func TestVMID_IsNotAnotherChains(t *testing.T) {
|
||||
for name, other := range map[string]ids.ID{
|
||||
"OracleVMID": constants.OracleVMID,
|
||||
"BridgeVMID": constants.BridgeVMID,
|
||||
"MPCVMID": constants.MPCVMID,
|
||||
"IdentityVMID": constants.IdentityVMID,
|
||||
} {
|
||||
if VMID == other {
|
||||
t.Fatalf("relayvm.VMID equals constants.%s (%s)", name, other)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// What the Factory builds is what the plugin serves.
|
||||
//
|
||||
// Factory.New satisfies the node's vms.Factory, so it returns interface{} and
|
||||
// the value's real type is checked nowhere at compile time. relayvm.go asserts
|
||||
// *VM implements chain.ChainVM; this asserts the Factory actually produces a
|
||||
// *VM. Together they are the whole claim, and the plugin needs no runtime
|
||||
// check of its own.
|
||||
func TestFactoryBuildsTheVMThePluginServes(t *testing.T) {
|
||||
raw, err := (&Factory{}).New(log.Root())
|
||||
if err != nil {
|
||||
t.Fatalf("Factory.New: %v", err)
|
||||
}
|
||||
vm, ok := raw.(*VM)
|
||||
if !ok {
|
||||
t.Fatalf("Factory.New built a %T, want *relayvm.VM", raw)
|
||||
}
|
||||
if vm == nil {
|
||||
t.Fatal("Factory.New built a nil *VM")
|
||||
}
|
||||
if _, ok := raw.(chain.ChainVM); !ok {
|
||||
t.Fatalf("Factory.New built a %T, which is not a chain.ChainVM — "+
|
||||
"the plugin cannot serve it", raw)
|
||||
}
|
||||
}
|
||||
|
||||
// A VM reports its own version, and the plugin prints that rather than a
|
||||
// second copy of the number. Two declarations of one version disagree the
|
||||
// first time either is bumped, and the one an operator reads is the plugin's.
|
||||
func TestTheVMReportsAVersion(t *testing.T) {
|
||||
raw, err := (&Factory{}).New(log.Root())
|
||||
if err != nil {
|
||||
t.Fatalf("Factory.New: %v", err)
|
||||
}
|
||||
v, err := raw.(chain.ChainVM).Version(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("Version: %v", err)
|
||||
}
|
||||
if v == "" {
|
||||
t.Fatal("the VM reports an empty version; the plugin would print none")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user