Files
cevm/cevm_cgo.go
zeekayandHanzo Dev 60d96130a2 A library whose CGpuTx is not this build's speaks no ABI the binding reads
Two version 7 libraries lay a tx out differently: one built before CGpuTx
carried the fee cap and tip has a 112-byte tx and no gpu_abi_tx_size. init
compares the library's gpu_abi_tx_size with its own sizeof(CGpuTx) and, on
a difference, declines every block as it declines a library of another
ABI; a library without the symbol does not link.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-23 14:33:46 -07:00

427 lines
16 KiB
Go

//go:build cgo
package cevm
/*
#cgo CFLAGS: -I${SRCDIR}/../../luxcpp/cevm/lib/evm/gpu
#cgo LDFLAGS: -L${SRCDIR}/../../luxcpp/cevm/build/lib
#cgo LDFLAGS: -L${SRCDIR}/../../luxcpp/cevm/build/lib/evm
#cgo LDFLAGS: -L${SRCDIR}/../../luxcpp/cevm/build/lib/evm/luxcpp-gpu
#cgo LDFLAGS: -L${SRCDIR}/../../luxcpp/cevm/build/lib/cevm_precompiles
#cgo LDFLAGS: -Wl,-rpath,${SRCDIR}/../../luxcpp/cevm/build/lib
#cgo LDFLAGS: -Wl,-rpath,${SRCDIR}/../../luxcpp/cevm/build/lib/evm/luxcpp-gpu
#cgo LDFLAGS: -levm
#cgo darwin LDFLAGS: -levm-gpu -levm-metal-hosts -levm-kernel-metal -levm-gpu -lluxgpu -lcevm_precompiles -lstdc++
#cgo darwin LDFLAGS: -framework Metal -framework Foundation
#cgo linux LDFLAGS: -Wl,--start-group -levm-gpu -lluxgpu -lcevm_precompiles -Wl,--end-group -lstdc++
// lux-crypto archives MUST come AFTER -lluxgpu/-levm-* (which reference them):
// macOS ld is single-pass, so the providers go last. These are the _cpu crypto
// libs libluxgpu + cevm reference (sr25519/ed25519/slhdsa/PQCLEAN/blake3/...).
#cgo darwin LDFLAGS: -L/opt/homebrew/opt/lux-crypto/lib -Wl,-rpath,/opt/homebrew/opt/lux-crypto/lib -lcrypto -laead_cpu -lattestation_cpu -lbanderwagon_cpu -lblake2b_cpu -lblake3_cpu -lbls_cpu -lblst -lbn254_cpu -lcggmp21_cpu -led25519_cpu -levm256_cpu -lfhe_cpu -lfrost_cpu -lgpukit_cpu -lipa_cpu -lkeccak_cpu -lkzg_cpu -llamport_cpu -lmldsa_cpu -lmlkem_cpu -lmodexp_cpu -lntt_cpu -lpedersen_cpu -lpoly_mul_cpu -lposeidon_cpu -lripemd160_cpu -lsecp256k1_cpu -lslhdsa_cpu -lsecp256r1_cpu -lsr25519_cpu -lsha256
#cgo linux LDFLAGS: -L/opt/homebrew/opt/lux-crypto/lib -lcrypto -lsr25519_cpu -led25519_cpu -lslhdsa_cpu -lmldsa_cpu -lmlkem_cpu -lblake3_cpu -lbn254_cpu -lmodexp_cpu -lkeccak_cpu -lsecp256k1_cpu
#include <stdlib.h>
#include "go_bridge.h"
*/
import "C"
import (
"fmt"
"runtime"
"unsafe"
)
// AutoDetect returns the best available backend for this machine.
func AutoDetect() Backend {
return Backend(C.gpu_auto_detect_backend())
}
// The header this builds against names the ABI this module is written to, or
// this file does not compile: each line fails when EVM_GPU_ABI_VERSION is
// smaller (the first) or larger (the second) than ABIVersion.
var (
_ [ABIVersion - C.EVM_GPU_ABI_VERSION]struct{}
_ [C.EVM_GPU_ABI_VERSION - ABIVersion]struct{}
)
// The C structs this file fills and reads, at the sizes go_bridge.h (ABI 7)
// 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. Each
// pair fails when the size is larger (the first) or smaller (the second).
var (
_ [unsafe.Sizeof(C.CGpuTx{}) - 120]struct{}
_ [120 - unsafe.Sizeof(C.CGpuTx{})]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{}
)
// libraryABI is the ABI the loaded library speaks, read once in init: the one
// it reports (gpu_abi_version), or 0 where its CGpuTx is not this file's
// (gpu_abi_tx_size), as it is not in a version 7 library built before the tx
// carried its fee cap and tip. The library a binary loads at run time need not
// be the one whose header it was built against.
var libraryABI uint32
// init reads the loaded library's ABI. A library of another ABI lays the
// structs out and means ok differently, so ExecuteBlock sends it nothing:
// every block is declined, and the caller runs it on its own EVM.
func init() {
libraryABI = uint32(C.gpu_abi_version())
if uint32(C.gpu_abi_tx_size()) != uint32(unsafe.Sizeof(C.CGpuTx{})) {
libraryABI = 0
}
}
// errOtherABI is ExecuteBlock's decline when the loaded library, or a result
// it returned, is of an ABI this file is not written to.
func errOtherABI(got uint32) error {
return fmt.Errorf("cevm: the library speaks ABI %d, this module reads %d: %w", got, ABIVersion, ErrDeclined)
}
// buildTxs converts Go transactions into C-layout transactions, pinning any
// Go-owned byte slices for the duration of the C call. The caller is
// responsible for invoking pinner.Unpin() once C has returned.
//
// Pinning rule: ctxs[i].data and ctxs[i].code are Go pointers inside Go
// memory that C will dereference. Per Go cgo rules these inner pointers
// MUST be pinned. ctxs[i].from / ctxs[i].to are stored by-value (array
// copy) so they don't need pinning.
func buildTxs(txs []Transaction, pinner *runtime.Pinner) []C.CGpuTx {
ctxs := make([]C.CGpuTx, len(txs))
for i := range txs {
t := &txs[i]
ctxs[i].from = *(*[20]C.uint8_t)(unsafe.Pointer(&t.From[0]))
ctxs[i].to = *(*[20]C.uint8_t)(unsafe.Pointer(&t.To[0]))
ctxs[i].gas_limit = C.uint64_t(t.GasLimit)
ctxs[i].value = C.uint64_t(t.Value)
ctxs[i].nonce = C.uint64_t(t.Nonce)
ctxs[i].max_fee_per_gas = C.uint64_t(t.GasFeeCap)
ctxs[i].max_priority_fee_per_gas = C.uint64_t(t.GasTipCap)
if t.HasTo {
ctxs[i].has_to = 1
}
if len(t.Data) > 0 {
pinner.Pin(&t.Data[0])
ctxs[i].data = (*C.uint8_t)(unsafe.Pointer(&t.Data[0]))
ctxs[i].data_len = C.uint32_t(len(t.Data))
}
if len(t.Code) > 0 {
pinner.Pin(&t.Code[0])
ctxs[i].code = (*C.uint8_t)(unsafe.Pointer(&t.Code[0]))
ctxs[i].code_len = C.uint32_t(len(t.Code))
}
}
return ctxs
}
// copyU64 safely copies up to want elements from a C uint64 array into a Go
// slice. Bounds-checks `want` against a sane maximum to defend against a
// corrupted result struct returning an absurd count.
func copyU64(ptr *C.uint64_t, want uint32) []uint64 {
if ptr == nil || want == 0 {
return nil
}
const maxTxsPerBlock = 1 << 24 // 16M txs/block — far above any realistic block
if want > maxTxsPerBlock {
return nil
}
src := unsafe.Slice((*uint64)(unsafe.Pointer(ptr)), int(want))
dst := make([]uint64, want)
copy(dst, src)
return dst
}
// ExecuteBlock runs a block of transactions through gpu_execute_block and
// returns per-tx gas and status, and the block-level Block-STM counters. The
// state root is zero: the entry roots no state.
//
// Defaults (override with options): hardware-concurrency threads
// (WithThreads), Cancun revision, zero block context (WithContext), no state
// snapshot. When the library declines the block (ok=0) ExecuteBlock returns
// ErrDeclined and no result: the caller runs the block on its own EVM. A
// loaded library of another ABI is never sent a block: every block is
// declined, with an error that wraps ErrDeclined. A zero block context has a zero gas
// limit, which holds no transaction, so a block that should run needs
// WithContext.
//
// Thread safety: ExecuteBlock is safe to call from multiple goroutines
// concurrently. The C++ engine uses thread-local kernel hosts, so each
// goroutine that reaches the GPU path gets its own MTLBuffer/CUDA context
// cache. There are no shared mutable globals between calls.
//
// Memory safety: every Go-owned []byte the C side dereferences (tx.Data,
// tx.Code) is pinned for the duration of the C call. The ctxs[] slice
// itself is a stack-allocated local (or heap-promoted by escape analysis,
// either way reachable) — runtime.KeepAlive(ctxs) at the end guarantees
// the GC won't collect it while the C call is still in flight. The pinner
// is unpinned via defer on every return path including errors. The
// BlockContext is passed by value into a stack-allocated C struct, so it
// doesn't need pinning.
func ExecuteBlock(backend Backend, txs []Transaction, opts ...BlockOption) (*BlockResult, error) {
var o blockOpts
for _, opt := range opts {
opt(&o)
}
if len(txs) == 0 {
return &BlockResult{ABIVersion: ABIVersion}, nil
}
if libraryABI != ABIVersion {
return nil, errOtherABI(libraryABI)
}
var pinner runtime.Pinner
defer pinner.Unpin()
ctxs := buildTxs(txs, &pinner)
// CBlockContext is layout-compatible with cevm.BlockContext: same field
// order, same widths, no padding deltas. Build it on the stack so we
// don't need to pin it. When o.ctx==nil we pass a NULL pointer and the C
// side defaults to a zero block context.
var cctxStorage C.CBlockContext
var cctxPtr *C.CBlockContext
if o.ctx != nil {
ctx := o.ctx
cctxStorage.origin = *(*[20]C.uint8_t)(unsafe.Pointer(&ctx.Origin[0]))
cctxStorage.gas_price = C.uint64_t(ctx.GasPrice)
cctxStorage.timestamp = C.uint64_t(ctx.Timestamp)
cctxStorage.number = C.uint64_t(ctx.Number)
cctxStorage.prevrandao = *(*[32]C.uint8_t)(unsafe.Pointer(&ctx.Prevrandao[0]))
cctxStorage.gas_limit = C.uint64_t(ctx.GasLimit)
cctxStorage.chain_id = C.uint64_t(ctx.ChainID)
cctxStorage.base_fee = C.uint64_t(ctx.BaseFee)
cctxStorage.blob_base_fee = C.uint64_t(ctx.BlobBaseFee)
cctxStorage.coinbase = *(*[20]C.uint8_t)(unsafe.Pointer(&ctx.Coinbase[0]))
cctxStorage.blob_hashes = *(*[8][32]C.uint8_t)(unsafe.Pointer(&ctx.BlobHashes[0][0]))
nbh := ctx.NumBlobHashes
if nbh > 8 {
nbh = 8
}
cctxStorage.num_blob_hashes = C.uint32_t(nbh)
cctxPtr = &cctxStorage
}
// The canonical C++ entry. Pass the resolved thread count and block
// context; revision is Cancun, the one revision the kernels implement.
// No state snapshot is passed, so go_bridge.h takes every account to not
// exist; code_blob is unused without one.
result := C.gpu_execute_block(
&ctxs[0],
C.uint32_t(len(ctxs)),
C.uint8_t(backend),
C.uint32_t(o.numThreads), // 0 ⇒ hardware concurrency for CPU_PARALLEL
C.uint8_t(C.EVM_GPU_REV_CANCUN), // revision
cctxPtr, // block_ctx: caller-supplied or NULL (zero ctx)
nil, // accounts: no state snapshot
0, // n_accounts
nil, // code_blob
0, // code_blob_size
)
// Always free the C-allocated result, even on the error path. The C
// implementation of gpu_free_result is null-safe so this is a no-op when
// result.gas_used is nil.
defer C.gpu_free_result(&result)
// Defensive: the cgo call above is synchronous so ctxs / cctxStorage are
// reachable throughout, but make the contract explicit so a future
// refactor that moves the C call into a goroutine or callback path
// doesn't silently break pointer reachability.
runtime.KeepAlive(ctxs)
runtime.KeepAlive(cctxStorage)
// What ok means is the ABI's, so a result of another ABI is not read at
// all. ok=0 names no gas or status the caller may use (every status reads
// EVM_GPU_TX_ERROR, and the arrays may be NULL). Either way nothing below
// is read: the block goes back to the caller whole.
if got := uint32(result.abi_version); got != ABIVersion {
return nil, errOtherABI(got)
}
if result.ok == 0 {
return nil, ErrDeclined
}
br := &BlockResult{
GasUsed: copyU64(result.gas_used, uint32(result.num_txs)),
TotalGas: uint64(result.total_gas),
ExecTimeMs: float64(result.exec_time_ms),
Conflicts: uint32(result.conflicts),
ReExecutions: uint32(result.re_executions),
ABIVersion: uint32(result.abi_version),
}
for i := 0; i < 32; i++ {
br.StateRoot[i] = byte(result.state_root[i])
}
if result.status != nil && result.num_txs > 0 {
const maxTxsPerBlock = 1 << 24
want := uint32(result.num_txs)
if want > maxTxsPerBlock {
return nil, fmt.Errorf("cevm: result.num_txs=%d exceeds sanity bound", want)
}
statSlice := unsafe.Slice((*uint8)(unsafe.Pointer(result.status)), int(want))
br.Status = make([]TxStatus, want)
for i, s := range statSlice {
br.Status[i] = TxStatus(s)
}
}
return br, nil
}
// BackendName returns the human-readable name of a backend as reported by the
// C++ library (which is authoritative).
func BackendName(b Backend) string {
cstr := C.gpu_backend_name(C.uint8_t(b))
if cstr == nil {
return "unknown"
}
return C.GoString(cstr)
}
// AvailableBackends returns the list of backends compiled and detected
// at runtime by the loaded library.
func AvailableBackends() []Backend {
n := uint32(C.gpu_available_backends(nil, 0))
if n == 0 {
return nil
}
buf := make([]C.uint8_t, n)
got := uint32(C.gpu_available_backends(&buf[0], C.uint32_t(n)))
out := make([]Backend, got)
for i := uint32(0); i < got; i++ {
out[i] = Backend(buf[i])
}
return out
}
// LibraryABIVersion returns the ABI the loaded library speaks: the one it
// reports, or 0 where its CGpuTx is not this build's. When it is not
// ABIVersion, ExecuteBlock declines every block.
func LibraryABIVersion() uint32 {
return libraryABI
}
// healthProbe is one entry in the Health() battery: a block, its context, and
// the gas each of its transactions must come back with at TxOK.
type healthProbe struct {
name string
txs []Transaction
ctx BlockContext
wantGas uint64
}
// healthBattery is what Health runs on every backend.
//
// gpu_execute_block runs one kind of block (go_bridge.h, ABI 7): plain value
// transfers, on a GPU lane's value-transfer path. It declines a batch with
// code, and the CPU lanes run nothing without a host. ExecuteBlock passes no
// state snapshot, so every sender is taken to not exist and to hold nothing:
// the battery is one transfer that costs nothing — no value, no price, no
// base fee — which a lane that can run anything runs to TxOK at its 21000
// intrinsic gas, and which every other lane declines. Its limit is above
// that, so a lane that answers a gas estimate (the limit) instead of running
// it is not taken for healthy.
func healthBattery() []healthProbe {
return []healthProbe{{
name: "transfer",
txs: []Transaction{{From: [20]byte{0x01}, To: [20]byte{0x02}, HasTo: true, GasLimit: 30000}},
ctx: BlockContext{GasLimit: 30_000_000, ChainID: 1},
wantGas: 21000,
}}
}
// HealthProbeResult is the outcome of a single probe on a single backend.
type HealthProbeResult struct {
Name string
OK bool
GasUsed uint64
Status TxStatus
Err error
}
// HealthReport is the per-backend result of Health(). It aggregates the
// per-probe results into a single OK / not-OK signal: a backend is healthy
// iff every probe ran every transaction to TxOK at the gas it must cost.
type HealthReport struct {
Backend Backend
Name string
OK bool
Err error
Probe string // first failing probe name, empty when OK
ProbesRun int
ProbeResults []HealthProbeResult
// Aggregate stats — sum of gas across probes, status of the last probe.
GasUsed uint64
Status TxStatus
ExecTime float64
}
// Health runs the battery through every backend the loaded library exposes
// and returns a per-backend report. Use at process start to see which lanes
// can run a block at all: a GPU lane whose device, driver or library is
// wrong fails here. Returns nil only if the runtime cannot enumerate
// backends.
//
// A backend is OK iff it ran every probe's transactions to TxOK at the gas
// they cost. A lane that declines is not OK and says so: its Err wraps
// ErrDeclined, which is the answer every CPU lane gives this entry.
func Health() []HealthReport {
backends := AvailableBackends()
if len(backends) == 0 {
return nil
}
probes := healthBattery()
out := make([]HealthReport, 0, len(backends))
for _, b := range backends {
rep := HealthReport{
Backend: b,
Name: BackendName(b),
ProbeResults: make([]HealthProbeResult, 0, len(probes)),
}
allOK := true
for _, p := range probes {
pr := runHealthProbe(b, p)
rep.ProbeResults = append(rep.ProbeResults, pr)
rep.ProbesRun++
rep.GasUsed += pr.GasUsed
rep.Status = pr.Status
if !pr.OK && allOK {
allOK = false
rep.Probe = pr.Name
rep.Err = pr.Err
}
}
rep.OK = allOK
out = append(out, rep)
}
return out
}
// runHealthProbe executes one probe on one backend and returns its result.
func runHealthProbe(b Backend, p healthProbe) HealthProbeResult {
pr := HealthProbeResult{Name: p.name}
ctx := p.ctx
r, err := ExecuteBlock(b, p.txs, WithContext(&ctx))
if err != nil {
pr.Err = fmt.Errorf("probe %q: %w", p.name, err)
return pr
}
if len(r.GasUsed) != len(p.txs) || len(r.Status) != len(p.txs) {
pr.Err = fmt.Errorf("probe %q: malformed result (gas=%d status=%d, want %d)",
p.name, len(r.GasUsed), len(r.Status), len(p.txs))
return pr
}
for i := range p.txs {
pr.GasUsed += r.GasUsed[i]
pr.Status = r.Status[i]
if r.Status[i] != TxOK || r.GasUsed[i] != p.wantGas {
pr.Err = fmt.Errorf("probe %q: tx %d came back %s at %d gas, want ok at %d",
p.name, i, r.Status[i], r.GasUsed[i], p.wantGas)
return pr
}
}
pr.OK = true
return pr
}