The cevm executor runs what cevm declines on the Go EVM

cevm's Go entry is gpu_execute_block (go_bridge.h, ABI 6); the
gpu_execute_block_v4 and gpu_free_result_v2 this package called are gone
from the library, so the native build did not link. The entry answers ok=0
for any block it does not run — any batch with code, and any refusal on
balance, nonce, price, block gas limit, revision or hashes — and that block
is the caller's. ExecuteBlock returns ErrDeclined for it, with no result,
and the executor declines the block to the Go EVM: (nil, nil), counted. A
decline is never an error, so CEVM_STRICT and ErrGPUEVMRequired are gone;
the state processor fell through on that error anyway.

Nothing wider than the 64-bit wire reaches cevm. A gas price or fee cap
above 2^64-1 crossed as 0; it, a value, a base fee or a chain id that does
not fit now declines the block, as does a tx the wire drops part of: an
access list, a blob or set-code tx, a tip above its fee cap.

Snapshot rows carry what the header asks for: keccak256 of nothing and the
empty trie root for an account that does not exist — the StateDB answers
zero, which cevm reads as saying nothing and declines — the real storage
root otherwise, and a row for the coinbase. cevm_cgo.go checks the four C
structs' sizes at compile time, so a header that grows a field cannot have
it cross as zero unnoticed.

