Removes the silent (nil, nil) "fall through to Go EVM" return at the three
points where cevm V4 cannot complete a block on device:
- tx value exceeds uint64
- kernel returns TxCallNotSupported (CALL/CREATE family)
- block has logs (V4 ABI has no per-tx logs)
Replaces each with declineBlock(reason, block, tx_index):
* Default (LUX_CEVM_STRICT=1): returns ErrGPUEVMRequired so the caller
cannot silently shadow-execute on the Go EVM. The lie that hid the
missing GPU CALL/CREATE is removed.
* Legacy (LUX_CEVM_STRICT=0): returns (nil, nil) for V4→V5 transition
rollback only.
Every fallback records backend.RecordFallback(FallbackUnsupported, "cevm:<reason>")
so production observability surfaces the rate at which blocks decline.
V5 kernel spec lands at evm/cevm/V5_ABI.md: gpu_execute_block_v5 with
code-table, storage_root threading, per-tx logs. Strict mode becomes
unconditional once V5 ships and the env var retires.
5.6 KiB
cevm GPU ABI v5 — CALL / CREATE on device
Status: spec. luxcpp-side work; Go-side scaffold lands in this directory.
Why v5
V4 (gpu_execute_block_v4) interprets ADD / SSTORE / SLOAD / Keccak / MSTORE
/ MLOAD / MCOPY on the GPU but traps on CALL / CREATE / CREATE2 /
DELEGATECALL / STATICCALL — any block touching a contract interaction
falls back to the Go EVM. That breaks block-batched dispatch on every
contract-heavy block, which is the common case for the C-Chain workload.
V5 adds (a) a code-table the device can resolve callees from, (b) a storage-trie root that survives nested SSTORE/SLOAD across frames, and (c) a per-tx log API so receipts can be reconstructed without re-running the block on the host.
ABI
C entry point (additive — V4 stays for back-compat):
// luxcpp/cevm/lib/evm/gpu/go_bridge.h (new in v5)
gpu_block_result_v5* gpu_execute_block_v5(
const gpu_tx_v5* txs,
uint32_t num_txs,
gpu_backend_t backend,
uint32_t num_threads,
int revision,
const BlockContext* block_ctx,
const StateAccount* state, // V4-shape, now with storage_root
uint32_t state_len,
const uint8_t* code_blob,
uint64_t code_size,
const gpu_code_entry* code_table, // NEW: addr -> code_blob offset
uint32_t code_table_len);
void gpu_free_result_v5(gpu_block_result_v5*);
New types:
typedef struct {
uint8_t address[20]; // contract address
uint64_t code_offset; // offset into code_blob where this address's bytecode lives
uint64_t code_len;
} gpu_code_entry;
typedef struct {
uint8_t address[20];
uint64_t nonce;
uint8_t balance[32]; // u256 LE
uint8_t storage_root[32]; // NEW: kept across CALL frames in the kernel
uint64_t code_offset; // points into code_blob (0 = EOA, no code)
uint64_t code_len;
} StateAccount; // unchanged shape from V4 except storage_root is now load-bearing
typedef struct {
uint8_t hash[32];
uint8_t sender[20];
uint8_t to[20]; // zero = contract creation
uint8_t value[32]; // u256 LE
uint64_t gas;
uint64_t gas_price;
uint64_t nonce;
const uint8_t* data;
uint64_t data_len;
} gpu_tx_v5; // unchanged from V4
typedef struct {
// per-tx outputs
uint64_t* gas_used;
uint8_t* status; // 0 = revert, 1 = success
uint8_t state_root[32];
// NEW in v5: per-tx logs flat-encoded
gpu_log_entry* logs;
uint32_t num_logs;
uint32_t* tx_log_count; // logs[] partition by tx
} gpu_block_result_v5;
typedef struct {
uint8_t address[20];
uint8_t topics[4][32];
uint8_t num_topics;
const uint8_t* data;
uint32_t data_len;
} gpu_log_entry;
Kernel-side work (luxcpp/cevm)
- Call stack on device: kernel maintains a per-tx stack of frames
(depth ≤ 1024 per EVM spec); each frame has its own scratch memory,
PC, and gas counter.
CALLpushes;RETURN/REVERTpops. - Code resolution:
CALL/DELEGATECALL/STATICCALL/CREATE2load callee bytecode fromcode_blobviacode_tablelookup (binary search on the 20-byte address). EOAs (no entry) are pure-value transfers. - Storage threading:
SSTORE/SLOADoperate on the active frame's account'sstorage_root. Frame pop merges the dirty set back into the parent's state-account row. - Log emission:
LOG0..LOG4opcodes append to a per-tx log list. On block completion the kernel concatenates per-tx lists into the flatgpu_log_entry[] logsbuffer and writes per-tx counts.
Go-side opt-in
chains/evm/cevm/cevm_cgo.go adds:
// ExecuteBlockV5 dispatches to gpu_execute_block_v5 when the linked
// libevm-gpu exports the symbol (runtime dlsym probe). Falls through
// to ExecuteBlockV4 (or Go EVM) otherwise.
func ExecuteBlockV5(...) (*BlockResultV5, error)
parallel/parallel.go::BlockExecutor picks V5 when:
- The block contains CALL / CREATE family opcodes (cheap pre-scan)
ExecuteBlockV5is bound at the dlsym layerbackend.IsGPU()
If any of those are false, the existing V4 path or the Go EVM fall-back runs unchanged.
Equivalence + tests
cevm_v5_parity_test.go (to write) runs each block through both the Go
EVM and ExecuteBlockV5, asserting byte-equal state-root + log set
across a synthetic suite covering: pure value transfer, CREATE / CREATE2,
nested CALL (depth 4), DELEGATECALL preserving storage, STATICCALL
trapping SSTORE, LOG0..LOG4 emission.
Existing block-batched parity tests (cevm_cgo_test.go) stay green
because V5 is a strict superset of V4.
Open questions
- Self-destructed accounts: V5 must thread the suicide-list out of
the kernel so the host trie can prune. Two options — return as part
of
gpu_block_result_v5or include in the per-tx log stream as a pseudo-log. Pick one and pin in a follow-up note. - Precompile dispatch: V4 punts all precompile addresses to the host.
V5 keeps that for now; native precompiles (Lux V4 DEX precompile at
0x9012, FHE precompiles 0x0200…0083) stay on the host until each one
gets a dedicated GPU kernel. Document the punt set in
cevm_cgo.go::isHostPrecompile. - Gas refund accounting (EIP-3529): the kernel returns
gas_usednet of refund; the host should not re-apply the refund.
Out of scope
- KZG / EIP-4844 blob handling — V4 already routes that to the host; no GPU win.
- Beacon root opcode (
PUSH 0x4A) — Lux disables Cancun beacon-root semantics; the kernel can short-circuit it to a no-op.