mirror of
https://github.com/luxfi/chains.git
synced 2026-09-24 00:26:00 +00:00
The executor receipts only cevm's one answer, and Health asks for it
cevm answers ok=1 only when every tx is a valid plain transfer that succeeded on its 21000 intrinsic gas (gpu_dispatch.cpp ran_through), so assemble accepts that and declines anything else: CallNotSupported, a revert, a gas estimate at the limit. The same check holds a library that answers ok=1 on other terms — main's still does, at the same ABI number — to the one answer the executor can receipt. A block with a creation, calldata or a recipient with code is declined before the snapshot and the cgo call, which could only end in a decline. BLOBBASEFEE is the fee the excess blob gas prices under the chain's blob schedule, not the excess. Declines count as FallbackBackendUnavailable, the linked backend refusing, rather than as a missing device. Health ran five bytecode probes, all of which the entry declines, so every lane logged unhealthy at start. It runs one funded plain transfer now: a GPU lane is healthy when it answers TxOK at 21000, and a CPU lane says it declined, which is all this entry does on a CPU. The package doc no longer shows how to register the executor: it writes no state, so a node that registered it would finalize a block on its parent's state. Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
+8
-13
@@ -1,12 +1,9 @@
|
||||
// Package cevm provides Go bindings to the C++ EVM (cevm) with GPU acceleration.
|
||||
// Import this package to use the C++ EVM as a drop-in replacement for go-ethereum's EVM.
|
||||
//
|
||||
// The C++ EVM supports:
|
||||
// - Block-STM parallel execution
|
||||
// - GPU Keccak-256 state hashing (Metal/CUDA)
|
||||
// - GPU batch ecrecover (Metal/CUDA)
|
||||
// - GPU EVM opcode interpreter (Metal/CUDA)
|
||||
// - ZAP VM plugin protocol (native)
|
||||
// Its block entry, gpu_execute_block (go_bridge.h, ABI 6), runs one kind of
|
||||
// block: plain value transfers, on a GPU lane's value-transfer path, answering
|
||||
// per-tx gas and status. Anything else it declines (ErrDeclined) and the
|
||||
// caller runs on its own EVM. BatchRecoverSenders batches ecrecover.
|
||||
//
|
||||
// Build against the native library: CGO_ENABLED=1 go build -tags lux_cevm_native
|
||||
// Build without it: go build (types only, no execution) — the default,
|
||||
@@ -46,13 +43,11 @@
|
||||
//
|
||||
// The Go module's ABIVersion constant is checked against the loaded
|
||||
// library's gpu_abi_version() in init(). A mismatch panics at process
|
||||
// start — that is intentional. A silent ABI mismatch produces wrong
|
||||
// gas/state results and would corrupt consensus, so fail-fast is the
|
||||
// only safe behaviour.
|
||||
// start — that is intentional. It catches a library built against another
|
||||
// ABI number; two libraries that report the same number are not told apart.
|
||||
//
|
||||
// Use Health() at startup to additionally verify each backend executes
|
||||
// the canonical health-check battery (arithmetic, storage, hashing,
|
||||
// memory, and the call bridge) without error.
|
||||
// Use Health() at startup to see which lanes can run a block: it runs a
|
||||
// funded plain transfer on each.
|
||||
package cevm
|
||||
|
||||
import (
|
||||
|
||||
+53
-197
@@ -47,6 +47,8 @@ import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"unsafe"
|
||||
|
||||
"github.com/luxfi/geth/core/types"
|
||||
)
|
||||
|
||||
// AutoDetect returns the best available backend for this machine.
|
||||
@@ -311,78 +313,41 @@ func LibraryABIVersion() uint32 {
|
||||
return uint32(C.gpu_abi_version())
|
||||
}
|
||||
|
||||
// healthProbe is one entry in the Health() battery — a named bytecode
|
||||
// program with its expected execution status. Every conformant backend must
|
||||
// run each probe to its expected status with non-zero gas.
|
||||
// healthProbe is one entry in the Health() battery: a block, the state before
|
||||
// it, and the gas each of its transactions must come back with at TxOK.
|
||||
type healthProbe struct {
|
||||
name string
|
||||
bytecode []byte
|
||||
wantStatus TxStatus
|
||||
// callBridge=true marks probes whose top-level opcode is in the CALL
|
||||
// family (CALL/CALLCODE/DELEGATECALL/STATICCALL/CREATE/CREATE2). On the
|
||||
// GPU path these currently route through the dispatcher's "not supported"
|
||||
// shim — we only require that the bridge is exercised (i.e. the probe
|
||||
// runs to completion, not that it succeeds with TxOK). Only enforced on
|
||||
// CPU backends; for GPU we accept any status as long as the kernel
|
||||
// returned cleanly.
|
||||
callBridge bool
|
||||
// strictParity=true means gas must match exactly across all backends
|
||||
// that ran this probe. False for probes whose dynamic gas accounting is
|
||||
// known to differ between the interpretive CPU path (cevm) and the
|
||||
// flat GPU kernel (e.g. KECCAK256 dynamic word cost, MCOPY dynamic
|
||||
// memory expansion). Differences on strict probes are kernel bugs and
|
||||
// must fail Health.
|
||||
strictParity bool
|
||||
name string
|
||||
txs []Transaction
|
||||
ctx BlockContext
|
||||
state []StateAccount
|
||||
wantGas uint64
|
||||
}
|
||||
|
||||
// healthBattery is the ordered list of probes Health() runs against every
|
||||
// backend. Each probe targets a different EVM subsystem so a backend that's
|
||||
// silently broken in one area (e.g. storage) fails its own probe instead
|
||||
// of slipping through.
|
||||
// healthBattery is what Health runs on every backend.
|
||||
//
|
||||
// Order matters only insofar as we want simple programs first so a complete
|
||||
// breakage shows up on probe[0] before we waste time on later probes.
|
||||
// gpu_execute_block runs one kind of block (go_bridge.h, ABI 6): plain value
|
||||
// transfers, on a GPU lane's value-transfer path. It declines a batch with
|
||||
// code, and the CPU lanes run nothing without a host. So the battery is one
|
||||
// such block — a funded sender's transfer to a fresh address — which a lane
|
||||
// that can run anything runs to TxOK at its 21000 intrinsic gas, and which
|
||||
// every other lane declines.
|
||||
func healthBattery() []healthProbe {
|
||||
// arith: PUSH1 1, PUSH1 1, ADD, POP, STOP — pure arithmetic.
|
||||
arith := []byte{0x60, 0x01, 0x60, 0x01, 0x01, 0x50, 0x00}
|
||||
|
||||
// storage: PUSH1 0xAB, PUSH1 0x01, SSTORE, PUSH1 0x01, SLOAD, POP, STOP.
|
||||
storage := []byte{0x60, 0xab, 0x60, 0x01, 0x55, 0x60, 0x01, 0x54, 0x50, 0x00}
|
||||
|
||||
// keccak: hash 32 bytes of zero memory at offset 0.
|
||||
// PUSH1 32, PUSH1 0, KECCAK256, POP, STOP.
|
||||
keccak := []byte{0x60, 0x20, 0x60, 0x00, 0x20, 0x50, 0x00}
|
||||
|
||||
// memory: MSTORE 0xAB at offset 0, MLOAD it back, MCOPY 32 bytes 0->32, STOP.
|
||||
// PUSH1 0xAB, PUSH1 0, MSTORE, PUSH1 0, MLOAD, POP,
|
||||
// PUSH1 32, PUSH1 0, PUSH1 32, MCOPY, STOP.
|
||||
memOps := []byte{
|
||||
0x60, 0xab, 0x60, 0x00, 0x52,
|
||||
0x60, 0x00, 0x51, 0x50,
|
||||
0x60, 0x20, 0x60, 0x00, 0x60, 0x20, 0x5e,
|
||||
0x00,
|
||||
}
|
||||
|
||||
// callBridge: CALL with constant target. On GPU this should hit the
|
||||
// "call not supported" branch in the dispatcher and return cleanly with
|
||||
// TxCallNotSupported. On CPU it executes to TxOK (the in-process EVM
|
||||
// supports CALL).
|
||||
// PUSH1 0 (retSize), PUSH1 0 (retOff), PUSH1 0 (argSize), PUSH1 0 (argOff),
|
||||
// PUSH1 0 (value), ADDRESS (to), PUSH1 0 (gas), CALL, POP, STOP.
|
||||
callBridge := []byte{
|
||||
0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, 0x00,
|
||||
0x60, 0x00, 0x30,
|
||||
0x60, 0x00, 0xf1,
|
||||
0x50, 0x00,
|
||||
}
|
||||
|
||||
return []healthProbe{
|
||||
{name: "arith", bytecode: arith, wantStatus: TxOK, strictParity: true},
|
||||
{name: "storage", bytecode: storage, wantStatus: TxOK, strictParity: true},
|
||||
{name: "keccak", bytecode: keccak, wantStatus: TxOK, strictParity: false},
|
||||
{name: "memory", bytecode: memOps, wantStatus: TxOK, strictParity: false},
|
||||
{name: "call-bridge", bytecode: callBridge, wantStatus: TxOK, callBridge: true, strictParity: false},
|
||||
from, to, coinbase := [20]byte{0x01}, [20]byte{0x02}, [20]byte{0xC0}
|
||||
row := func(addr [20]byte, balance uint64) StateAccount {
|
||||
return StateAccount{
|
||||
Address: addr,
|
||||
Balance: [4]uint64{balance},
|
||||
CodeHash: types.EmptyCodeHash,
|
||||
StorageRoot: types.EmptyRootHash,
|
||||
}
|
||||
}
|
||||
return []healthProbe{{
|
||||
name: "transfer",
|
||||
txs: []Transaction{{From: from, To: to, HasTo: true, GasLimit: 21000, Value: 1, GasPrice: 1}},
|
||||
ctx: BlockContext{GasLimit: 30_000_000, ChainID: 1, BaseFee: 1, Coinbase: coinbase},
|
||||
state: []StateAccount{row(from, 1_000_000), row(to, 0), row(coinbase, 0)},
|
||||
wantGas: 21000,
|
||||
}}
|
||||
}
|
||||
|
||||
// HealthProbeResult is the outcome of a single probe on a single backend.
|
||||
@@ -396,7 +361,7 @@ type HealthProbeResult struct {
|
||||
|
||||
// HealthReport is the per-backend result of Health(). It aggregates the
|
||||
// per-probe results into a single OK / not-OK signal: a backend is healthy
|
||||
// iff every probe ran to its expected status with non-zero gas.
|
||||
// iff every probe ran every transaction to TxOK at the gas it must cost.
|
||||
type HealthReport struct {
|
||||
Backend Backend
|
||||
Name string
|
||||
@@ -405,29 +370,21 @@ type HealthReport struct {
|
||||
Probe string // first failing probe name, empty when OK
|
||||
ProbesRun int
|
||||
ProbeResults []HealthProbeResult
|
||||
// Aggregate stats — sum of gas across probes, time of the last probe.
|
||||
// Aggregate stats — sum of gas across probes, status of the last probe.
|
||||
GasUsed uint64
|
||||
Status TxStatus
|
||||
ExecTime float64
|
||||
}
|
||||
|
||||
// Health runs a battery of canonical bytecode programs through every backend
|
||||
// the loaded library exposes and returns a per-backend report. Use at
|
||||
// process start to fail-fast on misconfigured GPUs (driver missing, library
|
||||
// mismatch, device permissions, kernel coverage gaps). Returns nil only if
|
||||
// the runtime cannot enumerate backends at all.
|
||||
// Health runs the battery through every backend the loaded library exposes
|
||||
// and returns a per-backend report. Use at process start to see which lanes
|
||||
// can run a block at all: a GPU lane whose device, driver or library is
|
||||
// wrong fails here. Returns nil only if the runtime cannot enumerate
|
||||
// backends.
|
||||
//
|
||||
// The battery covers:
|
||||
// - arithmetic (ADD/POP) — strict gas parity required across backends
|
||||
// - storage (SSTORE / SLOAD) — strict gas parity required
|
||||
// - hashing (KECCAK256) — non-zero gas required, parity not strict
|
||||
// - memory ops (MSTORE / MLOAD / MCOPY) — non-zero gas required, parity not strict
|
||||
// - the CALL bridge (CALL with a constant target) — must complete cleanly
|
||||
//
|
||||
// A backend is reported OK iff every probe executed to its expected status
|
||||
// with non-zero gas AND its gas matches every other backend on the strict-
|
||||
// parity probes. A failure sets Err and Probe to identify the offending
|
||||
// case.
|
||||
// A backend is OK iff it ran every probe's transactions to TxOK at the gas
|
||||
// they cost. A lane that declines is not OK and says so: its Err wraps
|
||||
// ErrDeclined, which is the answer every CPU lane gives this entry.
|
||||
func Health() []HealthReport {
|
||||
backends := AvailableBackends()
|
||||
if len(backends) == 0 {
|
||||
@@ -441,10 +398,9 @@ func Health() []HealthReport {
|
||||
Name: BackendName(b),
|
||||
ProbeResults: make([]HealthProbeResult, 0, len(probes)),
|
||||
}
|
||||
isGPU := b == GPUMetal || b == GPUCUDA
|
||||
allOK := true
|
||||
for _, p := range probes {
|
||||
pr := runHealthProbe(b, p, isGPU)
|
||||
pr := runHealthProbe(b, p)
|
||||
rep.ProbeResults = append(rep.ProbeResults, pr)
|
||||
rep.ProbesRun++
|
||||
rep.GasUsed += pr.GasUsed
|
||||
@@ -458,128 +414,28 @@ func Health() []HealthReport {
|
||||
rep.OK = allOK
|
||||
out = append(out, rep)
|
||||
}
|
||||
// Cross-backend strict-parity check on probes flagged strictParity=true.
|
||||
// Two backends that disagree on gas for "arith" or "storage" indicate a
|
||||
// real consensus bug — mark BOTH as not healthy so the deploy fails fast.
|
||||
enforceStrictParity(out, probes)
|
||||
return out
|
||||
}
|
||||
|
||||
// enforceStrictParity walks the per-backend reports, finds probes flagged
|
||||
// strictParity=true, and marks any backend whose gas differs from the
|
||||
// majority value as NotOK. We use the median (robust to one outlier) as
|
||||
// the reference rather than the first-seen, so a single buggy CPU build
|
||||
// doesn't poison every GPU report.
|
||||
func enforceStrictParity(reports []HealthReport, probes []healthProbe) {
|
||||
if len(reports) < 2 {
|
||||
return // nothing to compare
|
||||
}
|
||||
// Build map: probe name → strictParity bit.
|
||||
strict := make(map[string]bool, len(probes))
|
||||
for _, p := range probes {
|
||||
if p.strictParity {
|
||||
strict[p.name] = true
|
||||
}
|
||||
}
|
||||
// For each strict probe, collect the gas values across all backends
|
||||
// that produced an OK probe result, find the majority value, and flag
|
||||
// any backend whose gas differs from it.
|
||||
probeNames := make([]string, 0, len(strict))
|
||||
for n := range strict {
|
||||
probeNames = append(probeNames, n)
|
||||
}
|
||||
for _, probeName := range probeNames {
|
||||
// Per-probe gas histogram across backends.
|
||||
hist := make(map[uint64]int)
|
||||
for _, r := range reports {
|
||||
for _, pr := range r.ProbeResults {
|
||||
if pr.Name == probeName && pr.OK {
|
||||
hist[pr.GasUsed]++
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(hist) <= 1 {
|
||||
continue // all backends agree (or only one backend ran this probe)
|
||||
}
|
||||
// Find majority gas value. Tie → smallest gas (the most CPU-interpretive-like).
|
||||
var majorityGas uint64
|
||||
var majorityCount int
|
||||
for g, c := range hist {
|
||||
if c > majorityCount || (c == majorityCount && g < majorityGas) {
|
||||
majorityGas = g
|
||||
majorityCount = c
|
||||
}
|
||||
}
|
||||
// Flag backends whose gas differs from majority.
|
||||
for i := range reports {
|
||||
for _, pr := range reports[i].ProbeResults {
|
||||
if pr.Name != probeName || !pr.OK {
|
||||
continue
|
||||
}
|
||||
if pr.GasUsed != majorityGas {
|
||||
reports[i].OK = false
|
||||
if reports[i].Err == nil {
|
||||
reports[i].Err = fmt.Errorf(
|
||||
"strict-parity probe %q: gas=%d but majority=%d (likely kernel gas-accounting bug)",
|
||||
probeName, pr.GasUsed, majorityGas)
|
||||
reports[i].Probe = probeName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// runHealthProbe executes one probe on one backend and returns its result.
|
||||
// The result.OK rule: gas must be > 0 AND status must match the probe's
|
||||
// expectation, modulo the call-bridge exception described in healthProbe.
|
||||
func runHealthProbe(b Backend, p healthProbe, isGPU bool) HealthProbeResult {
|
||||
tx := Transaction{
|
||||
HasTo: true,
|
||||
Code: p.bytecode,
|
||||
GasLimit: 200_000,
|
||||
Nonce: 0,
|
||||
GasPrice: 1,
|
||||
}
|
||||
func runHealthProbe(b Backend, p healthProbe) HealthProbeResult {
|
||||
pr := HealthProbeResult{Name: p.name}
|
||||
r, err := ExecuteBlock(b, 0, []Transaction{tx}, nil, nil)
|
||||
r, err := ExecuteBlock(b, 0, p.txs, &p.ctx, p.state)
|
||||
if err != nil {
|
||||
pr.Err = fmt.Errorf("probe %q: %w", p.name, err)
|
||||
return pr
|
||||
}
|
||||
if len(r.GasUsed) != 1 || len(r.Status) != 1 {
|
||||
pr.Err = fmt.Errorf("probe %q: malformed result (gas=%d status=%d)",
|
||||
p.name, len(r.GasUsed), len(r.Status))
|
||||
if len(r.GasUsed) != len(p.txs) || len(r.Status) != len(p.txs) {
|
||||
pr.Err = fmt.Errorf("probe %q: malformed result (gas=%d status=%d, want %d)",
|
||||
p.name, len(r.GasUsed), len(r.Status), len(p.txs))
|
||||
return pr
|
||||
}
|
||||
pr.GasUsed = r.GasUsed[0]
|
||||
pr.Status = r.Status[0]
|
||||
if pr.GasUsed == 0 {
|
||||
pr.Err = fmt.Errorf("probe %q: 0 gas — kernel did not execute", p.name)
|
||||
return pr
|
||||
}
|
||||
// Call-bridge probe: any returning status is fine on every backend.
|
||||
// CALL/CREATE/DELEGATECALL/STATICCALL/CREATE2 still emit
|
||||
// CallNotSupported across CPU and GPU paths today (V5 kernel work
|
||||
// pending). The probe's purpose is to confirm the dispatcher reached
|
||||
// the bridge and returned cleanly — not to assert successful CALL
|
||||
// execution. Under the old V2 wire shape this hid behind a status
|
||||
// mask; now that the canonical ExecuteBlock surfaces the real kernel
|
||||
// status, the exception is explicit.
|
||||
if p.callBridge {
|
||||
pr.OK = true
|
||||
return pr
|
||||
}
|
||||
if pr.Status != p.wantStatus {
|
||||
// Accept TxReturn for arith probes that end in RETURN; we don't
|
||||
// because arith ends in STOP. But a backend that maps STOP-with-no-
|
||||
// data to TxReturn instead of TxOK is acceptable: both indicate
|
||||
// successful termination. So treat TxOK and TxReturn as equivalent
|
||||
// here.
|
||||
if !(pr.Status == TxReturn && p.wantStatus == TxOK) &&
|
||||
!(pr.Status == TxOK && p.wantStatus == TxReturn) {
|
||||
pr.Err = fmt.Errorf("probe %q: status=%s want=%s",
|
||||
p.name, pr.Status, p.wantStatus)
|
||||
for i := range p.txs {
|
||||
pr.GasUsed += r.GasUsed[i]
|
||||
pr.Status = r.Status[i]
|
||||
if r.Status[i] != TxOK || r.GasUsed[i] != p.wantGas {
|
||||
pr.Err = fmt.Errorf("probe %q: tx %d came back %s at %d gas, want ok at %d",
|
||||
p.name, i, r.Status[i], r.GasUsed[i], p.wantGas)
|
||||
return pr
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
package cevm
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -156,23 +157,25 @@ func contains(s []Backend, b Backend) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// TestHealth runs the Health() battery and verifies every available backend
|
||||
// reports OK with non-zero gas. This is the production-readiness gate.
|
||||
// TestHealth runs the Health() battery: a funded plain transfer. A GPU lane
|
||||
// runs it to TxOK at 21000 gas; a CPU lane declines it, because the Go entry
|
||||
// runs nothing on the CPU without a host (go_bridge.h). This is the
|
||||
// production-readiness gate.
|
||||
func TestHealth(t *testing.T) {
|
||||
reports := Health()
|
||||
if len(reports) == 0 {
|
||||
t.Fatal("Health() returned no reports — runtime cannot enumerate backends")
|
||||
}
|
||||
for _, r := range reports {
|
||||
if !r.OK {
|
||||
t.Errorf("Health: backend %q failed: %v (probe=%s)", r.Name, r.Err, r.Probe)
|
||||
if r.Backend == GPUMetal || r.Backend == GPUCUDA {
|
||||
if !r.OK || r.GasUsed != 21000 {
|
||||
t.Errorf("Health: GPU lane %q: ok=%v gas=%d err=%v, want ok at 21000", r.Name, r.OK, r.GasUsed, r.Err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if r.GasUsed == 0 {
|
||||
t.Errorf("Health: backend %q probe %q reported 0 gas", r.Name, r.Probe)
|
||||
if r.OK || !errors.Is(r.Err, ErrDeclined) {
|
||||
t.Errorf("Health: CPU lane %q: ok=%v err=%v, want declined", r.Name, r.OK, r.Err)
|
||||
}
|
||||
t.Logf("Health: %s ok (probes=%d gas=%d time=%.2fms)",
|
||||
r.Name, r.ProbesRun, r.GasUsed, r.ExecTime)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
evmparallel "github.com/luxfi/evm/core/parallel"
|
||||
"github.com/luxfi/evm/core/state"
|
||||
"github.com/luxfi/geth/common"
|
||||
"github.com/luxfi/geth/consensus/misc/eip4844"
|
||||
"github.com/luxfi/geth/core/rawdb"
|
||||
"github.com/luxfi/geth/core/tracing"
|
||||
"github.com/luxfi/geth/core/types"
|
||||
@@ -28,7 +29,7 @@ import (
|
||||
|
||||
// fallbacks is how many declines crypto/backend has counted.
|
||||
func fallbacks() uint64 {
|
||||
return backend.FallbackCounters()[backend.FallbackUnsupported.String()]
|
||||
return backend.FallbackCounters()[backend.FallbackBackendUnavailable.String()]
|
||||
}
|
||||
|
||||
// A decline is luxfi/evm's "not handled", (nil, nil), on which the state
|
||||
@@ -384,7 +385,7 @@ func TestANilGasPriceIsZeroAndNotAPanic(t *testing.T) {
|
||||
// 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)
|
||||
excess := uint64(10_000_000)
|
||||
header := &types.Header{
|
||||
Time: 1717171717,
|
||||
Number: big.NewInt(9_000_001),
|
||||
@@ -394,7 +395,13 @@ func TestTheBlockContextIsTheHeader(t *testing.T) {
|
||||
Coinbase: common.Address{0xC0, 0xFF, 0xEE},
|
||||
MixDigest: common.Hash{0xAB, 0xCD},
|
||||
}
|
||||
config := ðparams.ChainConfig{ChainID: big.NewInt(96369)}
|
||||
cancun := uint64(0)
|
||||
config := ðparams.ChainConfig{
|
||||
ChainID: big.NewInt(96369),
|
||||
LondonBlock: big.NewInt(0),
|
||||
CancunTime: &cancun,
|
||||
BlobScheduleConfig: ðparams.BlobScheduleConfig{Cancun: ethparams.DefaultCancunBlobConfig},
|
||||
}
|
||||
|
||||
got, ok := blockContext(config, header)
|
||||
if !ok {
|
||||
@@ -415,8 +422,13 @@ func TestTheBlockContextIsTheHeader(t *testing.T) {
|
||||
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)
|
||||
// BLOBBASEFEE is the fee the excess blob gas prices, not the excess.
|
||||
if want := eip4844.CalcBlobFee(config, header).Uint64(); got.BlobBaseFee != want || want == excess {
|
||||
t.Errorf("BlobBaseFee = %d, want %d (the fee %d excess blob gas prices)", got.BlobBaseFee, want, excess)
|
||||
}
|
||||
// Before a blob schedule there is no blob fee.
|
||||
if got, _ := blockContext(ðparams.ChainConfig{ChainID: big.NewInt(96369)}, header); got.BlobBaseFee != 0 {
|
||||
t.Errorf("BlobBaseFee = %d with no blob schedule, want 0", got.BlobBaseFee)
|
||||
}
|
||||
if common.Address(got.Coinbase) != header.Coinbase {
|
||||
t.Errorf("Coinbase = %x, want %x", got.Coinbase, header.Coinbase)
|
||||
@@ -470,25 +482,46 @@ func TestAResultThatDoesNotMatchTheBlockIsRefused(t *testing.T) {
|
||||
{GasUsed: []uint64{21000, 21000}, Status: []cevm.TxStatus{cevm.TxOK}},
|
||||
{},
|
||||
} {
|
||||
if _, err := assemble(txs, newState(t), r, newHeader()); err == nil {
|
||||
if _, err := assemble(txs, 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CallNotSupported never reaches a caller, go_bridge.h says. One that does
|
||||
// is a result that is not the block's; mixing backends mid-block would execute
|
||||
// part of it on cevm and part on the Go EVM, so the whole block is declined.
|
||||
func TestATransactionTheKernelCannotRunDeclinesTheWholeBlock(t *testing.T) {
|
||||
// cevm answers ok=1 only when every tx is a valid plain transfer that
|
||||
// succeeded on its 21000 intrinsic gas (gpu_dispatch.cpp ran_through). Any
|
||||
// other answer — CallNotSupported, which never reaches a caller, a reverted or
|
||||
// returning status, a gas estimate at the tx's limit — is not the block's, and
|
||||
// the whole block is declined rather than part of it receipted.
|
||||
func TestOnlyAnAnswerOfPlainTransfersIsReceipted(t *testing.T) {
|
||||
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},
|
||||
ok := &cevm.BlockResult{GasUsed: []uint64{21000, 21000}, Status: []cevm.TxStatus{cevm.TxOK, cevm.TxOK}}
|
||||
if got, err := assemble(txs, ok, newHeader()); err != nil || len(got) != 2 {
|
||||
t.Fatalf("assemble(two transfers at 21000) = (%v, %v), want two receipts", got, err)
|
||||
}
|
||||
receipts, err := assemble(txs, newState(t), result, newHeader())
|
||||
if err != nil || receipts != nil {
|
||||
t.Fatalf("assemble = (%v, %v), want the block declined", receipts, err)
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
gas uint64
|
||||
status cevm.TxStatus
|
||||
}{
|
||||
{"call not supported", 21000, cevm.TxCallNotSupported},
|
||||
{"return", 21000, cevm.TxReturn},
|
||||
{"revert", 21000, cevm.TxRevert},
|
||||
{"error", 21000, cevm.TxError},
|
||||
{"a gas estimate at the limit", 100000, cevm.TxOK},
|
||||
{"one gas over intrinsic", 21001, cevm.TxOK},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := &cevm.BlockResult{
|
||||
GasUsed: []uint64{21000, tc.gas},
|
||||
Status: []cevm.TxStatus{cevm.TxOK, tc.status},
|
||||
}
|
||||
receipts, err := assemble(txs, result, newHeader())
|
||||
if err != nil || receipts != nil {
|
||||
t.Fatalf("assemble = (%v, %v), want the block declined: (nil, nil)", receipts, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -551,26 +584,42 @@ func TestOnlyAPlainValueTransferIsReconstructable(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// A block cevm cannot receipt is declined to the Go EVM rather than receipted
|
||||
// without the logs or contract address it would need.
|
||||
func TestABlockBeyondValueTransferIsDeclined(t *testing.T) {
|
||||
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},
|
||||
}
|
||||
receipts, err := assemble(txs, newState(t), result, newHeader())
|
||||
if err != nil || receipts != nil {
|
||||
t.Fatalf("assemble = (%v, %v), want the block declined: (nil, nil)", receipts, err)
|
||||
// A block cevm cannot receipt is declined to the Go EVM before cevm is asked:
|
||||
// a creation, calldata, or a recipient with code. cevm declines code anyway,
|
||||
// and no answer it gives could be receipted past a plain transfer.
|
||||
func TestABlockBeyondValueTransferIsDeclinedBeforeCevm(t *testing.T) {
|
||||
sdb := newState(t)
|
||||
to, withCode := common.Address{0x11}, common.Address{0x99}
|
||||
sdb.SetCode(withCode, []byte{0x00}, tracing.CodeChangeUnspecified)
|
||||
plain := types.NewTx(&types.LegacyTx{To: &to, Value: big.NewInt(1), Gas: 21000})
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
tx *types.Transaction
|
||||
}{
|
||||
{"a creation", types.NewTx(&types.LegacyTx{Gas: 53000})},
|
||||
{"calldata", types.NewTx(&types.LegacyTx{To: &to, Gas: 30000, Data: []byte{0x01}})},
|
||||
{"a recipient with code", types.NewTx(&types.LegacyTx{To: &withCode, Value: big.NewInt(1), Gas: 21000})},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
e := &Executor{
|
||||
execute: func(cevm.Backend, uint32, []cevm.Transaction, *cevm.BlockContext, []cevm.StateAccount) (*cevm.BlockResult, error) {
|
||||
t.Fatal("cevm was asked to run a block it could not receipt")
|
||||
return nil, nil
|
||||
},
|
||||
}
|
||||
txs := types.Transactions{plain, tc.tx}
|
||||
receipts, err := e.run(chainConfig(), newHeader(), txs, senders(len(txs)), sdb)
|
||||
if err != nil || receipts != nil {
|
||||
t.Fatalf("run = (%v, %v), want the block declined: (nil, nil)", receipts, 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.
|
||||
// header, so every field has to be what the Go EVM would have produced. The
|
||||
// reconstruction is read here for every status, though assemble only ever
|
||||
// hands it plain transfers at 21000.
|
||||
func TestReceiptsCarryEachTransactionsOwnResult(t *testing.T) {
|
||||
to := common.Address{0x11}
|
||||
txs := types.Transactions{
|
||||
@@ -584,10 +633,7 @@ func TestReceiptsCarryEachTransactionsOwnResult(t *testing.T) {
|
||||
}
|
||||
header := newHeader()
|
||||
|
||||
got, err := assemble(txs, newState(t), result, header)
|
||||
if err != nil {
|
||||
t.Fatalf("assemble: %v", err)
|
||||
}
|
||||
got := receipts(txs, result, header)
|
||||
if len(got) != len(txs) {
|
||||
t.Fatalf("got %d receipts for %d transactions", len(got), len(txs))
|
||||
}
|
||||
|
||||
@@ -17,28 +17,18 @@
|
||||
//
|
||||
// # Wiring
|
||||
//
|
||||
// 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,
|
||||
// })
|
||||
// Nothing registers this executor, and nothing may yet: it returns receipts
|
||||
// for a block cevm ran and writes no state. gpu_execute_block roots no state
|
||||
// and hands none back, so a block it runs leaves the StateDB as it was, and a
|
||||
// node that registered the executor would finalize that block on the parent's
|
||||
// state and reject it for its root.
|
||||
//
|
||||
// When it is registered, it is registered explicitly, with
|
||||
// parallel.RegisterExecutor(&cevmparallel.Executor{CevmBackend: ...}).
|
||||
// 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().
|
||||
//
|
||||
// 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.
|
||||
// no complaint. A package that registered itself on import would take that
|
||||
// slot depending only on link order — so this package has no init().
|
||||
//
|
||||
// # Execution
|
||||
//
|
||||
@@ -48,6 +38,9 @@
|
||||
// cevm.ExecuteBlock — one cgo call
|
||||
// assemble — what comes back, as receipts
|
||||
//
|
||||
// cevm's Go entry runs nothing on the CPU lanes (no host, and code is
|
||||
// declined), so only a GPU lane with a device ever returns a block.
|
||||
//
|
||||
// Parity contract: every receipt produced here must byte-equal the receipt
|
||||
// produced by Go EVM Block-STM for the same input tuple.
|
||||
//
|
||||
@@ -70,6 +63,7 @@ import (
|
||||
evmparallel "github.com/luxfi/evm/core/parallel"
|
||||
"github.com/luxfi/evm/core/state"
|
||||
"github.com/luxfi/geth/common"
|
||||
"github.com/luxfi/geth/consensus/misc/eip4844"
|
||||
"github.com/luxfi/geth/core/types"
|
||||
"github.com/luxfi/geth/core/vm"
|
||||
ethparams "github.com/luxfi/geth/params"
|
||||
@@ -87,7 +81,7 @@ import (
|
||||
// code, and go_bridge.h's contract is that the caller runs a declined block
|
||||
// itself.
|
||||
func declineBlock(reason string, blockNumber, txIndex uint64) ([]*types.Receipt, error) {
|
||||
backend.RecordFallback(backend.FallbackUnsupported, "cevm:"+reason)
|
||||
backend.RecordFallback(backend.FallbackBackendUnavailable, "cevm:"+reason)
|
||||
log.Debug("cevm: declining block to the Go EVM",
|
||||
"reason", reason, "block", blockNumber, "tx_index", txIndex)
|
||||
return nil, nil
|
||||
@@ -99,11 +93,10 @@ type execute func(cevm.Backend, uint32, []cevm.Transaction, *cevm.BlockContext,
|
||||
// Executor is a luxfi/evm/core/parallel.BlockExecutor that dispatches
|
||||
// every block to cevm.ExecuteBlock in one cgo call.
|
||||
type Executor struct {
|
||||
// CevmBackend selects the cevm execution lane:
|
||||
// cevm.CPUSequential — single-threaded CPU baseline (parity reference)
|
||||
// cevm.CPUParallel — Block-STM on CPU
|
||||
// cevm.GPUMetal — Metal kernel dispatch (M1/M2/M3)
|
||||
// cevm.GPUCUDA — CUDA kernel dispatch (NVIDIA)
|
||||
// CevmBackend selects the cevm lane: cevm.GPUMetal (Apple silicon) or
|
||||
// cevm.GPUCUDA (NVIDIA). The CPU lanes, cevm.CPUSequential and
|
||||
// cevm.CPUParallel, decline every block through cevm's Go entry, which
|
||||
// passes no host.
|
||||
CevmBackend cevm.Backend
|
||||
|
||||
// Threads is the worker count for parallel backends. Ignored by
|
||||
@@ -173,6 +166,12 @@ func (e *Executor) run(
|
||||
if i < len(txs) {
|
||||
return declineBlock("tx_not_representable", header.Number.Uint64(), uint64(i))
|
||||
}
|
||||
// cevm declines a batch with code, and its answer could not be receipted
|
||||
// past a plain value transfer anyway: neither is worth a snapshot and a
|
||||
// cgo call.
|
||||
if i := firstBeyondValueTransfer(txs, statedb); i < len(txs) {
|
||||
return declineBlock("non_value_transfer", header.Number.Uint64(), uint64(i))
|
||||
}
|
||||
blockCtx, ok := blockContext(config, header)
|
||||
if !ok {
|
||||
return declineBlock("block_context_overflow_uint64", header.Number.Uint64(), 0)
|
||||
@@ -190,7 +189,7 @@ func (e *Executor) run(
|
||||
result, err := call(e.CevmBackend, threads, cevmTxs, &blockCtx, snapshot)
|
||||
switch {
|
||||
case err == nil:
|
||||
return assemble(txs, statedb, result, header)
|
||||
return assemble(txs, result, header)
|
||||
case errors.Is(err, cevm.ErrDeclined):
|
||||
// ok=0: no gas or status in it is the block's, so none is read.
|
||||
return declineBlock("cevm_declined", header.Number.Uint64(), 0)
|
||||
@@ -275,8 +274,8 @@ func fits(v *big.Int) (uint64, bool) {
|
||||
// block sees: what TIMESTAMP, NUMBER, CHAINID, BASEFEE, COINBASE, GASLIMIT,
|
||||
// PREVRANDAO and BLOBBASEFEE answer.
|
||||
//
|
||||
// It reports false when the chain id or the base fee does not fit cevm's
|
||||
// 64-bit wire. A base fee carried as anything else would let cevm pass a tx
|
||||
// It reports false when the chain id, the base fee or the blob base fee does
|
||||
// not fit cevm's 64-bit wire. A base fee carried as anything else would let cevm pass a tx
|
||||
// priced below the real one, which the EVM rejects.
|
||||
func blockContext(config *ethparams.ChainConfig, header *types.Header) (cevm.BlockContext, bool) {
|
||||
chainID, ok := fits(config.ChainID)
|
||||
@@ -294,8 +293,14 @@ func blockContext(config *ethparams.ChainConfig, header *types.Header) (cevm.Blo
|
||||
ChainID: chainID,
|
||||
BaseFee: baseFee,
|
||||
}
|
||||
// BLOBBASEFEE is the fee the header's excess blob gas prices, under the
|
||||
// chain's blob schedule; none before Cancun.
|
||||
if header.ExcessBlobGas != nil {
|
||||
ctx.BlobBaseFee = *header.ExcessBlobGas
|
||||
fee, ok := fits(eip4844.CalcBlobFee(config, header))
|
||||
if !ok {
|
||||
return cevm.BlockContext{}, false
|
||||
}
|
||||
ctx.BlobBaseFee = fee
|
||||
}
|
||||
copy(ctx.Coinbase[:], header.Coinbase.Bytes())
|
||||
// Prevrandao = post-merge MixDigest. Pre-merge headers carry zero
|
||||
@@ -304,14 +309,14 @@ func blockContext(config *ethparams.ChainConfig, header *types.Header) (cevm.Blo
|
||||
return ctx, true
|
||||
}
|
||||
|
||||
// assemble turns what cevm returned into receipts, or declines the block.
|
||||
// assemble turns what cevm returned for a block of plain value transfers 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) {
|
||||
@@ -320,27 +325,25 @@ func assemble(
|
||||
len(result.GasUsed), len(result.Status), len(txs))
|
||||
}
|
||||
|
||||
// CallNotSupported is cevm's internal hand-off signal and go_bridge.h
|
||||
// says it never reaches a caller. One that does is a result that is not
|
||||
// the block's: decline rather than mix backends mid-block.
|
||||
for i, st := range result.Status {
|
||||
if st == cevm.TxCallNotSupported {
|
||||
return declineBlock("call_or_create_unsupported", header.Number.Uint64(), uint64(i))
|
||||
// cevm answers ok=1 only for a batch its value-transfer paths ran through,
|
||||
// every tx a valid plain transfer that succeeded on its intrinsic gas
|
||||
// (go_bridge.h; gpu_dispatch.cpp ran_through). Any other status or gas —
|
||||
// CallNotSupported, which never reaches a caller, a gas estimate, a
|
||||
// kernel's own answer — is not that, and not the block's: decline rather
|
||||
// than receipt it. This also holds a library that answers ok=1 on other
|
||||
// terms to the one answer this executor can use.
|
||||
for i := range txs {
|
||||
if result.Status[i] != cevm.TxOK || result.GasUsed[i] != ethparams.TxGas {
|
||||
return declineBlock("result_not_value_transfer", header.Number.Uint64(), uint64(i))
|
||||
}
|
||||
}
|
||||
|
||||
// The 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_no_logs_in_abi", header.Number.Uint64(), uint64(i))
|
||||
}
|
||||
return receipts(txs, result, header), nil
|
||||
}
|
||||
|
||||
// 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.
|
||||
// every one does. The ABI returns (gas_used, status) and nothing else, so a
|
||||
// receipt is only reconstructable for a plain value transfer.
|
||||
//
|
||||
// Three things put a transaction beyond that:
|
||||
//
|
||||
|
||||
@@ -47,8 +47,9 @@ func TestABatchWithCodeIsDeclinedOnEveryBackend(t *testing.T) {
|
||||
}
|
||||
|
||||
// A block of plain transfers from funded senders, end to end through the
|
||||
// executor: a lane that runs it receipts every transfer at 21000 gas; a lane
|
||||
// that does not declines it to the Go EVM. Neither is an error.
|
||||
// executor: a GPU lane runs it and receipts every transfer at 21000 gas; a
|
||||
// CPU lane, which runs nothing through the Go entry, declines it to the Go
|
||||
// EVM. Neither is an error.
|
||||
func TestAPlainTransferBlockIsReceiptedOrDeclined(t *testing.T) {
|
||||
const n = 8
|
||||
sdb := newState(t)
|
||||
@@ -70,10 +71,15 @@ func TestAPlainTransferBlockIsReceiptedOrDeclined(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("run: %v", err)
|
||||
}
|
||||
if receipts == nil {
|
||||
t.Logf("%s declined the block to the Go EVM", cevm.BackendName(b))
|
||||
if b != cevm.GPUMetal && b != cevm.GPUCUDA {
|
||||
if receipts != nil {
|
||||
t.Fatalf("CPU lane %s receipted a block; it runs none", cevm.BackendName(b))
|
||||
}
|
||||
return
|
||||
}
|
||||
if receipts == nil {
|
||||
t.Fatalf("GPU lane %s declined a block of funded plain transfers", cevm.BackendName(b))
|
||||
}
|
||||
if len(receipts) != n {
|
||||
t.Fatalf("%d receipts for %d transactions", len(receipts), n)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user