mirror of
https://github.com/luxfi/chains.git
synced 2026-09-24 00:26:00 +00:00
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:
@@ -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
@@ -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
@@ -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)",
|
||||
|
||||
@@ -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
@@ -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: ðparams.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 := ðparams.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(ðparams.ChainConfig{ChainID: big.NewInt(1)}, header)
|
||||
got, ok := blockContext(ðparams.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(ðparams.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(ðparams.ChainConfig{ChainID: big.NewInt(1)}, header); got.BaseFee != 0 {
|
||||
t.Fatalf("BaseFee = %d; a value that does not fit must not be truncated", got.BaseFee)
|
||||
header.BaseFee = big.NewInt(1)
|
||||
if got, ok := blockContext(ðparams.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 ðparams.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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user