Files
cevm/cevm_cgo_test.go
zeekayandHanzo Dev 6e12d0889b The binding sends a tx's fee cap and tip, as ABI 7's CGpuTx carries them
go_bridge.h's CGpuTx has max_fee_per_gas and max_priority_fee_per_gas in
place of gas_price, 120 bytes on LP64. Transaction has GasFeeCap and
GasTipCap for them, both the gas price of a legacy or access-list tx, and
buildTxs sets both; the size check reads 120. cevm charges min(cap, base
fee + tip), as the EVM does, and declines a tip above the cap, which the
device test sends.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-23 13:45:07 -07:00

402 lines
12 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//go:build cgo
package cevm
import (
"errors"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"testing"
)
// Tests in this file require the C++ EVM library (CGO_ENABLED=1) and hold it
// to go_bridge.h, ABI 7: a batch of plain transfers runs on a GPU lane that
// has its device, to TxOK at 21000 gas each. Everything else is declined — a
// batch with code or calldata on any lane, a batch its senders cannot pay for,
// and every batch on a CPU lane — as ErrDeclined and no result, and the caller
// runs it on its own EVM.
//
// ExecuteBlock passes no state snapshot, so every sender is taken not to
// exist and to hold nothing: the transfers a device runs here cost nothing.
//
// A GPU lane is listed when the library was built with it, whether or not
// this host has the device, and without one it declines every block. A test
// that needs a device skips a lane whose device this host does not have
// (hasDevice), and on a lane whose device it has, a decline fails: a missing
// shader or a device-path regression declines too, and must not pass for an
// absent device.
func TestLibraryABIVersion(t *testing.T) {
if got := LibraryABIVersion(); got != ABIVersion {
t.Errorf("LibraryABIVersion() = %d, want %d (rebuild libevm-gpu)", got, ABIVersion)
}
}
// TestAvailableBackends_HasCPU: under cgo, the loaded library must always
// expose CPUSequential.
func TestAvailableBackends_HasCPU(t *testing.T) {
bs := AvailableBackends()
if !contains(bs, CPUSequential) {
t.Errorf("AvailableBackends() missing CPUSequential: %v", bs)
}
}
// A block of plain transfers that cost nothing runs on a GPU lane to TxOK at
// 21000 gas each; a CPU lane declines it. No lane answers anything else.
func TestATransferBlockRunsOnADeviceAndNowhereElse(t *testing.T) {
const n = 4
txs, ctx := transfers(n)
for _, b := range AvailableBackends() {
t.Run(BackendName(b), func(t *testing.T) {
if isGPU(b) && !hasDevice(b) {
t.Skipf("this host has no %s device", BackendName(b))
}
r, err := ExecuteBlock(b, txs, WithContext(&ctx))
if !isGPU(b) {
declined(t, r, err)
return
}
ranTransfers(t, r, err, n)
})
}
}
// With no state snapshot every sender holds nothing, so a transfer that costs
// anything cannot be paid for, and every lane declines it.
func TestATransferNobodyCanPayForIsDeclined(t *testing.T) {
txs, ctx := transfers(1)
txs[0].Value = 1
for _, b := range AvailableBackends() {
t.Run(BackendName(b), func(t *testing.T) {
r, err := ExecuteBlock(b, txs, WithContext(&ctx))
declined(t, r, err)
})
}
}
// A batch that carries code is declined on every backend: gpu_execute_block
// passes no host, so it runs no code (go_bridge.h). Declined is ErrDeclined
// and no result — never gas or statuses a caller could take for the block's.
func TestABatchWithCodeIsDeclined(t *testing.T) {
txs, ctx := transfers(1)
txs[0].Code = computeBytecode(1)
for _, b := range AvailableBackends() {
t.Run(BackendName(b), func(t *testing.T) {
r, err := ExecuteBlock(b, txs, WithContext(&ctx))
declined(t, r, err)
})
}
}
// TestHealth runs the Health() battery: a transfer that costs nothing. A GPU
// lane whose device this host has runs it to TxOK at 21000 gas; a CPU lane
// declines it, because the Go entry runs nothing on the CPU without a host
// (go_bridge.h).
func TestHealth(t *testing.T) {
reports := Health()
if len(reports) == 0 {
t.Fatal("Health() returned no reports — runtime cannot enumerate backends")
}
for _, r := range reports {
switch {
case isGPU(r.Backend) && !hasDevice(r.Backend):
t.Logf("Health: GPU lane %q: this host has no device for it (ok=%v err=%v)", r.Name, r.OK, r.Err)
case isGPU(r.Backend):
if !r.OK || r.GasUsed != 21000 {
t.Errorf("Health: GPU lane %q: ok=%v gas=%d err=%v, want ok at 21000", r.Name, r.OK, r.GasUsed, r.Err)
}
case r.OK || !errors.Is(r.Err, ErrDeclined):
t.Errorf("Health: CPU lane %q: ok=%v err=%v, want declined", r.Name, r.OK, r.Err)
}
}
}
// Goroutines running blocks on one device at once each get their own block's
// answer.
func TestConcurrentExecuteBlock(t *testing.T) {
for _, b := range deviceLanes(t) {
t.Run(BackendName(b), func(t *testing.T) {
const goroutines, iterations, n = 8, 16, 8
txs, ctx := transfers(n)
var wg sync.WaitGroup
for g := 0; g < goroutines; g++ {
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < iterations; i++ {
r, err := ExecuteBlock(b, txs, WithContext(&ctx))
if err != nil || !allTransfers(r, n) {
t.Errorf("concurrent run on %s: err=%v result=%+v", BackendName(b), err, r)
return
}
}
}()
}
wg.Wait()
})
}
}
// TestConcurrent_Stress: 100 goroutines × 100 txs, each tx with its own code
// and calldata allocation. The library copies both before it declines the
// batch, so this is the regression test for runtime.Pinner: an unpinned Go
// pointer read from C panics or corrupts here. Every call must decline, and
// none may panic. Run with -race for full effect.
func TestConcurrent_Stress(t *testing.T) {
if testing.Short() {
t.Skip("stress test skipped in -short mode")
}
backends := AvailableBackends()
const goroutines, txsPerGoroutine = 100, 100
makeBlock := func(seed uint64) []Transaction {
txs := make([]Transaction, txsPerGoroutine)
for i := range txs {
data := make([]byte, 32)
for j := range data {
data[j] = byte(seed + uint64(i) + uint64(j))
}
tx := bytecodeTx(seed*txsPerGoroutine+uint64(i), computeBytecode(30))
tx.Data = data
txs[i] = tx
}
return txs
}
var wg sync.WaitGroup
wg.Add(goroutines)
for g := 0; g < goroutines; g++ {
go func(seed uint64) {
defer wg.Done()
defer func() {
if r := recover(); r != nil {
t.Errorf("goroutine %d panicked: %v", seed, r)
}
}()
b := backends[int(seed)%len(backends)]
r, err := ExecuteBlock(b, makeBlock(seed))
if !errors.Is(err, ErrDeclined) || r != nil {
t.Errorf("goroutine %d on %s: (%+v, %v), want declined", seed, BackendName(b), r, err)
}
}(uint64(g))
}
wg.Wait()
}
// TestExecuteBlock_LargeCode: 48 KB of code crosses the boundary (the
// library copies it) and the batch is declined cleanly, never a segfault from
// an unchecked uint32 or an unpinned slice.
func TestExecuteBlock_LargeCode(t *testing.T) {
code := computeBytecode(8000)
if len(code) < 32_000 {
t.Fatalf("expected >= 32K bytecode, got %d", len(code))
}
tx := bytecodeTx(0, code)
tx.GasLimit = 50_000_000
r, err := ExecuteBlock(CPUSequential, []Transaction{tx})
declined(t, r, err)
}
// TestExecuteBlock_LargeData: 64 KiB of calldata on a transfer that costs
// nothing crosses the boundary through the data pin, and every lane declines
// it: the value-transfer device paths do not price calldata.
func TestExecuteBlock_LargeData(t *testing.T) {
txs, ctx := transfers(1)
txs[0].Data = make([]byte, 1<<16)
for i := range txs[0].Data {
txs[0].Data[i] = byte(i)
}
txs[0].GasLimit = 2_000_000
for _, b := range AvailableBackends() {
t.Run(BackendName(b), func(t *testing.T) {
r, err := ExecuteBlock(b, txs, WithContext(&ctx))
declined(t, r, err)
})
}
}
// TestBackendUnavailable: a backend the library does not offer declines the
// block, and never panics.
func TestBackendUnavailable(t *testing.T) {
available := AvailableBackends()
missing := Backend(-1)
for _, b := range []Backend{CPUSequential, CPUParallel, GPUMetal, GPUCUDA} {
if !contains(available, b) {
missing = b
break
}
}
if missing < 0 {
t.Skip("all backends available — cannot test unavailable path")
}
txs, ctx := transfers(1)
r, err := ExecuteBlock(missing, txs, WithContext(&ctx))
declined(t, r, err)
}
// A library that reports another ABI is sent nothing: every block is
// declined, a block a device would have run included, and the error says why.
func TestALibraryOfAnotherABIIsDeclined(t *testing.T) {
loaded := libraryABI
t.Cleanup(func() { libraryABI = loaded })
libraryABI = ABIVersion - 1
if got := LibraryABIVersion(); got != ABIVersion-1 {
t.Fatalf("LibraryABIVersion() = %d, want the library's %d", got, ABIVersion-1)
}
txs, ctx := transfers(2)
for _, b := range AvailableBackends() {
t.Run(BackendName(b), func(t *testing.T) {
r, err := ExecuteBlock(b, txs, WithContext(&ctx))
declined(t, r, err)
if !strings.Contains(err.Error(), "ABI") {
t.Errorf("the decline does not say the ABI is why: %v", err)
}
})
}
}
// -----------------------------------------------------------------------------
// helpers
// -----------------------------------------------------------------------------
func isGPU(b Backend) bool { return b == GPUMetal || b == GPUCUDA }
// transfers is a block of n plain transfers that cost nothing — no value, no
// price, no base fee — each from its own sender to its own fresh recipient,
// with the block context go_bridge.h asks for. Each limit is above the 21000
// a transfer costs, so a lane that answers the limit as an estimate is not
// taken for one that ran it.
func transfers(n int) ([]Transaction, BlockContext) {
txs := make([]Transaction, n)
for i := range txs {
txs[i] = Transaction{
From: [20]byte{0xA0, byte(i >> 8), byte(i)},
To: [20]byte{0xB0, byte(i >> 8), byte(i)},
HasTo: true,
GasLimit: 30_000,
}
}
return txs, BlockContext{GasLimit: 30_000_000, ChainID: 1}
}
// allTransfers reports whether r is n plain transfers run: every one TxOK at
// 21000 gas, at ABI 7.
func allTransfers(r *BlockResult, n int) bool {
if r == nil || len(r.GasUsed) != n || len(r.Status) != n || r.ABIVersion != ABIVersion {
return false
}
for i := 0; i < n; i++ {
if r.Status[i] != TxOK || r.GasUsed[i] != 21000 {
return false
}
}
return r.TotalGas == uint64(n)*21000
}
func ranTransfers(t *testing.T, r *BlockResult, err error, n int) {
t.Helper()
if err != nil {
// The ABI carries no reason: on a host with the device, a decline is
// its shader not found, a driver or architecture mismatch, or a
// regression in its path.
t.Fatalf("ExecuteBlock: %v", err)
}
if !allTransfers(r, n) {
t.Fatalf("result is not %d transfers run at 21000 gas: %+v", n, r)
}
}
// declined holds an answer to the one other thing ExecuteBlock may say:
// ErrDeclined, and no result.
func declined(t *testing.T, r *BlockResult, err error) {
t.Helper()
if !errors.Is(err, ErrDeclined) {
t.Fatalf("ExecuteBlock = %v, want ErrDeclined", err)
}
if r != nil {
t.Fatalf("a declined block came back with a result: %+v", r)
}
}
// deviceLanes is the GPU lanes whose device this host has. It skips the test
// when there is none.
func deviceLanes(t *testing.T) []Backend {
t.Helper()
var out []Backend
for _, b := range AvailableBackends() {
if isGPU(b) && hasDevice(b) {
out = append(out, b)
}
}
if len(out) == 0 {
t.Skip("this host has no device for any GPU lane the library offers")
}
return out
}
// hasDevice reports whether this host has the device GPU lane b runs on: the
// GPU every Apple silicon Mac has for Metal, and for CUDA an NVIDIA driver's
// control device with at least one GPU node this process can see (a container
// can be given the first without the second). The library lists a lane it
// was built with either way.
func hasDevice(b Backend) bool {
switch b {
case GPUMetal:
return runtime.GOOS == "darwin" && runtime.GOARCH == "arm64"
case GPUCUDA:
if _, err := os.Stat("/dev/nvidiactl"); err != nil {
return false
}
gpus, _ := filepath.Glob("/dev/nvidia[0-9]*")
return len(gpus) > 0
}
return false
}
// computeBytecode returns deterministic EVM bytecode that does iters
// additions then returns.
func computeBytecode(iters int) []byte {
out := make([]byte, 0, iters*6+5)
for i := 0; i < iters; i++ {
out = append(out,
0x60, 0x01, // PUSH1 1
0x60, 0x01, // PUSH1 1
0x01, // ADD
0x50, // POP
)
}
return append(out,
0x60, 0x00, // PUSH1 0
0x60, 0x00, // PUSH1 0
0xf3, // RETURN
)
}
func bytecodeTx(i uint64, code []byte) Transaction {
var from [20]byte
from[19] = byte(i)
from[18] = byte(i >> 8)
return Transaction{
From: from,
HasTo: true,
Code: code,
GasLimit: 1_000_000,
Nonce: i,
GasFeeCap: 1,
GasTipCap: 1,
}
}
// contains reports whether b appears in s.
func contains(s []Backend, b Backend) bool {
for _, x := range s {
if x == b {
return true
}
}
return false
}