The call into cevm is a field on the Executor, so the tests show a declined
block going to the Go EVM and a value of 2^64 never reaching cevm without
the library.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-09-23 09:22:10 -07:00
co-authored by Hanzo Dev
parent c1e4934ab7
commit 6223789846
7 changed files with 630 additions and 442 deletions
+8 -16
View File
@@ -172,26 +172,19 @@ Expected single log line:
[crypto/backend] GPU fallback: reason=disabled where=clob
```
## 8. cevm GPU EVM strict mode
## 8. cevm declines to the Go EVM
The cevm Go EVM fallback is now opt-OUT (strict by default). To verify the
strict policy:
cevm's Go entry (`gpu_execute_block`, go_bridge.h ABI 6) answers ok=0 for any
block it does not run — any batch with code, and any refusal on balance, nonce,
price, block gas limit, revision or hashes. The executor in
`evm/cevm/parallel` then returns `(nil, nil)`, and the Go EVM runs the block.
There is no switch: a decline is never an error.
```bash
cd ~/work/lux/chains
go test -count=1 ./evm/cevm/parallel/
```
To re-enable the legacy fallback during V5 transition (emergency rollback):
```bash
CEVM_STRICT=0 go test -count=1 ./evm/cevm/parallel/
```
V5 kernel work tracked at `chains/evm/cevm/V5_ABI.md`. Until that lands,
strict mode treats any CALL/CREATE block as `ErrGPUEVMRequired` — the lie
of silent Go-EVM shadowing is removed.
## 9. Full sweep (build everything, run everything)
```bash
@@ -242,9 +235,8 @@ overhead dominates and CPU wins.
## What's still on the luxcpp roadmap (not blocking this PR)
- **cevm V5 kernel**: CALL/CREATE on device per `chains/evm/cevm/V5_ABI.md`.
Without V5, strict-mode cevm refuses any CALL-containing block. Operators
needing the legacy fallback during the V5 transition set
`CEVM_STRICT=0`.
Until a Go entry runs code, cevm declines every block with code and the Go
EVM runs it.
- **Precompile-engine → MatchOrderGPU bridge**: the `lux/precompile/dex`
engine's matching path uses its own embedded path today. Hooking it to
`MatchOrderGPU` is one small Go change in `engine_embedded.go::Swap` /
+52 -35
View File
@@ -24,9 +24,9 @@
// tx.Code) that the C side dereferences, and is unpinned via defer
// after the C call returns — including on the error path.
//
// 2. The C result is freed via defer (gpu_free_result / gpu_free_result_v2)
// on every code path including failure. Gas/status arrays are copied
// into Go-owned slices before the result is freed.
// 2. The C result is freed via defer (gpu_free_result) on every code path
// including failure. Gas/status arrays are copied into Go-owned slices
// before the result is freed.
//
// 3. The C++ engine uses a thread_local engine cache (one per OS thread
// reached by goroutines via cgo) for the Keccak hasher; per-instance
@@ -72,9 +72,19 @@ import (
// 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
// and the post-execution state root.
// ErrDeclined is what ExecuteBlock returns when the library answers ok=0
// (go_bridge.h): the result is not the block's, and the caller runs the block
// on its own EVM. cevm declines any batch in which a transaction carries code,
// and any batch its value-transfer paths cannot run as the EVM does — a
// refusal on balance, nonce, price, block gas limit or revision, a snapshot
// row without its hashes, or a device that failed.
//
// A declined result carries no gas or status a caller may use, so ExecuteBlock
// returns none with it.
var ErrDeclined = errors.New("cevm: declined the block (ok=0); the caller runs it")
// BlockResult is what ExecuteBlock returns for a block cevm ran: per-tx gas
// and status. StateRoot is zero: gpu_execute_block roots no state.
type BlockResult struct {
StateRoot [32]byte
GasUsed []uint64
@@ -86,6 +96,7 @@ type BlockResult struct {
ABIVersion uint32
}
// Backend selects the C++ EVM execution mode.
type Backend int
const (
@@ -117,16 +128,19 @@ func (b Backend) String() string {
// Transaction is a single EVM transaction to execute.
//
// When Code is non-empty AND a GPU backend is selected, the C++ EVM
// dispatches each tx through the parallel opcode interpreter (Metal:
// kernel::EvmKernelHost, CUDA: cuda::EvmKernel). When Code is empty, GPU
// backends use the scheduler-only Block-STM kernel.
// cevm runs a batch only when it is plain value transfers on a GPU backend's
// value-transfer path: a batch in which any transaction carries Code is
// declined (ErrDeclined), because gpu_execute_block passes no host and would
// run that code as a message, on its whole limit, which is not the tx's gas.
//
// Value and GasPrice are 64-bit on the wire. A transaction whose value or
// price does not fit is the caller's to run; it never belongs in a batch.
type Transaction struct {
From [20]byte
To [20]byte
HasTo bool
Data []byte // Calldata
Code []byte // EVM bytecode (optional — required for real GPU execution)
Code []byte // The recipient's code; any makes cevm decline the batch
GasLimit uint64
Value uint64
Nonce uint64
@@ -176,8 +190,10 @@ func (s TxStatus) String() string {
// to 0, timestamp to 0, etc., which matches the dispatcher's pre-v0.26
// behaviour.
//
// Field layout matches the C-side CBlockContext byte-for-byte: this struct
// is passed to the C ABI via direct memcpy, no field-by-field translation.
// Field layout matches the C-side CBlockContext byte-for-byte. The binding
// copies it into a CBlockContext field by field, and the C side memcpy's that
// into evm::gpu::BlockContext; the Go layout is pinned to the C one all the
// same (TestBlockContextIsTheWireLayout, and the size check in cevm_cgo.go).
// Field order MUST match go_bridge.h CBlockContext exactly. Adding new
// fields requires bumping ABIVersion and the C-side EVM_GPU_ABI_VERSION
// in lockstep.
@@ -196,32 +212,33 @@ type BlockContext struct {
NumBlobHashes uint32
}
// ABIVersion is the C ABI version this Go module expects. Compare against
// the loaded library's gpu_abi_version() to detect version skew.
//
// v5 (v0.26.0): added gpu_execute_block_v3 with CBlockContext (TIMESTAMP,
// NUMBER, CHAINID, BASEFEE, etc.) and per-tx status[] in BlockResult. V2
// callers still work; only ExecuteBlock sees the new BlockContext fields.
//
// v6: added gpu_execute_block_v4 + CGpuStateAccount. Callers can now hand
// the GPU a state snapshot (account nonce, balance, code, code_hash) so
// the kernel CALL/CREATE path resolves targets on-device instead of
// returning CallNotSupported. V3 callers still see the same wire shape.
//
// ABIVersion (cevm_cgo.go / cevm_nocgo.go) is the C ABI this build links:
// go_bridge.h's EVM_GPU_ABI_VERSION, 6. v6 is one entry, gpu_execute_block,
// taking a block context and a state snapshot (CGpuStateAccount) and
// answering per-tx gas and status with ok, freed by gpu_free_result. The
// versioned _v2/_v3/_v4 entries are gone.
// StateAccount is one entry in the snapshot of touched accounts handed to
// ExecuteBlock. Fields mirror the C-side CGpuStateAccount byte-for-byte
// (modulo the inline `Code` slice which the binding flattens into a single
// blob before crossing the cgo boundary).
// ExecuteBlock, the state before the block. Fields mirror the C-side
// CGpuStateAccount (modulo the inline `Code` slice which the binding flattens
// into a single blob before crossing the cgo boundary).
//
// The snapshot must hold every account a transaction touches: cevm takes an
// account it lacks not to exist, so a transfer to a contract left out runs as
// a transfer to an empty account.
//
// Address is canonical 20-byte big-endian. Balance is little-endian limbs
// (Balance[0] = low 64 bits). Code may be nil for EOAs — empty code is the
// EOA marker. CodeHash should be keccak256(code); the dispatcher does not
// recompute it because callers usually have it cached on the StateDB side.
// EOA marker. CodeHash is keccak256(code), keccak256 of nothing
// (types.EmptyCodeHash) for an account with no code, including one that does
// not exist; zero says nothing, and cevm declines a batch whose sender or
// recipient row carries it. StorageRoot is the account's storage trie root,
// types.EmptyRootHash for an account with no storage.
type StateAccount struct {
Address [20]byte
Nonce uint64
Balance [4]uint64
Code []byte
CodeHash [32]byte
Address [20]byte
Nonce uint64
Balance [4]uint64
Code []byte
CodeHash [32]byte
StorageRoot [32]byte
}
+37 -16
View File
@@ -54,14 +54,31 @@ func AutoDetect() Backend {
return Backend(C.gpu_auto_detect_backend())
}
// init validates the ABI version of the loaded shared library against the Go
// module's expected ABIVersion. A mismatch means the binary and the cevm
// module were built against incompatible C++ headers and any execution would
// produce silently wrong results — fail fast at process start instead.
// ABIVersion is the ABI this build was compiled against, taken from the
// library's own header. There is no second copy to keep in sync.
const ABIVersion = uint32(C.EVM_GPU_ABI_VERSION)
// The C structs this file fills and reads, at the sizes go_bridge.h (ABI 6)
// gives them on LP64. A header that adds, drops or widens a field changes a
// size, and then this file does not compile until it has been read against
// the new header: a field it never sets would otherwise cross as zero, which
// is how storage_root went unset. Each pair fails when the size is larger
// (the first) or smaller (the second).
var (
_ [unsafe.Sizeof(C.CGpuTx{}) - 112]struct{}
_ [112 - unsafe.Sizeof(C.CGpuTx{})]struct{}
_ [unsafe.Sizeof(C.CGpuStateAccount{}) - 136]struct{}
_ [136 - unsafe.Sizeof(C.CGpuStateAccount{})]struct{}
_ [unsafe.Sizeof(C.CGpuBlockResult{}) - 88]struct{}
_ [88 - unsafe.Sizeof(C.CGpuBlockResult{})]struct{}
_ [unsafe.Sizeof(C.CBlockContext{}) - unsafe.Sizeof(BlockContext{})]struct{}
_ [unsafe.Sizeof(BlockContext{}) - unsafe.Sizeof(C.CBlockContext{})]struct{}
)
// init validates the ABI version of the loaded shared library against the Go
// module's expected ABIVersion. A mismatch means the binary and the cevm
// module were built against incompatible C++ headers and any execution would
// produce silently wrong results — fail fast at process start instead.
func init() {
got := uint32(C.gpu_abi_version())
if got != ABIVersion {
@@ -124,9 +141,13 @@ func copyU64(ptr *C.uint64_t, want uint32) []uint64 {
return dst
}
// ExecuteBlock runs txs against backend and returns the block result. A nil ctx
// leaves the C side on its own block defaults; nil state means no prestate is
// seeded. numThreads is passed through to the C executor.
// ExecuteBlock runs txs against backend through gpu_execute_block and returns
// per-tx gas and status. A nil ctx leaves the C side on a zero block context;
// state is the state before the block and must hold every account a tx
// touches (see StateAccount). numThreads is passed through to the C executor.
//
// When the library declines the block (ok=0) ExecuteBlock returns ErrDeclined
// and no result: the caller runs the block on its own EVM.
func ExecuteBlock(backend Backend, numThreads uint32, txs []Transaction, ctx *BlockContext, state []StateAccount) (*BlockResult, error) {
if len(txs) == 0 {
return &BlockResult{ABIVersion: ABIVersion}, nil
@@ -136,7 +157,7 @@ func ExecuteBlock(backend Backend, numThreads uint32, txs []Transaction, ctx *Bl
defer pinner.Unpin()
ctxs := buildTxs(txs, &pinner)
// Build the BlockContext mirror (same shape as V3).
// Build the CBlockContext.
var cctxStorage C.CBlockContext
var cctxPtr *C.CBlockContext
if ctx != nil {
@@ -185,10 +206,7 @@ func ExecuteBlock(backend Backend, numThreads uint32, txs []Transaction, ctx *Bl
cAccts[i].balance[j] = C.uint64_t(a.Balance[j])
}
cAccts[i].code_hash = *(*[32]C.uint8_t)(unsafe.Pointer(&a.CodeHash[0]))
// storage_root is left zero — the GPU CALL path doesn't
// consume it yet (LP-108 P5 reads code only). Filled in
// by the trie commit pass on the cevm side; safe to leave
// zero here for the dispatch hand-off.
cAccts[i].storage_root = *(*[32]C.uint8_t)(unsafe.Pointer(&a.StorageRoot[0]))
if n := len(a.Code); n > 0 {
cAccts[i].code_off = C.uint32_t(len(codeBlob))
cAccts[i].code_size = C.uint32_t(n)
@@ -206,26 +224,29 @@ func ExecuteBlock(backend Backend, numThreads uint32, txs []Transaction, ctx *Bl
}
}
result := C.gpu_execute_block_v4(
result := C.gpu_execute_block(
&ctxs[0],
C.uint32_t(len(ctxs)),
C.uint8_t(backend),
C.uint32_t(numThreads),
C.uint8_t(12), // EVM_GPU_REV_CANCUN
C.uint8_t(C.EVM_GPU_REV_CANCUN), // the one revision the kernels implement
cctxPtr,
cAcctsPtr,
C.uint32_t(len(state)),
codePtr,
C.uint32_t(codeSize),
)
defer C.gpu_free_result_v2(&result)
defer C.gpu_free_result(&result)
runtime.KeepAlive(ctxs)
runtime.KeepAlive(cctxStorage)
runtime.KeepAlive(cAccts)
runtime.KeepAlive(codeBlob)
// ok=0 names no gas or status the caller may use (every status reads
// EVM_GPU_TX_ERROR, and the arrays may be NULL), so nothing below it is
// read: the block goes back to the caller whole.
if result.ok == 0 {
return nil, fmt.Errorf("cevm: execute_block_v4 failed")
return nil, ErrDeclined
}
if uint32(result.abi_version) != ABIVersion {
return nil, fmt.Errorf("cevm: ABI version mismatch in result (lib=%d expected=%d)",
+3 -2
View File
@@ -25,8 +25,9 @@ func BackendName(b Backend) string { return b.String() }
// LibraryABIVersion returns the Go-side constant when there's no library.
func LibraryABIVersion() uint32 { return ABIVersion }
// ExecuteBlock returns an error when built without CGo. Mirrors the V4
// cgo signature so consumers can call it unconditionally.
// ExecuteBlock returns ErrNotLinked for any transactions: no library is
// linked. It mirrors the cgo signature so consumers can call it
// unconditionally.
func ExecuteBlock(backend Backend, numThreads uint32, txs []Transaction, ctx *BlockContext, state []StateAccount) (*BlockResult, error) {
if len(txs) == 0 {
return &BlockResult{ABIVersion: ABIVersion}, nil
+317 -130
View File
@@ -6,13 +6,12 @@ package parallel
import (
"errors"
"math/big"
"os"
"strings"
"testing"
"github.com/holiman/uint256"
"github.com/luxfi/chains/evm/cevm"
"github.com/luxfi/crypto/backend"
evmparallel "github.com/luxfi/evm/core/parallel"
"github.com/luxfi/evm/core/state"
"github.com/luxfi/geth/common"
@@ -24,79 +23,156 @@ import (
)
// -----------------------------------------------------------------------------
// The declining policy
// Declining
// -----------------------------------------------------------------------------
// 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)
}
})
// fallbacks is how many declines crypto/backend has counted.
func fallbacks() uint64 {
return backend.FallbackCounters()[backend.FallbackUnsupported.String()]
}
// A decline is luxfi/evm's "not handled", (nil, nil), on which the state
// processor runs the block on its sequential Go EVM — never an error, which
// would say the block is bad. It is counted, so a lane that declines
// everything is visible.
func TestADeclineFallsThroughToTheGoEVMAndIsCounted(t *testing.T) {
before := fallbacks()
receipts, err := declineBlock("cevm_declined", 42, 7)
if err != nil || receipts != nil {
t.Fatalf("declineBlock = (%v, %v), want (nil, nil) so the caller runs the block", receipts, err)
}
if fallbacks() != before+1 {
t.Errorf("fallback counter moved %d → %d, want one more", before, fallbacks())
}
}
// 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)
// ok=0 from the library is ErrDeclined, and the block goes to the Go EVM:
// (nil, nil). Whatever came back with the decline is not read — a declined
// result's statuses all say Error and its gas is the tx's whole limit, so
// receipting it would fail every transaction in a block that did not fail.
func TestABlockCevmDeclinesRunsOnTheGoEVM(t *testing.T) {
txs := types.Transactions{transfer(t, 0, big.NewInt(1)), transfer(t, 1, big.NewInt(1))}
calls := 0
e := &Executor{
CevmBackend: cevm.GPUCUDA,
execute: func(cevm.Backend, uint32, []cevm.Transaction, *cevm.BlockContext, []cevm.StateAccount) (*cevm.BlockResult, error) {
calls++
return &cevm.BlockResult{
GasUsed: []uint64{21000, 21000},
Status: []cevm.TxStatus{cevm.TxError, cevm.TxError},
}, cevm.ErrDeclined
},
}
if !strictGPUEVM() {
t.Fatal("with CEVM_STRICT unset the fallback is enabled; the default must be strict")
before := fallbacks()
receipts, err := e.run(chainConfig(), newHeader(), txs, senders(len(txs)), newState(t))
if err != nil || receipts != nil {
t.Fatalf("a declined block = (%v, %v), want (nil, nil): the Go EVM runs it", receipts, err)
}
if calls != 1 {
t.Fatalf("cevm was called %d times, want 1", calls)
}
if fallbacks() != before+1 {
t.Errorf("the decline was not counted")
}
}
// 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)
// A failure that is not a decline — a result for another ABI, say — is an
// error: the library broke its contract, which is not the same as refusing
// the block.
func TestACevmFailureThatIsNotADeclineIsAnError(t *testing.T) {
broken := errors.New("cevm: ABI version mismatch in result")
e := &Executor{
execute: func(cevm.Backend, uint32, []cevm.Transaction, *cevm.BlockContext, []cevm.StateAccount) (*cevm.BlockResult, error) {
return nil, broken
},
}
txs := types.Transactions{transfer(t, 0, big.NewInt(1))}
receipts, err := e.run(chainConfig(), newHeader(), txs, senders(len(txs)), newState(t))
if !errors.Is(err, broken) {
t.Fatalf("run = (%v, %v), want the library's error", receipts, 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.Errorf("an error came with %d receipts", len(receipts))
}
}
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 block cevm ran comes back as receipts built from its result.
func TestABlockCevmRanIsReceipted(t *testing.T) {
txs := types.Transactions{transfer(t, 0, big.NewInt(1)), transfer(t, 1, big.NewInt(1))}
e := &Executor{
execute: func(cevm.Backend, uint32, []cevm.Transaction, *cevm.BlockContext, []cevm.StateAccount) (*cevm.BlockResult, error) {
return &cevm.BlockResult{
GasUsed: []uint64{21000, 21000},
Status: []cevm.TxStatus{cevm.TxOK, cevm.TxOK},
}, nil
},
}
receipts, err := e.run(chainConfig(), newHeader(), txs, senders(len(txs)), newState(t))
if err != nil {
t.Fatalf("run: %v", err)
}
if len(receipts) != len(txs) || receipts[1].CumulativeGasUsed != 42000 {
t.Fatalf("receipts = %v, want two at 21000 each", receipts)
}
}
// Nothing wider than cevm's 64-bit wire reaches cevm. A value or a price of
// 2^64 is legal; truncated to 0 it is another transaction, so the block goes
// to the Go EVM and cevm is never called. The same holds for a tx the wire
// drops part of, and for a base fee or chain id that does not fit.
func TestWhatDoesNotFitTheWireNeverReachesCevm(t *testing.T) {
huge := new(big.Int).Lsh(big.NewInt(1), 64)
to := common.Address{0x11}
for _, tc := range []struct {
name string
tx *types.Transaction
header func(*types.Header)
config *ethparams.ChainConfig
}{
{name: "value 2^64", tx: types.NewTx(&types.LegacyTx{To: &to, Value: huge, Gas: 21000, GasPrice: big.NewInt(1)})},
{name: "gas price 2^64", tx: types.NewTx(&types.LegacyTx{To: &to, Value: big.NewInt(1), Gas: 21000, GasPrice: huge})},
{name: "fee cap 2^64", tx: types.NewTx(&types.DynamicFeeTx{To: &to, Value: big.NewInt(1), Gas: 21000, GasFeeCap: huge, GasTipCap: big.NewInt(1)})},
{
name: "an access list",
tx: types.NewTx(&types.AccessListTx{
To: &to, Value: big.NewInt(1), Gas: 23400, GasPrice: big.NewInt(1),
AccessList: types.AccessList{{Address: common.Address{0x22}}},
}),
},
{name: "tip above fee cap", tx: types.NewTx(&types.DynamicFeeTx{To: &to, Value: big.NewInt(1), Gas: 21000, GasFeeCap: big.NewInt(1), GasTipCap: big.NewInt(2)})},
{
name: "base fee 2^64",
tx: transfer(t, 0, big.NewInt(1)),
header: func(h *types.Header) { h.BaseFee = new(big.Int).Set(huge) },
},
{
name: "chain id 2^64",
tx: transfer(t, 0, big.NewInt(1)),
config: &ethparams.ChainConfig{ChainID: new(big.Int).Set(huge)},
},
} {
t.Run(tc.name, func(t *testing.T) {
e := &Executor{
CevmBackend: cevm.GPUCUDA,
execute: func(cevm.Backend, uint32, []cevm.Transaction, *cevm.BlockContext, []cevm.StateAccount) (*cevm.BlockResult, error) {
t.Fatal("cevm was called with a block its wire cannot carry")
return nil, nil
},
}
header := newHeader()
if tc.header != nil {
tc.header(header)
}
config := chainConfig()
if tc.config != nil {
config = tc.config
}
txs := types.Transactions{transfer(t, 0, big.NewInt(1)), tc.tx}
receipts, err := e.run(config, header, txs, senders(len(txs)), newState(t))
if err != nil || receipts != nil {
t.Fatalf("run = (%v, %v), want (nil, nil): the Go EVM runs the block", receipts, err)
}
})
}
}
@@ -105,31 +181,21 @@ func TestDecliningIsARefusalUnderStrictAndAFallThroughOtherwise(t *testing.T) {
// -----------------------------------------------------------------------------
// 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.
// hard-fail: the absence of the library is a property of the build, and every
// block then belongs to the Go EVM.
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)
t.Fatalf("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
@@ -223,29 +289,82 @@ func TestShapeCarriesEachTransactionsOwnFields(t *testing.T) {
}
}
// 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) {
// cevm.Transaction holds Value and GasPrice as uint64. A value or price 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 TestAValueOrPriceTooLargeForTheWireIsDeclinedAtItsIndex(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)
for _, tc := range []struct {
name string
wide, exact *types.Transaction
}{
{
name: "value",
wide: types.NewTx(&types.LegacyTx{Value: huge, Gas: 21000, GasPrice: big.NewInt(1)}),
exact: types.NewTx(&types.LegacyTx{Value: max, Gas: 21000, GasPrice: big.NewInt(1)}),
},
{
name: "legacy price",
wide: types.NewTx(&types.LegacyTx{Value: big.NewInt(1), Gas: 21000, GasPrice: huge}),
exact: types.NewTx(&types.LegacyTx{Value: big.NewInt(1), Gas: 21000, GasPrice: max}),
},
{
name: "fee cap",
wide: types.NewTx(&types.DynamicFeeTx{Value: big.NewInt(1), Gas: 21000, GasFeeCap: huge, GasTipCap: big.NewInt(1)}),
exact: types.NewTx(&types.DynamicFeeTx{Value: big.NewInt(1), Gas: 21000, GasFeeCap: max, GasTipCap: big.NewInt(1)}),
},
} {
t.Run(tc.name, func(t *testing.T) {
first := types.NewTx(&types.LegacyTx{Value: big.NewInt(1), Gas: 21000, GasPrice: big.NewInt(1)})
if _, i := shape(types.Transactions{first, tc.wide}, senders(2), newState(t)); i != 1 {
t.Fatalf("shape declined at index %d, want 1 (the transaction that overflows)", i)
}
// One below the boundary still fits, so the refusal is about the
// width and not about "large".
got, i := shape(types.Transactions{first, tc.exact}, senders(2), newState(t))
if i != 2 {
t.Fatalf("shape declined 2^64-1 at index %d; it is exactly representable", i)
}
if got[1].Value != tc.exact.Value().Uint64() || got[1].GasPrice != tc.exact.GasPrice().Uint64() {
t.Errorf("shaped value/price = %d/%d, want %d/%d", got[1].Value, got[1].GasPrice,
tc.exact.Value().Uint64(), tc.exact.GasPrice().Uint64())
}
})
}
if got[1].Value != max.Uint64() {
t.Errorf("value = %d, want %d", got[1].Value, max.Uint64())
}
// CGpuTx has one price and no access list, blob hashes, authorizations or tip.
// A tx that carries any of them is charged for them, or invalid by them, where
// cevm would not see it: it is declined at its index.
func TestATransactionTheWireDropsPartOfIsDeclinedAtItsIndex(t *testing.T) {
to := common.Address{0x11}
plain := types.NewTx(&types.LegacyTx{To: &to, Value: big.NewInt(1), Gas: 21000, GasPrice: big.NewInt(1)})
for _, tc := range []struct {
name string
tx *types.Transaction
want int
}{
{"legacy", plain, 2},
{"access-list type, empty list", types.NewTx(&types.AccessListTx{To: &to, Gas: 21000, GasPrice: big.NewInt(1)}), 2},
{"dynamic fee, tip at the cap", types.NewTx(&types.DynamicFeeTx{To: &to, Gas: 21000, GasFeeCap: big.NewInt(2), GasTipCap: big.NewInt(2)}), 2},
{
"an access list",
types.NewTx(&types.AccessListTx{
To: &to, Gas: 25300, GasPrice: big.NewInt(1),
AccessList: types.AccessList{{Address: common.Address{0x22}, StorageKeys: []common.Hash{{0x01}}}},
}),
1,
},
{"tip above the fee cap", types.NewTx(&types.DynamicFeeTx{To: &to, Gas: 21000, GasFeeCap: big.NewInt(1), GasTipCap: big.NewInt(2)}), 1},
} {
t.Run(tc.name, func(t *testing.T) {
if _, i := shape(types.Transactions{plain, tc.tx}, senders(2), newState(t)); i != tc.want {
t.Fatalf("shape stopped at %d, want %d", i, tc.want)
}
})
}
}
@@ -277,7 +396,10 @@ func TestTheBlockContextIsTheHeader(t *testing.T) {
}
config := &ethparams.ChainConfig{ChainID: big.NewInt(96369)}
got := blockContext(config, header)
got, ok := blockContext(config, header)
if !ok {
t.Fatal("blockContext refused a header whose every field fits")
}
if got.ChainID != 96369 {
t.Errorf("ChainID = %d, want 96369 — a block executed against the wrong chain id "+
@@ -309,21 +431,27 @@ func TestTheBlockContextIsTheHeader(t *testing.T) {
// every historical block.
func TestAHeaderWithoutBaseFeeOrBlobGasIsNotADereference(t *testing.T) {
header := &types.Header{Number: big.NewInt(1), Time: 1}
got := blockContext(&ethparams.ChainConfig{ChainID: big.NewInt(1)}, header)
got, ok := blockContext(&ethparams.ChainConfig{ChainID: big.NewInt(1)}, header)
if !ok {
t.Fatal("an absent base fee was refused; absent is zero, and zero fits")
}
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),
// A base fee or chain id wider than uint64 cannot be carried. Zero would be a
// different fee — one every underpriced tx clears — so the context is refused
// and the block declined, rather than truncated.
func TestABaseFeeOrChainIDTooLargeForTheWireIsRefused(t *testing.T) {
huge := new(big.Int).Lsh(big.NewInt(1), 64)
header := &types.Header{Number: big.NewInt(1), BaseFee: huge}
if got, ok := blockContext(&ethparams.ChainConfig{ChainID: big.NewInt(1)}, header); ok {
t.Fatalf("base fee 2^64 gave a context (BaseFee=%d); it must be refused", got.BaseFee)
}
if got := blockContext(&ethparams.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)
header.BaseFee = big.NewInt(1)
if got, ok := blockContext(&ethparams.ChainConfig{ChainID: huge}, header); ok {
t.Fatalf("chain id 2^64 gave a context (ChainID=%d); it must be refused", got.ChainID)
}
}
@@ -349,11 +477,10 @@ func TestAResultThatDoesNotMatchTheBlockIsRefused(t *testing.T) {
}
}
// 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.
// 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) {
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},
@@ -424,10 +551,9 @@ func TestOnlyAPlainValueTransferIsReconstructable(t *testing.T) {
}
}
// 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")
// 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}),
@@ -437,19 +563,15 @@ func TestABlockBeyondValueTransferIsDeclinedAtItsIndex(t *testing.T) {
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)
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)
}
}
// 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}),
@@ -537,12 +659,12 @@ func TestAnUnrecognisedStatusIsNotSuccess(t *testing.T) {
// 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.
// cevm takes an account the snapshot lacks not to exist, so every address the
// block touches — callers, targets and the coinbase — 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}
from, to, coinbase := common.Address{0x01}, common.Address{0x02}, common.Address{0xC0}
code := []byte{0x60, 0x01, 0x60, 0x02}
sdb.SetNonce(from, 9, tracing.NonceChangeUnspecified)
sdb.AddBalance(from, uint256.NewInt(1_000_000), tracing.BalanceChangeUnspecified)
@@ -554,9 +676,9 @@ func TestTheSnapshotHoldsEveryTouchedAccountOnce(t *testing.T) {
{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))
got := buildStateSnapshot(txs, coinbase, sdb)
if len(got) != 3 {
t.Fatalf("snapshot holds %d accounts, want 3 (the caller, the target and the coinbase, deduped)", len(got))
}
byAddr := map[common.Address]cevm.StateAccount{}
@@ -585,6 +707,62 @@ func TestTheSnapshotHoldsEveryTouchedAccountOnce(t *testing.T) {
if common.Hash(target.CodeHash) != sdb.GetCodeHash(to) {
t.Errorf("target code hash = %x, want %x", target.CodeHash, sdb.GetCodeHash(to))
}
if _, ok := byAddr[coinbase]; !ok {
t.Error("the coinbase, which every fee credits, is not in the snapshot")
}
// The coinbase is already a caller: it is not added twice.
if got := buildStateSnapshot(txs, from, sdb); len(got) != 2 {
t.Errorf("snapshot with the caller as coinbase holds %d accounts, want 2", len(got))
}
}
// An account that does not exist has no code and no storage, and its row says
// so with the hashes of nothing. The StateDB answers zero for both, which cevm
// reads as "says nothing" and declines: a sender or recipient that has never
// been seen — the usual first transfer to a fresh address — would decline
// every block it is in.
func TestAnAccountThatDoesNotExistSaysItHasNoCodeAndNoStorage(t *testing.T) {
sdb := newState(t)
absent := common.Address{0xAB}
if sdb.Exist(absent) {
t.Fatal("test setup: the account exists")
}
got := buildStateSnapshot([]cevm.Transaction{{From: [20]byte(absent)}}, absent, sdb)
if len(got) != 1 {
t.Fatalf("snapshot holds %d accounts, want 1", len(got))
}
if common.Hash(got[0].CodeHash) != types.EmptyCodeHash {
t.Errorf("code hash = %x, want keccak256 of nothing %x", got[0].CodeHash, types.EmptyCodeHash)
}
if common.Hash(got[0].StorageRoot) != types.EmptyRootHash {
t.Errorf("storage root = %x, want the empty trie root %x", got[0].StorageRoot, types.EmptyRootHash)
}
}
// An account with storage carries its storage trie root, not zero and not the
// empty root.
func TestAnAccountCarriesItsStorageRoot(t *testing.T) {
sdb := newState(t)
addr := common.Address{0x0D}
sdb.AddBalance(addr, uint256.NewInt(1), tracing.BalanceChangeUnspecified)
sdb.SetState(addr, common.Hash{0x01}, common.Hash{0x02})
sdb.IntermediateRoot(false)
root := sdb.GetStorageRoot(addr)
if root == (common.Hash{}) || root == types.EmptyRootHash {
t.Fatalf("test setup: storage root %x is not a written trie's", root)
}
got := buildStateSnapshot([]cevm.Transaction{{From: [20]byte(addr)}}, addr, sdb)
if len(got) != 1 {
t.Fatalf("snapshot holds %d accounts, want 1", len(got))
}
if common.Hash(got[0].StorageRoot) != root {
t.Errorf("storage root = %x, want %x", got[0].StorageRoot, root)
}
if common.Hash(got[0].CodeHash) != types.EmptyCodeHash {
t.Errorf("code hash = %x, want %x for an account with no code", got[0].CodeHash, types.EmptyCodeHash)
}
}
// Balance crosses as four little-endian uint64 limbs because that is the
@@ -605,7 +783,7 @@ func TestTheBalanceLimbsAreLittleEndian(t *testing.T) {
})
sdb.AddBalance(addr, bal, tracing.BalanceChangeUnspecified)
got := buildStateSnapshot([]cevm.Transaction{{From: [20]byte(addr)}}, sdb)
got := buildStateSnapshot([]cevm.Transaction{{From: [20]byte(addr)}}, addr, sdb)
if len(got) != 1 {
t.Fatalf("snapshot holds %d accounts, want 1", len(got))
}
@@ -627,10 +805,10 @@ func TestTheBalanceLimbsAreLittleEndian(t *testing.T) {
// 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 {
if got := buildStateSnapshot(nil, common.Address{}, newState(t)); got != nil {
t.Errorf("snapshot of no transactions = %v, want nil", got)
}
if got := buildStateSnapshot([]cevm.Transaction{{}}, nil); got != nil {
if got := buildStateSnapshot([]cevm.Transaction{{}}, common.Address{}, nil); got != nil {
t.Errorf("snapshot with no state = %v, want nil", got)
}
}
@@ -656,6 +834,15 @@ func chainConfig() *ethparams.ChainConfig {
return &ethparams.ChainConfig{ChainID: big.NewInt(96369)}
}
// senders is n distinct sender addresses, positionally paired with a block.
func senders(n int) []common.Address {
out := make([]common.Address, n)
for i := range out {
out[i] = common.Address{0xA0, byte(i + 1)}
}
return out
}
// transfer builds a plain value transfer to a fixed recipient.
func transfer(t *testing.T, nonce uint64, value *big.Int) *types.Transaction {
t.Helper()
+156 -99
View File
@@ -50,13 +50,21 @@
//
// Parity contract: every receipt produced here must byte-equal the receipt
// produced by Go EVM Block-STM for the same input tuple.
//
// # Declining
//
// cevm's Go entry (go_bridge.h, ABI 6) answers ok=0 for any block it does not
// run: a batch with code, and any refusal on balance, nonce, price, block gas
// limit, revision or hashes. Such a block, and one whose values do not fit its
// 64-bit wire, is the caller's to run. Every one of them leaves through
// declineBlock as (nil, nil), which is luxfi/evm's "not handled" and sends the
// block to the sequential Go EVM. A declined result is never read.
package parallel
import (
"errors"
"fmt"
"os"
"strings"
"math/big"
"github.com/luxfi/crypto/backend"
evmparallel "github.com/luxfi/evm/core/parallel"
@@ -70,57 +78,24 @@ import (
"github.com/luxfi/chains/evm/cevm"
)
// ErrGPUEVMRequired is the sentinel ExecuteBlock returns when the cevm
// V4 path cannot complete a block on-device and the caller must NOT
// silently shadow-execute it on the Go EVM. Strict mode (the default)
// propagates this error; legacy mode collapses it to (nil, nil) so the
// caller falls through.
// declineBlock is the single exit for a block this executor does not run. It
// records why, for observability, and returns (nil, nil): luxfi/evm's "not
// handled", on which the state processor runs the block on its sequential Go
// EVM.
//
// Strict mode is the production target. The legacy fallback exists only
// for the V4→V5 cevm transition window; flip CEVM_STRICT=0 to re-enable
// it for emergency rollback. Once the V5 kernel implements CALL/CREATE
// on device, the strict path becomes unconditional and the env var is
// retired.
var ErrGPUEVMRequired = errors.New("cevm: GPU EVM cannot execute this block (V4 ABI); waiting for V5 kernel — Go EVM fallback disabled by CEVM_STRICT")
const envCEVMStrict = "CEVM_STRICT"
// strictGPUEVM reports whether the silent Go EVM fallback is disabled.
//
// 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 {
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.
// A decline is never an error: cevm's Go entry passes no host, so it runs no
// 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)
if strictGPUEVM() {
return nil, fmt.Errorf("%w: reason=%s block=%d tx_index=%d",
ErrGPUEVMRequired, reason, blockNumber, txIndex)
}
log.Debug("cevm: declining block to Go EVM (CEVM_STRICT=0)",
log.Debug("cevm: declining block to the Go EVM",
"reason", reason, "block", blockNumber, "tx_index", txIndex)
return nil, nil
}
// execute is the one call into cevm: cevm.ExecuteBlock's signature.
type execute func(cevm.Backend, uint32, []cevm.Transaction, *cevm.BlockContext, []cevm.StateAccount) (*cevm.BlockResult, error)
// Executor is a luxfi/evm/core/parallel.BlockExecutor that dispatches
// every block to cevm.ExecuteBlock in one cgo call.
type Executor struct {
@@ -134,6 +109,11 @@ type Executor struct {
// Threads is the worker count for parallel backends. Ignored by
// CPUSequential; defaults to 1 when zero.
Threads uint32
// execute is the call into cevm; nil is cevm.ExecuteBlock. It is a field
// so that what reaches cevm, and what is done with its answer, can be
// shown without the library.
execute execute
}
var _ evmparallel.BlockExecutor = (*Executor)(nil)
@@ -142,10 +122,9 @@ var _ evmparallel.BlockExecutor = (*Executor)(nil)
// whole block in one cgo call to cevm.ExecuteBlock and reconstructs
// receipts.
//
// Returns (nil, nil) — the documented "fall through to sequential"
// 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.
// Returns (nil, nil) — the documented "fall through to sequential" signal —
// for every block cevm does not run (see declineBlock). On hard errors it
// returns the error.
func (e *Executor) ExecuteBlock(
config *ethparams.ChainConfig,
header *types.Header,
@@ -168,35 +147,58 @@ func (e *Executor) ExecuteBlock(
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.
// of the block, so the block goes to the Go EVM like any other
// decline rather than failing: in a build without the library that
// is every block.
return declineBlock("native_evm_not_linked", header.Number.Uint64(), 0)
}
return nil, fmt.Errorf("cevm: batch sender recovery: %w", err)
}
return e.run(config, header, txs, senders, statedb)
}
// run takes the block from its recovered senders to receipts, or declines it.
//
// Nothing that does not fit cevm's 64-bit wire reaches cevm: a value, price,
// base fee or chain id wider than that is the caller's to run, and truncating
// it would execute a block other than the one that was signed.
func (e *Executor) run(
config *ethparams.ChainConfig,
header *types.Header,
txs types.Transactions,
senders []common.Address,
statedb *state.StateDB,
) ([]*types.Receipt, error) {
cevmTxs, i := shape(txs, senders, statedb)
if i < len(txs) {
return declineBlock("value_overflow_uint64", header.Number.Uint64(), uint64(i))
return declineBlock("tx_not_representable", header.Number.Uint64(), uint64(i))
}
blockCtx := blockContext(config, header)
snapshot := buildStateSnapshot(cevmTxs, statedb)
blockCtx, ok := blockContext(config, header)
if !ok {
return declineBlock("block_context_overflow_uint64", header.Number.Uint64(), 0)
}
snapshot := buildStateSnapshot(cevmTxs, header.Coinbase, statedb)
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)
}
call := e.execute
if call == nil {
call = cevm.ExecuteBlock
}
result, err := call(e.CevmBackend, threads, cevmTxs, &blockCtx, snapshot)
switch {
case err == nil:
return assemble(txs, statedb, 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)
case errors.Is(err, cevm.ErrNotLinked):
return declineBlock("native_evm_not_linked", header.Number.Uint64(), 0)
default:
return nil, fmt.Errorf("cevm: ExecuteBlock: %w", err)
}
return assemble(txs, statedb, result, header)
}
// Backend returns the cevm backend lane this Executor dispatches to.
@@ -207,12 +209,19 @@ func (e *Executor) Backend() cevm.Backend { return e.CevmBackend }
//
// 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.
// uint64; a value or price above 2^64-1 is rare but legal, and truncating it
// would execute a transaction other than the one that was signed. The price is
// tx.GasPrice(), the fee cap of a dynamic-fee tx: what the EVM's buy-gas
// balance check charges, and what cevm checks against the base fee. A tx the
// wire drops part of (see carried) is not representable either.
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() {
if !carried(tx) || !tx.Value().IsUint64() {
return out, i
}
price, ok := fits(tx.GasPrice())
if !ok {
return out, i
}
ct := cevm.Transaction{
@@ -220,35 +229,70 @@ func shape(txs types.Transactions, senders []common.Address, statedb *state.Stat
Nonce: tx.Nonce(),
Data: tx.Data(),
Value: tx.Value().Uint64(),
GasPrice: price,
}
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.
// The recipient's code rides with the tx, as go_bridge.h asks:
// cevm declines a batch that carries any, and one whose code is
// not its recipient's.
ct.Code = statedb.GetCode(*to)
}
if tx.GasPrice() != nil && tx.GasPrice().IsUint64() {
ct.GasPrice = tx.GasPrice().Uint64()
}
out[i] = ct
}
return out, len(txs)
}
// carried reports whether cevm's wire carries everything tx's gas and status
// depend on. CGpuTx has one price and no access list, blob hashes,
// authorizations or tip: a tx with an access list is charged for it, a blob
// or set-code tx for what it carries, and one whose tip exceeds its fee cap is
// invalid, which only the Go EVM would see.
func carried(tx *types.Transaction) bool {
switch tx.Type() {
case types.LegacyTxType, types.AccessListTxType, types.DynamicFeeTxType:
default:
return false
}
return len(tx.AccessList()) == 0 && tx.GasTipCap().Cmp(tx.GasFeeCap()) <= 0
}
// fits reads v as cevm's 64-bit wire carries it. A nil v is zero; one wider
// than 64 bits does not fit.
func fits(v *big.Int) (uint64, bool) {
if v == nil {
return 0, true
}
if !v.IsUint64() {
return 0, false
}
return v.Uint64(), true
}
// 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 {
//
// 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
// 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)
if !ok {
return cevm.BlockContext{}, false
}
baseFee, ok := fits(header.BaseFee)
if !ok {
return cevm.BlockContext{}, false
}
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()
ChainID: chainID,
BaseFee: baseFee,
}
if header.ExcessBlobGas != nil {
ctx.BlobBaseFee = *header.ExcessBlobGas
@@ -257,7 +301,7 @@ func blockContext(config *ethparams.ChainConfig, header *types.Header) cevm.Bloc
// 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
return ctx, true
}
// assemble turns what cevm returned into receipts, or declines the block.
@@ -276,21 +320,20 @@ func assemble(
len(result.GasUsed), len(result.Status), len(txs))
}
// 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.
// 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_v4", header.Number.Uint64(), uint64(i))
return declineBlock("call_or_create_unsupported", header.Number.Uint64(), uint64(i))
}
}
// 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.
// 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_logs_abi_pending_v5", header.Number.Uint64(), uint64(i))
return declineBlock("non_value_transfer_no_logs_in_abi", header.Number.Uint64(), uint64(i))
}
return receipts(txs, result, header), nil
}
@@ -301,7 +344,7 @@ func assemble(
//
// Three things put a transaction beyond that:
//
// - calldata, which can reach code that emits LOGs — and the V4 ABI carries
// - calldata, which can reach code that emits LOGs — and the 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;
@@ -356,23 +399,28 @@ func receipts(txs types.Transactions, result *cevm.BlockResult, header *types.He
return out
}
// 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
// nonce / balance / code without a host trampoline.
// buildStateSnapshot is the state before the block for every account the
// batch touches: each tx's caller and target, and the coinbase its fee goes
// to. go_bridge.h takes an account the snapshot lacks not to exist, so a
// touched account left out would be run as an empty one.
//
// Dedupe by address: every account appears at most once in the snapshot.
// EOAs (no contract code) are emitted with empty Code — the kernel reads
// nonce / balance only.
// EOAs (no contract code) are emitted with empty Code.
//
// Each row names its code and its storage: CodeHash is keccak256 of its code,
// and StorageRoot its storage trie root. An account that does not exist has
// neither, and the StateDB answers zero for both, which cevm reads as "says
// nothing" and declines; so it is given the hashes of nothing,
// types.EmptyCodeHash and types.EmptyRootHash, which is what it is.
//
// Balance encoding: 4×uint64 little-endian limbs (Balance[0] = low 64 bits)
// to match the kernel's HostStateAccount layout exactly.
func buildStateSnapshot(txs []cevm.Transaction, statedb *state.StateDB) []cevm.StateAccount {
func buildStateSnapshot(txs []cevm.Transaction, coinbase common.Address, statedb *state.StateDB) []cevm.StateAccount {
if len(txs) == 0 || statedb == nil {
return nil
}
seen := make(map[common.Address]struct{}, len(txs)*2)
out := make([]cevm.StateAccount, 0, len(txs)*2)
seen := make(map[common.Address]struct{}, len(txs)*2+1)
out := make([]cevm.StateAccount, 0, len(txs)*2+1)
add := func(addr common.Address) {
if _, ok := seen[addr]; ok {
return
@@ -394,8 +442,16 @@ func buildStateSnapshot(txs []cevm.Transaction, statedb *state.StateDB) []cevm.S
}
}
acct.Code = statedb.GetCode(addr)
hash := statedb.GetCodeHash(addr)
copy(acct.CodeHash[:], hash.Bytes())
codeHash := statedb.GetCodeHash(addr)
if codeHash == (common.Hash{}) {
codeHash = types.EmptyCodeHash
}
copy(acct.CodeHash[:], codeHash.Bytes())
root := statedb.GetStorageRoot(addr)
if root == (common.Hash{}) {
root = types.EmptyRootHash
}
copy(acct.StorageRoot[:], root.Bytes())
out = append(out, acct)
}
for i := range txs {
@@ -408,5 +464,6 @@ func buildStateSnapshot(txs []cevm.Transaction, statedb *state.StateDB) []cevm.S
add(target)
}
}
add(coinbase)
return out
}
+57 -144
View File
@@ -6,167 +6,80 @@
package parallel
import (
"errors"
"math/big"
"testing"
"github.com/holiman/uint256"
"github.com/luxfi/chains/evm/cevm"
"github.com/luxfi/geth/common"
"github.com/luxfi/geth/core/tracing"
"github.com/luxfi/geth/core/types"
)
// TestExecuteBlockV4_StateSnapshot_GPUCall exercises the state-aware GPU
// dispatch path: a tx whose top-level opcode is CALL into a deployed
// contract address. Pre-V4 every backend would return TxCallNotSupported
// because the kernel had no way to resolve the target's code on-device.
// With V4 the dispatcher hands the kernel a state snapshot containing the
// target's code, so the call executes through the GPU CALL trampoline.
//
// We don't assert TxOK strictly — the LP-108 P5 corpus is the cevm side's
// gate for which CALL shapes the GPU CALL trampoline supports. What we
// verify here is that:
// 1. ExecuteBlockV4 returns without error.
// 2. The result has the new ABIVersion (6) — proves the v4 ABI wired
// end-to-end.
// 3. Per-tx status / gas slices have the right length.
// 4. Backends that DO support state-aware CALL return a non-OOG status
// (i.e. the kernel made progress past the kernel-internal "not
// supported" sentinel).
func TestExecuteBlockV4_StateSnapshot_GPUCall(t *testing.T) {
backends := cevm.AvailableBackends()
if len(backends) == 0 {
t.Skip("no backends available")
}
// Caller program: CALL with constant 20-byte target, 0 value, 0 gas hint.
// PUSH1 0 (retSize)
// PUSH1 0 (retOff)
// PUSH1 0 (argSize)
// PUSH1 0 (argOff)
// PUSH1 0 (value)
// PUSH20 <target> (to)
// PUSH2 0xFFFF (gas)
// CALL
// POP
// STOP
//
// The target's bytecode is a trivial STOP — fine because the test only
// verifies that the kernel resolved the target on-device instead of
// bailing out with CallNotSupported.
target := [20]byte{
0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33,
0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd,
}
caller := [20]byte{
0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80, 0x90, 0xa0,
0xb0, 0xc0, 0xd0, 0xe0, 0xf0, 0x01, 0x02, 0x03, 0x04, 0x05,
}
callerCode := []byte{
0x60, 0x00, // PUSH1 0 retSize
0x60, 0x00, // PUSH1 0 retOff
0x60, 0x00, // PUSH1 0 argSize
0x60, 0x00, // PUSH1 0 argOff
0x60, 0x00, // PUSH1 0 value
0x73, // PUSH20
}
callerCode = append(callerCode, target[:]...)
callerCode = append(callerCode,
0x61, 0xff, 0xff, // PUSH2 0xFFFF gas
0xf1, // CALL
0x50, // POP
0x00, // STOP
)
// Target bytecode: STOP only.
targetCode := []byte{0x00}
// Code-hash: kernel reads it from the snapshot, doesn't recompute.
// Empty for the test — the GPU CALL path checks size != 0 first.
var emptyHash [32]byte
// These run against the linked library and hold it to go_bridge.h (ABI 6).
// A batch that carries code is declined on every backend the library offers:
// gpu_execute_block passes no host, so it would run the code as a message on
// its whole limit, which is not the tx's gas. Declined means ErrDeclined and
// no result.
func TestABatchWithCodeIsDeclinedOnEveryBackend(t *testing.T) {
tx := cevm.Transaction{
From: [20]byte{0x11},
To: [20]byte{0x22},
HasTo: true,
Code: callerCode,
GasLimit: 200_000,
GasPrice: 1,
}
tx.From = caller
tx.To = caller // tx target is self; the inner CALL goes to `target`
state := []cevm.StateAccount{
{Address: caller, Nonce: 1, CodeHash: emptyHash},
{Address: target, Code: targetCode, CodeHash: emptyHash},
}
for _, b := range backends {
b := b
t.Run(cevm.BackendName(b), func(t *testing.T) {
r, err := cevm.ExecuteBlock(b, 0, []cevm.Transaction{tx}, nil, state)
if err != nil {
t.Fatalf("ExecuteBlockV4: %v", err)
}
if r.ABIVersion != cevm.ABIVersion {
t.Errorf("ABIVersion=%d want %d", r.ABIVersion, cevm.ABIVersion)
}
if len(r.GasUsed) != 1 || len(r.Status) != 1 {
t.Fatalf("malformed result: gas_len=%d status_len=%d",
len(r.GasUsed), len(r.Status))
}
t.Logf("backend=%s status=%s gas=%d", b, r.Status[0], r.GasUsed[0])
})
}
}
// TestExecuteBlockV4_EmptySnapshot is the V3-equivalent path: an empty
// snapshot must produce the same result as ExecuteBlockV3. Guards against
// a regression where the new code path corrupts results when the caller
// passes no state.
func TestExecuteBlockV4_EmptySnapshot(t *testing.T) {
backends := cevm.AvailableBackends()
if len(backends) == 0 {
t.Skip("no backends available")
}
// Simple ADD program — no CALL involvement.
code := []byte{
0x60, 0x01, // PUSH1 1
0x60, 0x01, // PUSH1 1
0x01, // ADD
0x50, // POP
0x00, // STOP
}
tx := cevm.Transaction{
HasTo: true,
Code: code,
Code: []byte{0x60, 0x01, 0x60, 0x01, 0x01, 0x50, 0x00}, // 1+1, POP, STOP
GasLimit: 100_000,
GasPrice: 1,
}
for _, b := range backends {
b := b
for _, b := range cevm.AvailableBackends() {
t.Run(cevm.BackendName(b), func(t *testing.T) {
r3, err3 := cevm.ExecuteBlock(b, 0, []cevm.Transaction{tx}, nil, nil)
if err3 != nil {
t.Fatalf("V3: %v", err3)
r, err := cevm.ExecuteBlock(b, 0, []cevm.Transaction{tx}, nil, nil)
if !errors.Is(err, cevm.ErrDeclined) {
t.Fatalf("ExecuteBlock = %v, want ErrDeclined", err)
}
r4, err4 := cevm.ExecuteBlock(b, 0, []cevm.Transaction{tx}, nil, nil)
if err4 != nil {
t.Fatalf("V4: %v", err4)
if r != nil {
t.Errorf("a declined block came back with a result: %+v", r)
}
if len(r3.GasUsed) != len(r4.GasUsed) {
t.Fatalf("gas length mismatch V3=%d V4=%d",
len(r3.GasUsed), len(r4.GasUsed))
})
}
}
// 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.
func TestAPlainTransferBlockIsReceiptedOrDeclined(t *testing.T) {
const n = 8
sdb := newState(t)
header := newHeader()
header.BaseFee = big.NewInt(1)
header.Coinbase = common.Address{0xC0}
txs := make(types.Transactions, n)
from := senders(n)
for i := range txs {
sdb.AddBalance(from[i], uint256.NewInt(1_000_000_000_000_000_000), tracing.BalanceChangeUnspecified)
to := common.Address{0xB0, byte(i + 1)} // fresh: not in the state
txs[i] = types.NewTx(&types.LegacyTx{To: &to, Value: big.NewInt(1), Gas: 21000, GasPrice: big.NewInt(1)})
}
for _, b := range cevm.AvailableBackends() {
t.Run(cevm.BackendName(b), func(t *testing.T) {
receipts, err := (&Executor{CevmBackend: b}).run(chainConfig(), header, txs, from, sdb)
if err != nil {
t.Fatalf("run: %v", err)
}
if len(r3.Status) != len(r4.Status) {
t.Fatalf("status length mismatch V3=%d V4=%d",
len(r3.Status), len(r4.Status))
if receipts == nil {
t.Logf("%s declined the block to the Go EVM", cevm.BackendName(b))
return
}
for i := range r3.GasUsed {
if r3.GasUsed[i] != r4.GasUsed[i] {
t.Errorf("gas[%d] V3=%d V4=%d (empty snapshot must match V3)",
i, r3.GasUsed[i], r4.GasUsed[i])
}
if len(receipts) != n {
t.Fatalf("%d receipts for %d transactions", len(receipts), n)
}
for i := range r3.Status {
if r3.Status[i] != r4.Status[i] {
t.Errorf("status[%d] V3=%s V4=%s (empty snapshot must match V3)",
i, r3.Status[i], r4.Status[i])
for i, r := range receipts {
if r.Status != types.ReceiptStatusSuccessful || r.GasUsed != 21000 {
t.Errorf("receipt %d: status %d gas %d, want success at 21000", i, r.Status, r.GasUsed)
}
}
})