mirror of
https://github.com/luxfi/cevm.git
synced 2026-10-30 17:00:54 +00:00
C++ bridge: gpu_execute_block_v4 -> gpu_execute_block, CGpuBlockResultV2 -> CGpuBlockResult, gpu_free_result_v2 -> gpu_free_result (unversioned, rebuilt). Go: one ExecuteBlock(backend, txs, opts...) with WithThreads/WithContext functional options + one full BlockResult; deleted ExecuteBlockV2/V3 and BlockResultV2; updated the !cgo mirror + 4 test files. ABIVersion -> 6. Also fixed the cgo include path (../../luxcpp -> ../../../luxcpp; origin couldn't find go_bridge.h) and ordered the lux-crypto archives after -lluxgpu. Package builds CGO=1 and CGO=0. KNOWN PRE-EXISTING GAP (not introduced; origin never linked at all): the TEST binary pulls cevm's full crypto surface where a few bare-C symbols (ed25519_verify, PQCLEAN_randombytes, lux_donna_sr25519_*, evmc_create_cevm) have no provider archive yet — the blake3-class extern-C shim gap, to be closed in luxcpp/cevm + lux-crypto packaging. Co-authored-by: Hanzo Dev <[email protected]>
513 lines
23 KiB
Go
513 lines
23 KiB
Go
//go:build cgo
|
|
|
|
package cevm
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"sync"
|
|
"testing"
|
|
)
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Bytecode builder
|
|
// ----------------------------------------------------------------------------
|
|
|
|
// prog is a tiny, append-only EVM bytecode builder. Every method returns the
|
|
// receiver so call chains read like an assembly listing.
|
|
type prog struct{ b []byte }
|
|
|
|
// op appends a single opcode byte.
|
|
func (p *prog) op(o byte) *prog {
|
|
p.b = append(p.b, o)
|
|
return p
|
|
}
|
|
|
|
// raw appends arbitrary bytes (used after PUSH for the immediate operand).
|
|
func (p *prog) raw(bs ...byte) *prog {
|
|
p.b = append(p.b, bs...)
|
|
return p
|
|
}
|
|
|
|
// push appends a PUSH<n> with the supplied operand bytes.
|
|
// n must be in [1, 32]; len(val) must equal n.
|
|
func (p *prog) push(n int, val ...byte) *prog {
|
|
if n < 1 || n > 32 {
|
|
panic(fmt.Sprintf("prog.push: bad n=%d", n))
|
|
}
|
|
if len(val) != n {
|
|
panic(fmt.Sprintf("prog.push: n=%d but len(val)=%d", n, len(val)))
|
|
}
|
|
p.b = append(p.b, 0x60+byte(n-1)) // PUSH1=0x60 ... PUSH32=0x7F
|
|
p.b = append(p.b, val...)
|
|
return p
|
|
}
|
|
|
|
// push1 is the common case.
|
|
func (p *prog) push1(v byte) *prog { return p.push(1, v) }
|
|
|
|
// bytes returns the assembled bytecode.
|
|
func (p *prog) bytes() []byte { return p.b }
|
|
|
|
// stop / revert / ret terminate the program.
|
|
func (p *prog) stop() *prog { return p.op(0x00) }
|
|
func (p *prog) revert() *prog { return p.push1(0).push1(0).op(0xFD) }
|
|
func (p *prog) ret() *prog { return p.push1(0).push1(0).op(0xF3) }
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Test table — one entry per defined EVM opcode.
|
|
// ----------------------------------------------------------------------------
|
|
|
|
// opcodeTest describes a single opcode probe.
|
|
type opcodeTest struct {
|
|
name string
|
|
opcode byte
|
|
bytecode []byte
|
|
// expectStatus is the status the CPU reference path is expected to return.
|
|
// We assert CPU status equals this; GPU is compared to CPU when not skipGPU.
|
|
expectStatus TxStatus
|
|
// skipGPU=true for opcodes the GPU kernel cannot run by itself (CALL family).
|
|
// We still run them through CPU to confirm CPU baseline; GPU is skipped with
|
|
// a logged note instead of failing.
|
|
skipGPU bool
|
|
// needsCalldata populates tx.Data for calldata opcodes.
|
|
needsCalldata bool
|
|
// needsTo populates tx.HasTo for context opcodes that read the recipient.
|
|
needsTo bool
|
|
}
|
|
|
|
// allOpcodeTests is the canonical opcode coverage table. One entry per opcode.
|
|
// Order is by opcode value so coverage gaps are visible at a glance.
|
|
var allOpcodeTests = []opcodeTest{
|
|
// 0x00 — STOP
|
|
{name: "STOP", opcode: 0x00, bytecode: (&prog{}).stop().bytes(), expectStatus: TxOK},
|
|
|
|
// 0x01..0x0B — arithmetic
|
|
{name: "ADD", opcode: 0x01, bytecode: (&prog{}).push1(5).push1(3).op(0x01).stop().bytes(), expectStatus: TxOK},
|
|
{name: "MUL", opcode: 0x02, bytecode: (&prog{}).push1(5).push1(3).op(0x02).stop().bytes(), expectStatus: TxOK},
|
|
{name: "SUB", opcode: 0x03, bytecode: (&prog{}).push1(3).push1(5).op(0x03).stop().bytes(), expectStatus: TxOK},
|
|
{name: "DIV", opcode: 0x04, bytecode: (&prog{}).push1(2).push1(10).op(0x04).stop().bytes(), expectStatus: TxOK},
|
|
{name: "SDIV", opcode: 0x05, bytecode: (&prog{}).push1(2).push1(10).op(0x05).stop().bytes(), expectStatus: TxOK},
|
|
{name: "MOD", opcode: 0x06, bytecode: (&prog{}).push1(3).push1(10).op(0x06).stop().bytes(), expectStatus: TxOK},
|
|
{name: "SMOD", opcode: 0x07, bytecode: (&prog{}).push1(3).push1(10).op(0x07).stop().bytes(), expectStatus: TxOK},
|
|
{name: "ADDMOD", opcode: 0x08, bytecode: (&prog{}).push1(7).push1(5).push1(3).op(0x08).stop().bytes(), expectStatus: TxOK},
|
|
{name: "MULMOD", opcode: 0x09, bytecode: (&prog{}).push1(7).push1(5).push1(3).op(0x09).stop().bytes(), expectStatus: TxOK},
|
|
{name: "EXP", opcode: 0x0A, bytecode: (&prog{}).push1(3).push1(2).op(0x0A).stop().bytes(), expectStatus: TxOK},
|
|
{name: "SIGNEXTEND", opcode: 0x0B, bytecode: (&prog{}).push1(0xFF).push1(0).op(0x0B).stop().bytes(), expectStatus: TxOK},
|
|
|
|
// 0x10..0x1D — comparison + bitwise
|
|
{name: "LT", opcode: 0x10, bytecode: (&prog{}).push1(5).push1(3).op(0x10).stop().bytes(), expectStatus: TxOK},
|
|
{name: "GT", opcode: 0x11, bytecode: (&prog{}).push1(3).push1(5).op(0x11).stop().bytes(), expectStatus: TxOK},
|
|
{name: "SLT", opcode: 0x12, bytecode: (&prog{}).push1(5).push1(3).op(0x12).stop().bytes(), expectStatus: TxOK},
|
|
{name: "SGT", opcode: 0x13, bytecode: (&prog{}).push1(3).push1(5).op(0x13).stop().bytes(), expectStatus: TxOK},
|
|
{name: "EQ", opcode: 0x14, bytecode: (&prog{}).push1(5).push1(5).op(0x14).stop().bytes(), expectStatus: TxOK},
|
|
{name: "ISZERO", opcode: 0x15, bytecode: (&prog{}).push1(0).op(0x15).stop().bytes(), expectStatus: TxOK},
|
|
{name: "AND", opcode: 0x16, bytecode: (&prog{}).push1(0xF0).push1(0x0F).op(0x16).stop().bytes(), expectStatus: TxOK},
|
|
{name: "OR", opcode: 0x17, bytecode: (&prog{}).push1(0xF0).push1(0x0F).op(0x17).stop().bytes(), expectStatus: TxOK},
|
|
{name: "XOR", opcode: 0x18, bytecode: (&prog{}).push1(0xFF).push1(0x0F).op(0x18).stop().bytes(), expectStatus: TxOK},
|
|
{name: "NOT", opcode: 0x19, bytecode: (&prog{}).push1(0).op(0x19).stop().bytes(), expectStatus: TxOK},
|
|
{name: "BYTE", opcode: 0x1A, bytecode: (&prog{}).push1(0xAB).push1(31).op(0x1A).stop().bytes(), expectStatus: TxOK},
|
|
{name: "SHL", opcode: 0x1B, bytecode: (&prog{}).push1(1).push1(2).op(0x1B).stop().bytes(), expectStatus: TxOK},
|
|
{name: "SHR", opcode: 0x1C, bytecode: (&prog{}).push1(8).push1(2).op(0x1C).stop().bytes(), expectStatus: TxOK},
|
|
{name: "SAR", opcode: 0x1D, bytecode: (&prog{}).push1(8).push1(2).op(0x1D).stop().bytes(), expectStatus: TxOK},
|
|
|
|
// 0x20 — KECCAK256: hash 32 bytes of zeroed memory at offset 0.
|
|
{name: "KECCAK256", opcode: 0x20, bytecode: (&prog{}).push1(32).push1(0).op(0x20).stop().bytes(), expectStatus: TxOK},
|
|
|
|
// 0x30..0x4A — context / block
|
|
{name: "ADDRESS", opcode: 0x30, bytecode: (&prog{}).op(0x30).stop().bytes(), expectStatus: TxOK, needsTo: true},
|
|
{name: "BALANCE", opcode: 0x31, bytecode: (&prog{}).op(0x30).op(0x31).stop().bytes(), expectStatus: TxOK, needsTo: true},
|
|
{name: "ORIGIN", opcode: 0x32, bytecode: (&prog{}).op(0x32).stop().bytes(), expectStatus: TxOK},
|
|
{name: "CALLER", opcode: 0x33, bytecode: (&prog{}).op(0x33).stop().bytes(), expectStatus: TxOK},
|
|
{name: "CALLVALUE", opcode: 0x34, bytecode: (&prog{}).op(0x34).stop().bytes(), expectStatus: TxOK},
|
|
{name: "CALLDATALOAD", opcode: 0x35, bytecode: (&prog{}).push1(0).op(0x35).stop().bytes(), expectStatus: TxOK, needsCalldata: true},
|
|
{name: "CALLDATASIZE", opcode: 0x36, bytecode: (&prog{}).op(0x36).stop().bytes(), expectStatus: TxOK, needsCalldata: true},
|
|
{name: "CALLDATACOPY", opcode: 0x37, bytecode: (&prog{}).push1(32).push1(0).push1(0).op(0x37).stop().bytes(), expectStatus: TxOK, needsCalldata: true},
|
|
{name: "CODESIZE", opcode: 0x38, bytecode: (&prog{}).op(0x38).stop().bytes(), expectStatus: TxOK},
|
|
{name: "CODECOPY", opcode: 0x39, bytecode: (&prog{}).push1(4).push1(0).push1(0).op(0x39).stop().bytes(), expectStatus: TxOK},
|
|
{name: "GASPRICE", opcode: 0x3A, bytecode: (&prog{}).op(0x3A).stop().bytes(), expectStatus: TxOK},
|
|
{name: "EXTCODESIZE", opcode: 0x3B, bytecode: (&prog{}).op(0x30).op(0x3B).stop().bytes(), expectStatus: TxOK, needsTo: true},
|
|
{name: "EXTCODECOPY", opcode: 0x3C, bytecode: (&prog{}).push1(0).push1(0).push1(0).op(0x30).op(0x3C).stop().bytes(), expectStatus: TxOK, needsTo: true},
|
|
{name: "RETURNDATASIZE", opcode: 0x3D, bytecode: (&prog{}).op(0x3D).stop().bytes(), expectStatus: TxOK},
|
|
{name: "RETURNDATACOPY", opcode: 0x3E, bytecode: (&prog{}).push1(0).push1(0).push1(0).op(0x3E).stop().bytes(), expectStatus: TxOK},
|
|
{name: "EXTCODEHASH", opcode: 0x3F, bytecode: (&prog{}).op(0x30).op(0x3F).stop().bytes(), expectStatus: TxOK, needsTo: true},
|
|
{name: "BLOCKHASH", opcode: 0x40, bytecode: (&prog{}).push1(0).op(0x40).stop().bytes(), expectStatus: TxOK},
|
|
{name: "COINBASE", opcode: 0x41, bytecode: (&prog{}).op(0x41).stop().bytes(), expectStatus: TxOK},
|
|
{name: "TIMESTAMP", opcode: 0x42, bytecode: (&prog{}).op(0x42).stop().bytes(), expectStatus: TxOK},
|
|
{name: "NUMBER", opcode: 0x43, bytecode: (&prog{}).op(0x43).stop().bytes(), expectStatus: TxOK},
|
|
{name: "PREVRANDAO", opcode: 0x44, bytecode: (&prog{}).op(0x44).stop().bytes(), expectStatus: TxOK},
|
|
{name: "GASLIMIT", opcode: 0x45, bytecode: (&prog{}).op(0x45).stop().bytes(), expectStatus: TxOK},
|
|
{name: "CHAINID", opcode: 0x46, bytecode: (&prog{}).op(0x46).stop().bytes(), expectStatus: TxOK},
|
|
{name: "SELFBALANCE", opcode: 0x47, bytecode: (&prog{}).op(0x47).stop().bytes(), expectStatus: TxOK, needsTo: true},
|
|
{name: "BASEFEE", opcode: 0x48, bytecode: (&prog{}).op(0x48).stop().bytes(), expectStatus: TxOK},
|
|
{name: "BLOBHASH", opcode: 0x49, bytecode: (&prog{}).push1(0).op(0x49).stop().bytes(), expectStatus: TxOK},
|
|
{name: "BLOBBASEFEE", opcode: 0x4A, bytecode: (&prog{}).op(0x4A).stop().bytes(), expectStatus: TxOK},
|
|
|
|
// 0x50..0x5F — stack/memory/storage/flow
|
|
{name: "POP", opcode: 0x50, bytecode: (&prog{}).push1(1).op(0x50).stop().bytes(), expectStatus: TxOK},
|
|
{name: "MLOAD", opcode: 0x51, bytecode: (&prog{}).push1(0).op(0x51).stop().bytes(), expectStatus: TxOK},
|
|
{name: "MSTORE", opcode: 0x52, bytecode: (&prog{}).push1(0xAB).push1(0).op(0x52).stop().bytes(), expectStatus: TxOK},
|
|
{name: "MSTORE8", opcode: 0x53, bytecode: (&prog{}).push1(0xAB).push1(0).op(0x53).stop().bytes(), expectStatus: TxOK},
|
|
{name: "SLOAD", opcode: 0x54, bytecode: (&prog{}).push1(0).op(0x54).stop().bytes(), expectStatus: TxOK},
|
|
{name: "SSTORE", opcode: 0x55, bytecode: (&prog{}).push1(1).push1(0).op(0x55).stop().bytes(), expectStatus: TxOK},
|
|
// JUMP: PUSH1 4, JUMP, INVALID, JUMPDEST, STOP — jumps over the INVALID byte.
|
|
{name: "JUMP", opcode: 0x56, bytecode: (&prog{}).push1(4).op(0x56).op(0xFE).op(0x5B).stop().bytes(), expectStatus: TxOK},
|
|
// JUMPI: cond=1, dest=6, PUSH1 1, PUSH1 6, JUMPI, INVALID, JUMPDEST, STOP.
|
|
{name: "JUMPI", opcode: 0x57, bytecode: (&prog{}).push1(1).push1(6).op(0x57).op(0xFE).op(0x5B).stop().bytes(), expectStatus: TxOK},
|
|
{name: "PC", opcode: 0x58, bytecode: (&prog{}).op(0x58).stop().bytes(), expectStatus: TxOK},
|
|
{name: "MSIZE", opcode: 0x59, bytecode: (&prog{}).op(0x59).stop().bytes(), expectStatus: TxOK},
|
|
{name: "GAS", opcode: 0x5A, bytecode: (&prog{}).op(0x5A).stop().bytes(), expectStatus: TxOK},
|
|
{name: "JUMPDEST", opcode: 0x5B, bytecode: (&prog{}).op(0x5B).stop().bytes(), expectStatus: TxOK},
|
|
{name: "TLOAD", opcode: 0x5C, bytecode: (&prog{}).push1(0).op(0x5C).stop().bytes(), expectStatus: TxOK},
|
|
{name: "TSTORE", opcode: 0x5D, bytecode: (&prog{}).push1(1).push1(0).op(0x5D).stop().bytes(), expectStatus: TxOK},
|
|
{name: "MCOPY", opcode: 0x5E, bytecode: (&prog{}).push1(32).push1(0).push1(0).op(0x5E).stop().bytes(), expectStatus: TxOK},
|
|
{name: "PUSH0", opcode: 0x5F, bytecode: (&prog{}).op(0x5F).stop().bytes(), expectStatus: TxOK},
|
|
|
|
// 0x60..0x7F — PUSH1..PUSH32 (table built below)
|
|
// 0x80..0x8F — DUP1..DUP16 (table built below)
|
|
// 0x90..0x9F — SWAP1..SWAP16 (table built below)
|
|
|
|
// 0xA0..0xA4 — LOG0..LOG4
|
|
{name: "LOG0", opcode: 0xA0, bytecode: (&prog{}).push1(0).push1(0).op(0xA0).stop().bytes(), expectStatus: TxOK},
|
|
{name: "LOG1", opcode: 0xA1, bytecode: (&prog{}).push1(0xAA).push1(0).push1(0).op(0xA1).stop().bytes(), expectStatus: TxOK},
|
|
{name: "LOG2", opcode: 0xA2, bytecode: (&prog{}).push1(0xAA).push1(0xBB).push1(0).push1(0).op(0xA2).stop().bytes(), expectStatus: TxOK},
|
|
{name: "LOG3", opcode: 0xA3, bytecode: (&prog{}).push1(0xAA).push1(0xBB).push1(0xCC).push1(0).push1(0).op(0xA3).stop().bytes(), expectStatus: TxOK},
|
|
{name: "LOG4", opcode: 0xA4, bytecode: (&prog{}).push1(0xAA).push1(0xBB).push1(0xCC).push1(0xDD).push1(0).push1(0).op(0xA4).stop().bytes(), expectStatus: TxOK},
|
|
|
|
// 0xF0..0xFF — system
|
|
// CALL family is expected to return CallNotSupported on GPU. We assert
|
|
// on CPU via expectStatus (CPU may execute or also flag unsupported), and
|
|
// skipGPU=true so we do not require gas equivalence.
|
|
{name: "CREATE", opcode: 0xF0, bytecode: (&prog{}).push1(0).push1(0).push1(0).op(0xF0).stop().bytes(), expectStatus: TxOK, skipGPU: true},
|
|
{name: "CALL", opcode: 0xF1, bytecode: (&prog{}).push1(0).push1(0).push1(0).push1(0).push1(0).op(0x30).push1(0).op(0xF1).stop().bytes(), expectStatus: TxOK, skipGPU: true, needsTo: true},
|
|
{name: "CALLCODE", opcode: 0xF2, bytecode: (&prog{}).push1(0).push1(0).push1(0).push1(0).push1(0).op(0x30).push1(0).op(0xF2).stop().bytes(), expectStatus: TxOK, skipGPU: true, needsTo: true},
|
|
{name: "RETURN", opcode: 0xF3, bytecode: (&prog{}).push1(0).push1(0).op(0xF3).bytes(), expectStatus: TxReturn},
|
|
{name: "DELEGATECALL", opcode: 0xF4, bytecode: (&prog{}).push1(0).push1(0).push1(0).push1(0).op(0x30).push1(0).op(0xF4).stop().bytes(), expectStatus: TxOK, skipGPU: true, needsTo: true},
|
|
{name: "CREATE2", opcode: 0xF5, bytecode: (&prog{}).push1(0).push1(0).push1(0).push1(0).op(0xF5).stop().bytes(), expectStatus: TxOK, skipGPU: true},
|
|
{name: "STATICCALL", opcode: 0xFA, bytecode: (&prog{}).push1(0).push1(0).push1(0).push1(0).op(0x30).push1(0).op(0xFA).stop().bytes(), expectStatus: TxOK, skipGPU: true, needsTo: true},
|
|
{name: "REVERT", opcode: 0xFD, bytecode: (&prog{}).push1(0).push1(0).op(0xFD).bytes(), expectStatus: TxRevert},
|
|
// INVALID intentionally fails — both backends should flag it.
|
|
{name: "INVALID", opcode: 0xFE, bytecode: []byte{0xFE}, expectStatus: TxError},
|
|
{name: "SELFDESTRUCT", opcode: 0xFF, bytecode: (&prog{}).op(0x30).op(0xFF).bytes(), expectStatus: TxOK, skipGPU: true, needsTo: true},
|
|
}
|
|
|
|
// init expands the tables for PUSH1..PUSH32, DUP1..DUP16, SWAP1..SWAP16.
|
|
// Each is generated programmatically — there's nothing interesting to
|
|
// hand-write.
|
|
func init() {
|
|
// PUSH1..PUSH32: PUSH<n> with n bytes of 0xAA, then STOP.
|
|
for n := 1; n <= 32; n++ {
|
|
op := byte(0x5F + n)
|
|
operand := make([]byte, n)
|
|
for i := range operand {
|
|
operand[i] = 0xAA
|
|
}
|
|
bc := append([]byte{op}, operand...)
|
|
bc = append(bc, 0x00) // STOP
|
|
allOpcodeTests = append(allOpcodeTests, opcodeTest{
|
|
name: fmt.Sprintf("PUSH%d", n),
|
|
opcode: op,
|
|
bytecode: bc,
|
|
expectStatus: TxOK,
|
|
})
|
|
}
|
|
// DUP1..DUP16: push N+1 zeros then DUP<N>, STOP.
|
|
for n := 1; n <= 16; n++ {
|
|
op := byte(0x7F + n)
|
|
p := &prog{}
|
|
for i := 0; i <= n; i++ {
|
|
p.push1(byte(i + 1))
|
|
}
|
|
p.op(op).stop()
|
|
allOpcodeTests = append(allOpcodeTests, opcodeTest{
|
|
name: fmt.Sprintf("DUP%d", n),
|
|
opcode: op,
|
|
bytecode: p.bytes(),
|
|
expectStatus: TxOK,
|
|
})
|
|
}
|
|
// SWAP1..SWAP16: push N+1 distinct vals then SWAP<N>, STOP.
|
|
for n := 1; n <= 16; n++ {
|
|
op := byte(0x8F + n)
|
|
p := &prog{}
|
|
for i := 0; i <= n; i++ {
|
|
p.push1(byte(i + 1))
|
|
}
|
|
p.op(op).stop()
|
|
allOpcodeTests = append(allOpcodeTests, opcodeTest{
|
|
name: fmt.Sprintf("SWAP%d", n),
|
|
opcode: op,
|
|
bytecode: p.bytes(),
|
|
expectStatus: TxOK,
|
|
})
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Coverage report (shared across subtests, summarised by TestOpcodeCoverage_Summary)
|
|
// ----------------------------------------------------------------------------
|
|
|
|
type opcodeOutcome struct {
|
|
name string
|
|
opcode byte
|
|
cpuStatus TxStatus
|
|
gpuStatus TxStatus
|
|
cpuGas uint64
|
|
gpuGas uint64
|
|
gasMatch bool
|
|
gpuRan bool
|
|
gpuMatchedCPU bool
|
|
skippedCallFam bool
|
|
notImplemented bool
|
|
executeErr string
|
|
}
|
|
|
|
var (
|
|
coverageMu sync.Mutex
|
|
coverageResults = map[string]opcodeOutcome{}
|
|
)
|
|
|
|
func recordOutcome(o opcodeOutcome) {
|
|
coverageMu.Lock()
|
|
defer coverageMu.Unlock()
|
|
coverageResults[o.name] = o
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Test helpers
|
|
// ----------------------------------------------------------------------------
|
|
|
|
// runTx builds a Transaction for the given opcode test and runs it through
|
|
// `backend` with a single worker thread so we get a deterministic per-tx
|
|
// status from ExecuteBlock.
|
|
func runOpcodeTx(backend Backend, ot opcodeTest) (*BlockResult, error) {
|
|
var from [20]byte
|
|
from[19] = 0x42
|
|
tx := Transaction{
|
|
From: from,
|
|
GasLimit: 1_000_000,
|
|
GasPrice: 1,
|
|
Nonce: 0,
|
|
Code: ot.bytecode,
|
|
}
|
|
if ot.needsTo {
|
|
var to [20]byte
|
|
to[19] = 0xCA
|
|
tx.To = to
|
|
tx.HasTo = true
|
|
}
|
|
if ot.needsCalldata {
|
|
tx.Data = []byte{
|
|
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
|
|
0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10,
|
|
0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
|
|
0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20,
|
|
}
|
|
}
|
|
return ExecuteBlock(backend, []Transaction{tx}, WithThreads(1))
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// The big test: run every opcode through CPU + GPU and compare.
|
|
// ----------------------------------------------------------------------------
|
|
|
|
func TestOpcodeCoverage_GPU_vs_CPU(t *testing.T) {
|
|
available := AvailableBackends()
|
|
if !contains(available, CPUSequential) {
|
|
t.Skip("CPUSequential backend not available")
|
|
}
|
|
gpuAvailable := contains(available, GPUMetal)
|
|
if !gpuAvailable {
|
|
t.Logf("GPU Metal backend NOT available — running CPU-only coverage")
|
|
}
|
|
|
|
for _, ot := range allOpcodeTests {
|
|
ot := ot // capture
|
|
t.Run(ot.name, func(t *testing.T) {
|
|
outcome := opcodeOutcome{name: ot.name, opcode: ot.opcode}
|
|
defer func() { recordOutcome(outcome) }()
|
|
|
|
// 1) CPU reference run.
|
|
cpuRes, err := runOpcodeTx(CPUSequential, ot)
|
|
if err != nil {
|
|
outcome.executeErr = "cpu: " + err.Error()
|
|
t.Fatalf("CPU execute failed for %s (0x%02X): %v", ot.name, ot.opcode, err)
|
|
}
|
|
if len(cpuRes.Status) != 1 || len(cpuRes.GasUsed) != 1 {
|
|
t.Fatalf("CPU returned malformed result for %s: status=%v gas=%v", ot.name, cpuRes.Status, cpuRes.GasUsed)
|
|
}
|
|
outcome.cpuStatus = cpuRes.Status[0]
|
|
outcome.cpuGas = cpuRes.GasUsed[0]
|
|
|
|
// 2) GPU comparison (if available and not in skipGPU set).
|
|
if !gpuAvailable {
|
|
return
|
|
}
|
|
if ot.skipGPU {
|
|
outcome.skippedCallFam = true
|
|
t.Logf("skip GPU for %s: CALL family / contract creation (CPU status=%s gas=%d)",
|
|
ot.name, outcome.cpuStatus, outcome.cpuGas)
|
|
return
|
|
}
|
|
|
|
gpuRes, err := runOpcodeTx(GPUMetal, ot)
|
|
if err != nil {
|
|
outcome.executeErr = "gpu: " + err.Error()
|
|
// Coverage measurement: log but do not fail the whole suite.
|
|
t.Logf("OPCODE NOT IMPLEMENTED ON GPU (execute error): %s (0x%02X): %v",
|
|
ot.name, ot.opcode, err)
|
|
return
|
|
}
|
|
outcome.gpuRan = true
|
|
if len(gpuRes.Status) != 1 || len(gpuRes.GasUsed) != 1 {
|
|
t.Fatalf("GPU returned malformed result for %s: status=%v gas=%v", ot.name, gpuRes.Status, gpuRes.GasUsed)
|
|
}
|
|
outcome.gpuStatus = gpuRes.Status[0]
|
|
outcome.gpuGas = gpuRes.GasUsed[0]
|
|
|
|
// If GPU reports CallNotSupported but the opcode isn't in our
|
|
// skip list, treat that as "not implemented yet" data.
|
|
if outcome.gpuStatus == TxCallNotSupported {
|
|
outcome.notImplemented = true
|
|
t.Logf("OPCODE NOT IMPLEMENTED ON GPU (CallNotSupported): %s (0x%02X)", ot.name, ot.opcode)
|
|
return
|
|
}
|
|
if outcome.gpuStatus == TxError && outcome.cpuStatus != TxError {
|
|
outcome.notImplemented = true
|
|
t.Logf("OPCODE NOT IMPLEMENTED ON GPU (TxError, cpu=%s): %s (0x%02X)",
|
|
outcome.cpuStatus, ot.name, ot.opcode)
|
|
return
|
|
}
|
|
|
|
// 3) Compare CPU vs GPU.
|
|
if outcome.cpuStatus != outcome.gpuStatus {
|
|
t.Errorf("%s (0x%02X): status mismatch cpu=%s gpu=%s",
|
|
ot.name, ot.opcode, outcome.cpuStatus, outcome.gpuStatus)
|
|
}
|
|
if outcome.cpuGas == outcome.gpuGas {
|
|
outcome.gasMatch = true
|
|
outcome.gpuMatchedCPU = outcome.cpuStatus == outcome.gpuStatus
|
|
} else {
|
|
t.Logf("%s (0x%02X): gas differs cpu=%d gpu=%d (status cpu=%s gpu=%s)",
|
|
ot.name, ot.opcode, outcome.cpuGas, outcome.gpuGas,
|
|
outcome.cpuStatus, outcome.gpuStatus)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Coverage summary report
|
|
// ----------------------------------------------------------------------------
|
|
|
|
// TestOpcodeCoverage_Summary depends on TestOpcodeCoverage_GPU_vs_CPU running
|
|
// first (Go test order is deterministic by source position, this file's main
|
|
// test is declared above). It prints a human-readable coverage report.
|
|
func TestOpcodeCoverage_Summary(t *testing.T) {
|
|
coverageMu.Lock()
|
|
defer coverageMu.Unlock()
|
|
|
|
if len(coverageResults) == 0 {
|
|
t.Skip("no opcode results recorded — run TestOpcodeCoverage_GPU_vs_CPU first")
|
|
}
|
|
|
|
type bucket struct {
|
|
ran, gasMatch, skippedCall, notImpl, errored int
|
|
failedNames []string
|
|
}
|
|
var b bucket
|
|
for _, o := range coverageResults {
|
|
switch {
|
|
case o.skippedCallFam:
|
|
b.skippedCall++
|
|
case o.notImplemented:
|
|
b.notImpl++
|
|
b.failedNames = append(b.failedNames, fmt.Sprintf("%s(0x%02X)", o.name, o.opcode))
|
|
case o.executeErr != "":
|
|
b.errored++
|
|
b.failedNames = append(b.failedNames, fmt.Sprintf("%s(0x%02X)", o.name, o.opcode))
|
|
case o.gpuRan && o.gasMatch:
|
|
b.ran++
|
|
b.gasMatch++
|
|
case o.gpuRan:
|
|
b.ran++
|
|
}
|
|
}
|
|
sort.Strings(b.failedNames)
|
|
|
|
total := len(coverageResults)
|
|
t.Logf("=== EVM Opcode GPU Coverage Report ===")
|
|
t.Logf("Total opcodes tested: %d", total)
|
|
t.Logf("GPU executed: %d", b.ran)
|
|
t.Logf("Gas-match w/ CPU: %d", b.gasMatch)
|
|
t.Logf("Skipped (CALL/CREATE): %d", b.skippedCall)
|
|
t.Logf("Not implemented on GPU: %d", b.notImpl)
|
|
t.Logf("GPU execute errors: %d", b.errored)
|
|
if len(b.failedNames) > 0 {
|
|
t.Logf("Failing opcodes:")
|
|
for _, n := range b.failedNames {
|
|
t.Logf(" - %s", n)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestOpcodeCoverage_TableComplete asserts the static table covers every
|
|
// opcode in the canonical set (sanity check on the test author).
|
|
func TestOpcodeCoverage_TableComplete(t *testing.T) {
|
|
got := map[byte]string{}
|
|
for _, ot := range allOpcodeTests {
|
|
if other, ok := got[ot.opcode]; ok {
|
|
t.Errorf("duplicate opcode 0x%02X: %q and %q", ot.opcode, other, ot.name)
|
|
continue
|
|
}
|
|
got[ot.opcode] = ot.name
|
|
}
|
|
|
|
// Canonical list of opcodes the EVM spec defines (Cancun + EIP-4844 + EIP-1153).
|
|
want := []byte{
|
|
// 0x00..0x0B
|
|
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B,
|
|
// 0x10..0x1D
|
|
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D,
|
|
// 0x20
|
|
0x20,
|
|
// 0x30..0x3F
|
|
0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F,
|
|
// 0x40..0x4A
|
|
0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A,
|
|
// 0x50..0x5F
|
|
0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x5B, 0x5C, 0x5D, 0x5E, 0x5F,
|
|
// 0xA0..0xA4
|
|
0xA0, 0xA1, 0xA2, 0xA3, 0xA4,
|
|
// 0xF0..0xF5, 0xFA, 0xFD..0xFF
|
|
0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xFA, 0xFD, 0xFE, 0xFF,
|
|
}
|
|
// PUSH1..PUSH32 (0x60..0x7F)
|
|
for op := byte(0x60); op <= 0x7F; op++ {
|
|
want = append(want, op)
|
|
}
|
|
// DUP1..DUP16 (0x80..0x8F)
|
|
for op := byte(0x80); op <= 0x8F; op++ {
|
|
want = append(want, op)
|
|
}
|
|
// SWAP1..SWAP16 (0x90..0x9F)
|
|
for op := byte(0x90); op <= 0x9F; op++ {
|
|
want = append(want, op)
|
|
}
|
|
|
|
missing := []byte{}
|
|
for _, op := range want {
|
|
if _, ok := got[op]; !ok {
|
|
missing = append(missing, op)
|
|
}
|
|
}
|
|
if len(missing) > 0 {
|
|
var s []string
|
|
for _, op := range missing {
|
|
s = append(s, fmt.Sprintf("0x%02X", op))
|
|
}
|
|
t.Errorf("table is missing %d opcodes: %v", len(missing), s)
|
|
}
|
|
t.Logf("table covers %d / %d opcodes", len(got), len(want))
|
|
}
|