core/parallel: wire cevm (C++ EVM) in as a byte-exact block shadow-verifier

Route-gated activation of the Lux C++ EVM (cevm) through the BlockExecutor
seam (SEAM 1): parallel.RegisterExecutor makes core/state_processor.go run
ExecuteBlock on every block. Because the CONSENSUS root is taken from the Go
statedb.IntermediateRoot() (block_validator.go), cevm-as-applier would need
host-callback state write-back (deferred); so this lands cevm as a pure
SHADOW VERIFIER — it recomputes the block's keccak-MPT root independently and
confirms it matches the Go EVM's header.Root, then ALWAYS returns (nil,nil)
so the Go path applies the block. Root-only checking sidesteps both known
cevm gaps (event logs, state write-back).

core/parallel/cevmbridge: cgo seam to cevm's extern "C" cevm_process_block
(luxcpp/cevm build-mpt CPU libs, real MPT commit — NOT the placeholder-root
gpu path). Two variants share one API: bridge_cevm.go (//go:build cevm) links
the verified build-mpt + Conan link line and calls the real process_block;
bridge_stub.go (//go:build !cevm) is pure Go and declines, so `go build ./...`
stays green on any machine and the C++ path is opt-in via -tags cevm (the
existing backend_cevm.go convention). Balance is LE limbs; value/gasPrice/ctx
256-bit fields are BE — matching go_process_block.h exactly.

core/parallel/cevm_shadow.go: the BlockExecutor. Snapshots the pre-block state
read-only via DumpToCollector (address<-trie-key preimage, nonce, balance,
code, storage), builds the cevm tx array (sender via block signer, effective
gas price matching evm/core/state_transition.go's full-fee-to-coinbase model),
runs cevmbridge.ProcessBlock, and cross-checks the root against header.Root.
Atomic counters tally agree / disagree / finalize-gap (withdrawals move
header.Root; cevm runs no engine.Finalize — a state no-op on Lux otherwise) /
declined-too-large / declined-no-preimage / declined-tx / errored, read via
CevmShadowStats. Panics in the snapshot/bridge are swallowed and counted: the
verifier can never destabilise consensus. Registration is gated on
cevmbridge.Enabled so default and -tags parallel builds are byte-unchanged.

parallel.go: initialize txExecutors at declaration instead of in init().
Backend files register from their own init() (backend_cevm.go under -tags
cevm), and Go orders init() funcs alphabetically by filename — backend_cevm.go
precedes parallel.go, so the old init()-time map creation raced too late and
panicked on a nil map under -tags cevm. Var initializers run before all
init() funcs, fixing the order deterministically.

Proven:
  - cevmbridge golden roots through the Go marshalling byte-match luxfi/geth:
    N=1 0xb9d849f9…805e9, N=100 0x2028713f…7cf0f, N=1000 0x57b72507…20099.
  - e2e (TestCevmShadowVerifiesRealBlocks, -tags cevm): 3 real blocks through
    core.BlockChain.InsertChain -> StateProcessor.Process -> shadow ExecuteBlock;
    cevm's real-MPT root byte-matched the Go EVM header.Root on all 3
    (Processed:3 Agree:3 Disagree:0, zero declines/errors).
  - default (stub) core/parallel suite unchanged and green.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-07-19 18:04:03 -07:00
co-authored by Hanzo Dev
parent 2141ab22c4
commit f70050eed6
7 changed files with 875 additions and 6 deletions
+350
View File
@@ -0,0 +1,350 @@
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package parallel
// cevm_shadow.go wires the Lux C++ EVM (cevm) into the block executor as a
// byte-exact SHADOW VERIFIER, not an applier.
//
// The map proved two facts that lock this design:
// 1. parallel.RegisterExecutor(e) makes core/state_processor.go route EVERY
// block through e.ExecuteBlock (no SetBackend needed).
// 2. The CONSENSUS state root is taken from the GO statedb.IntermediateRoot()
// (block_validator.go), NOT from cevm's returned root. So cevm as the
// APPLIER would have to mutate the Go statedb via host callbacks (heavy,
// deferred). As a VERIFIER it needs none of that.
//
// So this executor: (a) snapshots the pre-block Go state read-only, (b) replays
// the block's transactions through cevm's real process_block over that snapshot
// to get cevm's independent keccak-MPT root, (c) cross-checks that root against
// header.Root (the post-state root the Go EVM produced when the block was
// built), tallying agree/disagree/finalize-gap/declined counters, and then
// ALWAYS returns (nil, nil) so the Go sequential path is the one that actually
// applies the block. cevm never fabricates receipts and never touches the live
// state — the suite stays correct while cevm's real MPT is checked on every
// block it can represent.
//
// Root-only verification sidesteps BOTH known cevm gaps: it needs no logs (the
// receipts gap) and no state write-back (the applier gap). The proven safe
// subset (pure value transfers + non-reverting calls into existing code) yields
// a byte-exact match; everything else (top-level CREATE, REVERT/OOG, EIP-2930
// access lists, EIP-3529 refunds, or a state too large to snapshot) is tallied
// as a decline/disagree and the Go EVM's root stands unchallenged.
//
// This file is intentionally NOT behind //go:build cevm: it always compiles and
// always registers. When built without -tags cevm, cevmbridge.Enabled is false
// and ExecuteBlock returns (nil, nil) immediately — a behavioural no-op
// identical to the fallback executor, so the default build is unchanged.
import (
"math/big"
"sync/atomic"
"github.com/holiman/uint256"
"github.com/luxfi/evm/core/parallel/cevmbridge"
"github.com/luxfi/evm/core/state"
"github.com/luxfi/geth/common"
"github.com/luxfi/geth/core/types"
"github.com/luxfi/geth/core/vm"
ethparams "github.com/luxfi/geth/params"
)
// Snapshot caps. A block whose pre-state exceeds these is declined (counted as
// declined-too-large) rather than risk an unbounded dump on live chains — the
// shadow verifier is a correctness harness, not a mainnet path.
const (
maxShadowAccounts = 200_000
maxShadowStorage = 1_000_000
)
// Package-level shadow counters (atomic). Read via CevmShadowStats.
var (
cevmProcessed uint64 // blocks cevm actually ran and returned a root for
cevmAgree uint64 // cevm root == header.Root (byte-exact)
cevmDisagree uint64 // cevm root != header.Root, no known-alignment excuse
cevmFinalizeGap uint64 // mismatch attributable to withdrawals (cevm runs no engine.Finalize)
cevmDeclinedTooLarge uint64 // pre-state exceeded the snapshot caps
cevmDeclinedNoPreimage uint64 // an account address was unrecoverable (preimages off) — cannot seed cevm
cevmDeclinedTx uint64 // a tx sender could not be recovered
cevmErrored uint64 // cevm process_block failed / panicked
)
// ShadowStats is a snapshot of the cevm shadow-verifier counters.
type ShadowStats struct {
Processed uint64
Agree uint64
Disagree uint64
FinalizeGap uint64
DeclinedTooLarge uint64
DeclinedNoPreimage uint64
DeclinedTx uint64
Errored uint64
}
// CevmShadowStats returns the current cevm shadow-verifier counters.
func CevmShadowStats() ShadowStats {
return ShadowStats{
Processed: atomic.LoadUint64(&cevmProcessed),
Agree: atomic.LoadUint64(&cevmAgree),
Disagree: atomic.LoadUint64(&cevmDisagree),
FinalizeGap: atomic.LoadUint64(&cevmFinalizeGap),
DeclinedTooLarge: atomic.LoadUint64(&cevmDeclinedTooLarge),
DeclinedNoPreimage: atomic.LoadUint64(&cevmDeclinedNoPreimage),
DeclinedTx: atomic.LoadUint64(&cevmDeclinedTx),
Errored: atomic.LoadUint64(&cevmErrored),
}
}
// ResetCevmShadowStats zeroes the counters (test helper).
func ResetCevmShadowStats() {
for _, p := range []*uint64{
&cevmProcessed, &cevmAgree, &cevmDisagree, &cevmFinalizeGap,
&cevmDeclinedTooLarge, &cevmDeclinedNoPreimage, &cevmDeclinedTx, &cevmErrored,
} {
atomic.StoreUint64(p, 0)
}
}
// CevmShadowEnabled reports whether the real cevm bridge is linked (-tags cevm).
func CevmShadowEnabled() bool { return cevmbridge.Enabled }
func init() {
// Register only when the real C++ EVM is linked (-tags cevm). In the default
// and -tags parallel builds the bridge is a no-op stub, so registering would
// only risk clobbering another package's real BlockExecutor (e.g. a Block-STM
// engine) with a verifier that always declines. Gating on Enabled keeps those
// builds byte-for-byte unchanged while activating the shadow under -tags cevm.
if cevmbridge.Enabled {
RegisterExecutor(cevmShadowExecutor{})
}
}
// cevmShadowExecutor is a BlockExecutor that only ever verifies: it returns
// (nil, nil) unconditionally so the Go sequential path applies the block.
type cevmShadowExecutor struct{}
// ExecuteBlock runs the cevm shadow cross-check and ALWAYS declines. It never
// returns receipts or an error, so it can never break block processing — a
// mismatch or an internal failure only moves a counter.
func (cevmShadowExecutor) ExecuteBlock(
config *ethparams.ChainConfig,
header *types.Header,
txs types.Transactions,
statedb *state.StateDB,
_ vm.Config,
) ([]*types.Receipt, error) {
if !cevmbridge.Enabled || config == nil || header == nil || statedb == nil || len(txs) == 0 {
return nil, nil
}
// The verifier must NEVER destabilise consensus: any panic in the snapshot
// or bridge is swallowed and counted, and we still decline.
defer func() {
if r := recover(); r != nil {
atomic.AddUint64(&cevmErrored, 1)
}
}()
shadowVerify(config, header, txs, statedb)
return nil, nil
}
// shadowVerify performs the read-only snapshot, cevm replay, and root
// cross-check, updating exactly one counter.
func shadowVerify(
config *ethparams.ChainConfig,
header *types.Header,
txs types.Transactions,
statedb *state.StateDB,
) {
// (a) Snapshot the pre-block state read-only. DumpToCollector iterates the
// trie at statedb's original (parent) root, which is exactly the pre-block
// state the block was applied to.
snap := &shadowSnapshot{maxAccts: maxShadowAccounts, maxStorage: maxShadowStorage}
statedb.DumpToCollector(snap, &state.DumpConfig{})
switch {
case snap.tooLarge:
atomic.AddUint64(&cevmDeclinedTooLarge, 1)
return
case snap.missingAddr || snap.badBalance:
// Addresses come from trie-key preimages; without them cevm cannot be
// seeded (its leaf key is keccak256(addr)). Decline honestly.
atomic.AddUint64(&cevmDeclinedNoPreimage, 1)
return
}
// (b) Build the cevm tx array from the block's transactions.
signer := types.MakeSigner(config, header.Number, header.Time)
ctxs := make([]cevmbridge.Tx, len(txs))
for i, tx := range txs {
from, err := types.Sender(signer, tx)
if err != nil {
atomic.AddUint64(&cevmDeclinedTx, 1)
return
}
var t cevmbridge.Tx
copy(t.Sender[:], from[:])
if to := tx.To(); to != nil {
copy(t.Recipient[:], to[:])
} else {
t.IsCreate = true
}
fillBE32(&t.Value, tx.Value())
fillBE32(&t.GasPrice, effectiveGasPrice(tx, header.BaseFee))
t.GasLimit = tx.Gas()
t.Nonce = tx.Nonce()
if d := tx.Data(); len(d) > 0 {
t.Data = d
}
ctxs[i] = t
}
// (c) cevm's independent root over the same pre-state + txs.
res, err := cevmbridge.ProcessBlock(snap.accts, snap.storage, ctxs, blockCtx(config, header))
if err != nil || !res.OK {
atomic.AddUint64(&cevmErrored, 1)
return
}
atomic.AddUint64(&cevmProcessed, 1)
// (d) Cross-check against the root the Go EVM committed for this block.
if common.BytesToHash(res.StateRoot[:]) == header.Root {
atomic.AddUint64(&cevmAgree, 1)
return
}
// Known-alignment gap: cevm credits the coinbase per-tx (gas*gasPrice, which
// matches evm's full-fee-to-coinbase state_transition) but does NOT run
// engine.Finalize. On Lux that Finalize is a state no-op EXCEPT for
// EIP-4895 withdrawals, which move header.Root. Attribute such mismatches to
// the finalize gap rather than blaming cevm.
if header.WithdrawalsHash != nil && *header.WithdrawalsHash != types.EmptyWithdrawalsHash {
atomic.AddUint64(&cevmFinalizeGap, 1)
return
}
atomic.AddUint64(&cevmDisagree, 1)
}
// effectiveGasPrice mirrors evm/core/state_transition.go: the price used for the
// sender debit and the full coinbase fee credit. For a legacy tx this is the
// gas price; for a dynamic-fee tx, min(gasFeeCap, gasTipCap+baseFee).
func effectiveGasPrice(tx *types.Transaction, baseFee *big.Int) *big.Int {
if baseFee == nil {
return tx.GasPrice()
}
sum := new(big.Int).Add(tx.GasTipCap(), baseFee)
if sum.Cmp(tx.GasFeeCap()) > 0 {
return new(big.Int).Set(tx.GasFeeCap())
}
return sum
}
// blockCtx builds the cevm block context from the header. The 256-bit fields are
// big-endian; PrevRandao is the post-merge MixDigest. Revision is derived from
// the chain config (immaterial to the root for the proven transfer subset).
func blockCtx(config *ethparams.ChainConfig, header *types.Header) cevmbridge.BlockCtx {
var c cevmbridge.BlockCtx
copy(c.Coinbase[:], header.Coinbase[:])
if header.Number != nil {
c.BlockNumber = header.Number.Int64()
}
c.BlockTime = int64(header.Time)
c.BlockGasLimit = int64(header.GasLimit)
if config.ChainID != nil {
fillBE32(&c.ChainID, config.ChainID)
}
if header.BaseFee != nil {
fillBE32(&c.BaseFee, header.BaseFee)
}
copy(c.PrevRandao[:], header.MixDigest[:])
c.Revision = revisionOf(config, header)
return c
}
func revisionOf(config *ethparams.ChainConfig, header *types.Header) uint8 {
num, t := header.Number, header.Time
switch {
case config.IsCancun(num, t):
return cevmbridge.RevCancun
case config.IsShanghai(num, t):
return cevmbridge.RevShanghai
case config.IsLondon(num):
return cevmbridge.RevLondon
case config.IsBerlin(num):
return cevmbridge.RevBerlin
case config.IsIstanbul(num):
return cevmbridge.RevIstanbul
default:
return cevmbridge.RevIstanbul
}
}
// fillBE32 writes v as a 32-byte big-endian value (right-aligned, zero-padded).
func fillBE32(dst *[32]byte, v *big.Int) {
*dst = [32]byte{}
if v == nil || v.Sign() == 0 {
return
}
b := v.Bytes() // big-endian, minimal
if len(b) >= 32 {
copy(dst[:], b[len(b)-32:])
return
}
copy(dst[32-len(b):], b)
}
// shadowSnapshot is a DumpCollector that materializes the pre-block accounts +
// storage into cevmbridge structs, bounded by the snapshot caps.
type shadowSnapshot struct {
maxAccts, maxStorage int
accts []cevmbridge.Account
storage []cevmbridge.Storage
missingAddr bool
badBalance bool
tooLarge bool
}
func (s *shadowSnapshot) OnRoot(common.Hash) {}
func (s *shadowSnapshot) OnAccount(addr *common.Address, acc state.DumpAccount) {
if s.tooLarge || s.missingAddr || s.badBalance {
return
}
if addr == nil {
// No preimage for this trie key: cannot recover the 20-byte address.
s.missingAddr = true
return
}
if len(s.accts) >= s.maxAccts {
s.tooLarge = true
return
}
var a cevmbridge.Account
copy(a.Address[:], addr[:])
a.Nonce = acc.Nonce
bal, err := uint256.FromDecimal(acc.Balance)
if err != nil {
s.badBalance = true
return
}
a.Balance = [4]uint64{bal[0], bal[1], bal[2], bal[3]}
if len(acc.Code) > 0 {
a.Code = append([]byte(nil), acc.Code...)
}
s.accts = append(s.accts, a)
for k, vhex := range acc.Storage {
if len(s.storage) >= s.maxStorage {
s.tooLarge = true
return
}
var st cevmbridge.Storage
copy(st.Address[:], addr[:])
copy(st.Key[:], k[:])
vb := common.Hex2Bytes(vhex) // dump value is hex without 0x, big-endian minimal
if len(vb) >= 32 {
copy(st.Value[:], vb[len(vb)-32:])
} else {
copy(st.Value[32-len(vb):], vb)
}
s.storage = append(s.storage, st)
}
}
+113
View File
@@ -0,0 +1,113 @@
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
//go:build cevm
// End-to-end proof that the cevm shadow verifier runs over REAL Go-produced
// blocks: it drives core.BlockChain.InsertChain (which calls
// StateProcessor.Process -> parallel.DefaultExecutor().ExecuteBlock on every
// block), and asserts that cevm's independent keccak-MPT root byte-matches the
// header.Root the Go EVM committed. This is the honest deliverable: cevm's real
// process_block is exercised against genuine blocks, not a synthetic fixture.
//
// Built only under -tags cevm (needs the linked C++ EVM). Lives in the external
// test package so it may import core without an import cycle.
package parallel_test
import (
"math/big"
"testing"
"github.com/luxfi/crypto"
"github.com/luxfi/evm/consensus/dummy"
"github.com/luxfi/evm/core"
"github.com/luxfi/evm/core/parallel"
"github.com/luxfi/evm/params"
"github.com/luxfi/evm/plugin/evm/upgrade/legacy"
"github.com/luxfi/geth/common"
"github.com/luxfi/geth/core/rawdb"
"github.com/luxfi/geth/core/types"
"github.com/luxfi/geth/core/vm"
)
func TestCevmShadowVerifiesRealBlocks(t *testing.T) {
if !parallel.CevmShadowEnabled() {
t.Skip("cevm bridge not linked (build with -tags cevm)")
}
key1, _ := crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
key2, _ := crypto.HexToECDSA("8a1f9a8f95be41cd7ccb6168179afb4504aefe388d1e14474d32c45c72ce7b7a")
cryptoFrom := crypto.PubkeyToAddress(key1.PublicKey)
cryptoTo := crypto.PubkeyToAddress(key2.PublicKey)
from := common.BytesToAddress(cryptoFrom[:])
to := common.BytesToAddress(cryptoTo[:])
coinbase := common.HexToAddress("0xCafE00000000000000000000000000000000C0DE")
funds := new(big.Int).Mul(big.NewInt(1000), big.NewInt(params.Ether))
gspec := &core.Genesis{
Config: params.TestChainConfig,
Alloc: core.GenesisAlloc{from: {Balance: funds}, to: {Balance: funds}},
BaseFee: big.NewInt(legacy.BaseFee),
}
signer := types.LatestSigner(gspec.Config)
engine := dummy.NewCoinbaseFaker()
// Commit genesis WITH preimages so the shadow verifier can recover the
// 20-byte addresses from the pre-block trie keys (its leaf key is
// keccak256(addr)). Preimages off => the verifier declines honestly.
db := rawdb.NewMemoryDatabase()
cacheConfig := *core.DefaultCacheConfig
cacheConfig.Preimages = true
cacheConfig.SnapshotLimit = 0
chain, err := core.NewBlockChain(db, &cacheConfig, gspec, engine, vm.Config{}, common.Hash{}, false, nil)
if err != nil {
t.Fatalf("NewBlockChain: %v", err)
}
defer chain.Stop()
// Generate real blocks of pure value transfers (the proven safe subset).
// gasLimit == 21000 so gasUsed == gasLimit (no refund ambiguity); gasPrice
// == baseFee so the effective price is exactly baseFee; full fee -> coinbase
// matches cevm's per-tx coinbase credit.
const nBlocks = 3
blocks, _, err := core.GenerateChain(gspec.Config, chain.Genesis(), engine, db, nBlocks, 10, func(i int, b *core.BlockGen) {
b.SetCoinbase(coinbase)
tx := types.NewTransaction(uint64(i), to, big.NewInt(int64(1000+i)), 21000, big.NewInt(legacy.BaseFee), nil)
signed, serr := types.SignTx(tx, signer, key1)
if serr != nil {
t.Fatalf("SignTx: %v", serr)
}
b.AddTx(signed)
})
if err != nil {
t.Fatalf("GenerateChain: %v", err)
}
parallel.ResetCevmShadowStats()
if _, err := chain.InsertChain(blocks); err != nil {
t.Fatalf("InsertChain: %v", err)
}
s := parallel.CevmShadowStats()
t.Logf("cevm shadow stats over %d real blocks: %+v", nBlocks, s)
// Every block carried exactly one pure transfer over a fully-snapshottable
// pre-state with preimages on, so cevm must have PROCESSED every block and
// AGREED byte-exactly with the Go EVM root on each.
if s.DeclinedNoPreimage > 0 {
t.Fatalf("verifier declined %d blocks for missing preimages — genesis/state preimages not available", s.DeclinedNoPreimage)
}
if s.Processed != nBlocks {
t.Fatalf("cevm processed %d blocks, want %d (declinedTooLarge=%d declinedTx=%d errored=%d)",
s.Processed, nBlocks, s.DeclinedTooLarge, s.DeclinedTx, s.Errored)
}
if s.Agree != nBlocks {
t.Fatalf("cevm agreed on %d/%d blocks (disagree=%d finalizeGap=%d) — root mismatch vs Go EVM",
s.Agree, nBlocks, s.Disagree, s.FinalizeGap)
}
if s.Disagree != 0 {
t.Fatalf("cevm disagreed on %d blocks", s.Disagree)
}
t.Logf("PASS: cevm real-MPT root byte-matched the Go EVM header.Root on all %d blocks", s.Agree)
}
+177
View File
@@ -0,0 +1,177 @@
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
//go:build cevm
package cevmbridge
// Real cgo bridge into the Lux C++ EVM (cevm) block-batch consensus entry
// point. Links the FRESH build-mpt CPU libs that contain the real keccak
// Merkle-Patricia-Trie commit — NOT the stale /opt/homebrew pre-MPT libs and
// NOT the gpu execute_block placeholder-root path.
//
// The cgo link line below is byte-for-byte the one proven to link and return
// the byte-exact geth empty-MPT root through this exact entry point (see the
// map:linking verification). cgo takes LITERAL absolute paths — no env/${SRCDIR}
// expansion — so the paths are spelled out. The Conan hash dirs
// (lux-c4c40b1a25281d = CPU precompile deps, bls3f47da987ce4d = blst) are the
// authoritative set read from the gate's own link.txt. -Wl,-rpath is MANDATORY:
// libevm.0.19.0.dylib has install name @rpath/libevm.0.19.dylib.
//
// The preamble only includes the C ABI header (pure C, extern "C"); the C++
// symbols it pulls resolve from the linked archives, so -lc++ is required and
// not duplicated (this is a C cgo package — cgo does not auto-add libc++ here).
/*
#cgo CFLAGS: -I/Users/z/work/luxcpp/cevm/lib/evm
#cgo LDFLAGS: /Users/z/work/luxcpp/cevm/build-mpt/build/Release/lib/evm/libevm-state.a /Users/z/work/luxcpp/cevm/build-mpt/build/Release/lib/libevm.0.19.0.dylib /Users/z/work/luxcpp/cevm/build-mpt/build/Release/lib/cevm_precompiles/libcevm_precompiles.a /Users/z/work/luxcpp/cevm/build-mpt/build/Release/lib/cevm_precompiles/libcevm_bls_kzg_canonical_cpu.a /Users/z/.conan2/p/b/lux-c4c40b1a25281d/p/lib/libbls_evm.a /Users/z/.conan2/p/b/bls3f47da987ce4d/p/lib/libblst.a /Users/z/.conan2/p/b/lux-c4c40b1a25281d/p/lib/libbn254.a /Users/z/.conan2/p/b/lux-c4c40b1a25281d/p/lib/libbn254_cpu.a /Users/z/.conan2/p/b/lux-c4c40b1a25281d/p/lib/libmodexp.a /Users/z/.conan2/p/b/lux-c4c40b1a25281d/p/lib/libmodexp_cpu.a /Users/z/.conan2/p/b/lux-c4c40b1a25281d/p/lib/libsha256.a /Users/z/.conan2/p/b/lux-c4c40b1a25281d/p/lib/libsha256_cpu.a /Users/z/.conan2/p/b/lux-c4c40b1a25281d/p/lib/libripemd160.a /Users/z/.conan2/p/b/lux-c4c40b1a25281d/p/lib/libripemd160_cpu.a /Users/z/.conan2/p/b/lux-c4c40b1a25281d/p/lib/libblake2b.a /Users/z/.conan2/p/b/lux-c4c40b1a25281d/p/lib/libblake2b_cpu.a /Users/z/.conan2/p/b/lux-c4c40b1a25281d/p/lib/libsecp256r1.a /Users/z/.conan2/p/b/lux-c4c40b1a25281d/p/lib/libsecp256r1_cpu.a -Wl,-rpath,/Users/z/work/luxcpp/cevm/build-mpt/build/Release/lib -lc++
#include <stdlib.h>
#include "state/go_process_block.h"
*/
import "C"
import (
"fmt"
"unsafe"
)
// Enabled reports that the real C++ EVM bridge is linked.
const Enabled = true
// ABIVersion returns the ABI version compiled into the linked cevm library.
func ABIVersion() uint32 { return uint32(C.cevm_pb_abi_version()) }
// ProcessBlock seeds a cevm StateDB from accts+storage, replays txs through a
// real evmc_create_cevm() VM, and returns the real MPT state_root + per-tx
// status/gas. It is a pure function: it does not touch any Go state.
func ProcessBlock(accts []Account, storage []Storage, txs []Tx, ctx BlockCtx) (Result, error) {
// C buffers allocated for code/data payloads; freed on return.
var frees []unsafe.Pointer
defer func() {
for _, p := range frees {
C.free(p)
}
}()
// --- accounts --------------------------------------------------------
cAccts := make([]C.cevm_pb_acct, len(accts))
for i := range accts {
a := &accts[i]
for j := 0; j < 20; j++ {
cAccts[i].address[j] = C.uint8_t(a.Address[j])
}
cAccts[i].nonce = C.uint64_t(a.Nonce)
for j := 0; j < 4; j++ {
cAccts[i].balance[j] = C.uint64_t(a.Balance[j])
}
if len(a.Code) > 0 {
p := C.CBytes(a.Code)
frees = append(frees, p)
cAccts[i].code = (*C.uint8_t)(p)
cAccts[i].code_len = C.uint32_t(len(a.Code))
}
}
// --- storage ---------------------------------------------------------
cStor := make([]C.cevm_pb_storage, len(storage))
for i := range storage {
s := &storage[i]
for j := 0; j < 20; j++ {
cStor[i].address[j] = C.uint8_t(s.Address[j])
}
for j := 0; j < 32; j++ {
cStor[i].key[j] = C.uint8_t(s.Key[j])
cStor[i].value[j] = C.uint8_t(s.Value[j])
}
}
// --- transactions ----------------------------------------------------
cTxs := make([]C.cevm_pb_tx, len(txs))
for i := range txs {
t := &txs[i]
for j := 0; j < 20; j++ {
cTxs[i].sender[j] = C.uint8_t(t.Sender[j])
cTxs[i].recipient[j] = C.uint8_t(t.Recipient[j])
}
for j := 0; j < 32; j++ {
cTxs[i].value[j] = C.uint8_t(t.Value[j])
cTxs[i].gas_price[j] = C.uint8_t(t.GasPrice[j])
}
cTxs[i].gas_limit = C.uint64_t(t.GasLimit)
cTxs[i].nonce = C.uint64_t(t.Nonce)
if len(t.Data) > 0 {
p := C.CBytes(t.Data)
frees = append(frees, p)
cTxs[i].data = (*C.uint8_t)(p)
cTxs[i].data_len = C.uint32_t(len(t.Data))
}
if t.IsCreate {
cTxs[i].is_create = 1
}
}
// --- context ---------------------------------------------------------
var cCtx C.cevm_pb_ctx
for j := 0; j < 20; j++ {
cCtx.coinbase[j] = C.uint8_t(ctx.Coinbase[j])
}
cCtx.block_number = C.int64_t(ctx.BlockNumber)
cCtx.block_timestamp = C.int64_t(ctx.BlockTime)
cCtx.block_gas_limit = C.int64_t(ctx.BlockGasLimit)
for j := 0; j < 32; j++ {
cCtx.chain_id[j] = C.uint8_t(ctx.ChainID[j])
cCtx.block_base_fee[j] = C.uint8_t(ctx.BaseFee[j])
cCtx.prev_randao[j] = C.uint8_t(ctx.PrevRandao[j])
cCtx.blob_base_fee[j] = C.uint8_t(ctx.BlobBaseFee[j])
}
cCtx.revision = C.uint8_t(ctx.Revision)
var (
txPtr *C.cevm_pb_tx
acctPtr *C.cevm_pb_acct
storPtr *C.cevm_pb_storage
)
if len(cTxs) > 0 {
txPtr = &cTxs[0]
}
if len(cAccts) > 0 {
acctPtr = &cAccts[0]
}
if len(cStor) > 0 {
storPtr = &cStor[0]
}
r := C.cevm_process_block(
txPtr, C.uint32_t(len(cTxs)),
acctPtr, C.uint32_t(len(cAccts)),
storPtr, C.uint32_t(len(cStor)),
&cCtx,
)
defer C.cevm_pb_free(&r)
var res Result
res.ABIVersion = uint32(r.abi_version)
res.OK = r.ok != 0
res.TotalGas = uint64(r.total_gas)
for j := 0; j < 32; j++ {
res.StateRoot[j] = byte(r.state_root[j])
}
if r.ok == 0 {
return res, fmt.Errorf("cevmbridge: cevm_process_block returned ok=0")
}
if n := int(r.n_tx_results); n > 0 && r.tx_results != nil {
cres := unsafe.Slice((*C.cevm_pb_txresult)(unsafe.Pointer(r.tx_results)), n)
res.TxResults = make([]TxResult, n)
for i := 0; i < n; i++ {
res.TxResults[i] = TxResult{
EVMCStatus: int32(cres[i].evmc_status),
Status: uint8(cres[i].status),
Rejected: cres[i].rejected != 0,
GasUsed: uint64(cres[i].gas_used),
CumulativeGasUsed: uint64(cres[i].cumulative_gas_used),
NLogs: uint32(cres[i].n_logs),
}
}
}
return res, nil
}
@@ -0,0 +1,95 @@
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
//go:build cevm
package cevmbridge
import (
"encoding/hex"
"testing"
)
// sndr mirrors test/cabi_block_parity.c: bytes[19]=i, [18]=i>>8, [17]=i>>16.
func sndr(i uint32) [20]byte {
var a [20]byte
a[19] = byte(i)
a[18] = byte(i >> 8)
a[17] = byte(i >> 16)
return a
}
// rcpt(i) = sndr(i) with bytes[16]=0x01.
func rcpt(i uint32) [20]byte {
a := sndr(i)
a[16] = 0x01
return a
}
// TestProcessBlockGoldenRoots drives the exact golden N-transfer blocks the C
// driver (cevm-cabi-block-parity) proves byte-identical to luxfi/geth, but
// through the GO marshalling in this package. A match here proves the Go structs
// serialize into the C ABI identically to the reference C driver.
func TestProcessBlockGoldenRoots(t *testing.T) {
cases := []struct {
n uint32
golden string
}{
{1, "b9d849f90993abb25f57172eb086e4b6ac15369954b0c556ae2280a1559805e9"},
{100, "2028713fd7a12c1c73fb90f7b9de08be87c1d23b76acb9d2a4cfe3788577cf0f"},
{1000, "57b7250733ad7f7a1be82d7f1e8d385496b00d2bb93a553b6730dd83dd020099"},
}
if ABIVersion() != 1 {
t.Fatalf("unexpected ABI version %d (want 1)", ABIVersion())
}
for _, c := range cases {
accts := make([]Account, c.n)
txs := make([]Tx, c.n)
for i := uint32(0); i < c.n; i++ {
accts[i] = Account{Address: sndr(i), Nonce: 0, Balance: [4]uint64{1000000, 0, 0, 0}}
var value [32]byte
value[31] = 1 // big-endian value = 1
txs[i] = Tx{
Sender: sndr(i),
Recipient: rcpt(i),
Value: value,
// GasPrice all-zero
GasLimit: 21000,
Nonce: 0,
}
}
var ctx BlockCtx
ctx.Coinbase[19] = 0xFF
ctx.BlockNumber = 1
ctx.BlockTime = 1700000000
ctx.BlockGasLimit = 30000000
ctx.ChainID[31] = 1
ctx.Revision = RevShanghai
res, err := ProcessBlock(accts, nil, txs, ctx)
if err != nil {
t.Fatalf("N=%d: ProcessBlock error: %v", c.n, err)
}
if !res.OK {
t.Fatalf("N=%d: ProcessBlock ok=false", c.n)
}
got := hex.EncodeToString(res.StateRoot[:])
if got != c.golden {
t.Fatalf("N=%d: root mismatch\n got=0x%s\nwant=0x%s", c.n, got, c.golden)
}
t.Logf("N=%-4d cevm=0x%s == geth PASS", c.n, got)
if len(res.TxResults) != int(c.n) {
t.Fatalf("N=%d: got %d tx results, want %d", c.n, len(res.TxResults), c.n)
}
for i, tr := range res.TxResults {
if tr.Status != 1 || tr.GasUsed != 21000 || tr.NLogs != 0 || tr.Rejected {
t.Fatalf("N=%d tx[%d] unexpected: status=%d gas=%d nlogs=%d rejected=%v",
c.n, i, tr.Status, tr.GasUsed, tr.NLogs, tr.Rejected)
}
}
}
}
+20
View File
@@ -0,0 +1,20 @@
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
//go:build !cevm
package cevmbridge
// Enabled reports whether the real C++ EVM bridge is linked. It is false in the
// default build; build with -tags cevm to link the cevm libs and get the real
// process_block path.
const Enabled = false
// ABIVersion returns 0 when the bridge is not linked.
func ABIVersion() uint32 { return 0 }
// ProcessBlock is a no-op in the non-cevm build: it always declines with
// ErrDisabled so callers fall through to the Go EVM.
func ProcessBlock(_ []Account, _ []Storage, _ []Tx, _ BlockCtx) (Result, error) {
return Result{}, ErrDisabled
}
+113
View File
@@ -0,0 +1,113 @@
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Package cevmbridge is the cgo seam from Go into the Lux C++ EVM (cevm)
// block-batch consensus entry point evm::state::process_block, exposed through
// the extern "C" ABI declared in luxcpp/cevm/lib/evm/state/go_process_block.h.
//
// ProcessBlock seeds a fresh C++ StateDB from a caller-supplied read-only
// snapshot of the pre-block accounts + storage, replays the block's
// transactions through a real evmc_create_cevm() VM, and returns the REAL
// keccak Merkle-Patricia-Trie state root (db.commit()) plus per-tx status and
// gas. It NEVER mutates the caller's Go state — it is a pure function of its
// inputs, which is exactly what a byte-exact shadow verifier needs.
//
// Two build variants share this one API:
//
// - //go:build cevm : bridge_cevm.go links the fresh build-mpt CPU libs and
// calls the real cevm_process_block. Enabled == true.
// - //go:build !cevm : bridge_stub.go is pure Go, ProcessBlock returns
// ErrDisabled. Enabled == false. This keeps `go build ./...` green on any
// machine (no hardcoded C++ lib paths, no cgo link) while the real path is
// opt-in via -tags cevm, mirroring the existing backend_cevm.go convention.
//
// All multi-byte scalar encodings match go_process_block.h exactly:
// - Account.Balance : 4 x uint64 little-endian limbs, [0] = low 64 bits.
// - Tx.Value / Tx.GasPrice, Storage.Key / Storage.Value, and the BlockCtx
// 256-bit fields : 32-byte BIG-endian.
package cevmbridge
import "errors"
// ErrDisabled is returned by ProcessBlock when the package was built without
// the `cevm` tag (the C++ EVM is not linked).
var ErrDisabled = errors.New("cevmbridge: built without -tags cevm (C++ EVM not linked)")
// Account is a read-only snapshot of one pre-block account, materialized into
// the cevm StateDB before the block runs.
type Account struct {
Address [20]byte
Nonce uint64
Balance [4]uint64 // little-endian limbs, [0] = low 64 bits
Code []byte // nil/empty => EOA
}
// Storage is one pre-existing contract storage slot.
type Storage struct {
Address [20]byte // owning account
Key [32]byte // big-endian slot key
Value [32]byte // big-endian slot value
}
// Tx is one transaction to replay. Value and GasPrice are 32-byte big-endian.
type Tx struct {
Sender [20]byte
Recipient [20]byte // ignored when IsCreate
Value [32]byte // big-endian
GasPrice [32]byte // big-endian
GasLimit uint64
Nonce uint64
Data []byte // calldata or init code
IsCreate bool
}
// BlockCtx is the block / transaction context. The 256-bit fields are 32-byte
// big-endian; Revision is an evmc_revision value (see the CEVM_PB_REV_* consts
// mirrored below).
type BlockCtx struct {
Coinbase [20]byte
BlockNumber int64
BlockTime int64
BlockGasLimit int64
ChainID [32]byte // big-endian
BaseFee [32]byte // big-endian
PrevRandao [32]byte // big-endian
BlobBaseFee [32]byte // big-endian
Revision uint8
}
// TxResult is the per-tx outcome cevm surfaced (index-aligned to the input txs).
type TxResult struct {
EVMCStatus int32 // raw evmc_status_code (SUCCESS=0, FAILURE=1, REVERT=2, ...)
Status uint8 // Go receipt status: 1 iff EVMC_SUCCESS
Rejected bool // true iff EVMC_REJECTED (nonce/intrinsic/balance) — not block-includable
GasUsed uint64
CumulativeGasUsed uint64
NLogs uint32 // logs this tx emitted (0 => within the proven safe subset)
}
// Result is the whole-block outcome.
type Result struct {
StateRoot [32]byte // REAL keccak Merkle-Patricia-Trie root
TxResults []TxResult // length == len(txs) on success
TotalGas uint64
OK bool
ABIVersion uint32
}
// evmc_revision values (mirror go_process_block.h CEVM_PB_REV_*).
const (
RevFrontier uint8 = 0
RevHomestead uint8 = 1
RevTangerine uint8 = 2
RevSpurious uint8 = 3
RevByzantium uint8 = 4
RevConstantinople uint8 = 5
RevPetersburg uint8 = 6
RevIstanbul uint8 = 7
RevBerlin uint8 = 8
RevLondon uint8 = 9
RevParis uint8 = 10
RevShanghai uint8 = 11
RevCancun uint8 = 12
)
+7 -6
View File
@@ -27,13 +27,14 @@ var (
mu sync.RWMutex
executor BlockExecutor
accelerator GPUAccelerator
txExecutors map[EVMBackend]TransactionExecutor
activeBack EVMBackend = GoEVM
)
func init() {
// txExecutors is initialized at declaration (not in init()) so it exists
// before ANY package init() runs. Backend files register from their own
// init() (e.g. backend_cevm.go under -tags cevm), and Go orders init()
// funcs alphabetically by filename — backend_cevm.go precedes parallel.go,
// so an init()-time map creation would be too late and panic on a nil map.
txExecutors = make(map[EVMBackend]TransactionExecutor)
}
activeBack = GoEVM
)
// RegisterExecutor sets the parallel block executor.
func RegisterExecutor(e BlockExecutor) {