mirror of
https://github.com/luxfi/evm.git
synced 2026-09-24 00:27:44 +00:00
core/parallel: cevm applier holds RESIDENT state across blocks (no per-block dump)
The cevm gated applier (cevm_shadow.go) dumped the WHOLE Go pre-state
every block and rebuilt a fresh cevm StateDB for each — capped at
maxShadowAccounts, which WAS the declined-too-large decline at scale.
Wire the persistent cevm StateDB behind a per-chain handle instead:
- bridge_cevm.go: StateCreate/StateSeed/StateApplyBlock/StateRoot/StateFree
wrappers over the new cevm_state_* C-ABI; marshalling factored into
shared marshal*/unmarshal* helpers (DRY with ProcessBlock). Stubs in
bridge_stub.go keep the default (non-cevm) build a no-op.
- cevm_shadow.go: a residentState per chain (keyed by chain ID). The FIRST
block seeds cevm once from the Go pre-state dump; SUBSEQUENT in-sync
blocks call StateApplyBlock with NO dump and compare cevm's resident root
to header.Root. On AGREE the four-way apply gate writes cevm's post-state
delta into the Go statedb (unchanged) and the resident advances to height
N. On DISAGREE/DECLINE/ERROR the resident is flagged out-of-sync and
re-seeded from the (correct) Go state on the next block. New counters
ResidentApplied / ResidentSeeds / ResidentReseeds. A wrong resident root
can never commit — the root check + four-way gate decline it.
Proof (CGO_ENABLED=1 -tags cevm):
- TestCevmResidentVerifiesRealBlocks: over the same 256 real InsertChain
blocks, cevm's RESIDENT root == Go header.Root at ALL 256 heights,
applied via the resident handle, with ResidentSeeds == 1 (not 256) and
0 resyncs — the declined-too-large killer: 1 full-state dump vs 256 blocks.
- TestStateApplyBlockGoldenRoots + TestStateResidentAcrossBlocksMatchesStateless
(cevmbridge): the resident C-ABI is byte-identical to the geth golden
roots and to a stateless reseed across blocks.
- Existing TestProcessBlockGoldenRoots (stateless), the 256-block
TestCevmShadowVerifiesRealBlocks, and the 9/9 applier conformance all
stay green.
Caveat: this makes the SHADOW/applier resident (no per-block dump). cevm as
the SOLE mainnet engine additionally needs zero declines + disk-backed state
+ node ownership — not claimed here.
Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
// 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 applier holds RESIDENT state across blocks: it
|
||||
// drives core.BlockChain.InsertChain over the SAME 256 real Go-produced blocks
|
||||
// TestCevmShadowVerifiesRealBlocks uses, but asserts the resident metrics —
|
||||
// cevm's resident keccak-MPT root byte-matches the Go header.Root at EVERY
|
||||
// height (Agree == N, applied via the resident handle), while the full pre-state
|
||||
// is dumped+seeded ~ONCE, not N times. The ResidentSeeds == 1 assertion is the
|
||||
// declined-too-large-killer: the old shadow dumped the whole pre-state every
|
||||
// block (capped at maxShadowAccounts, which WAS the declined-too-large decline);
|
||||
// the resident applier dumps once and then applies incrementally.
|
||||
//
|
||||
// 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 TestCevmResidentVerifiesRealBlocks(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(1_000_000), 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 applier can recover 20-byte addresses
|
||||
// from the pre-block trie keys when it SEEDS (its leaf key is keccak256(addr)).
|
||||
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()
|
||||
|
||||
// MANY real blocks of pure value transfers (the proven safe subset). Each
|
||||
// block carries several transfers from key1 to deterministically-distinct
|
||||
// fresh recipients. gasLimit == 21000 so gasUsed == gasLimit; gasPrice ==
|
||||
// baseFee so full fee -> coinbase matches cevm's per-tx credit. The blocks
|
||||
// flow through the REAL StateProcessor (InsertChain -> Process ->
|
||||
// parallel.DefaultExecutor().ExecuteBlock = the RESIDENT cevm applier).
|
||||
const (
|
||||
nBlocks = 256
|
||||
txper = 3
|
||||
)
|
||||
var nonce uint64
|
||||
blocks, _, err := core.GenerateChain(gspec.Config, chain.Genesis(), engine, db, nBlocks, 10, func(i int, b *core.BlockGen) {
|
||||
b.SetCoinbase(coinbase)
|
||||
for j := 0; j < txper; j++ {
|
||||
var rcpt common.Address
|
||||
rcpt[0] = 0x11
|
||||
rcpt[18] = byte(i)
|
||||
rcpt[19] = byte(j)
|
||||
tx := types.NewTransaction(nonce, rcpt, big.NewInt(int64(1000+i*txper+j)), 21000, big.NewInt(legacy.BaseFee), nil)
|
||||
signed, serr := types.SignTx(tx, signer, key1)
|
||||
if serr != nil {
|
||||
t.Fatalf("SignTx: %v", serr)
|
||||
}
|
||||
b.AddTx(signed)
|
||||
nonce++
|
||||
}
|
||||
_ = to
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateChain: %v", err)
|
||||
}
|
||||
|
||||
// Reset (also frees any prior resident handle so this chain seeds fresh).
|
||||
parallel.ResetCevmShadowStats()
|
||||
if _, err := chain.InsertChain(blocks); err != nil {
|
||||
t.Fatalf("InsertChain: %v", err)
|
||||
}
|
||||
s := parallel.CevmShadowStats()
|
||||
|
||||
t.Logf("cevm RESIDENT stats over %d real blocks (%d txs): %+v", nBlocks, nBlocks*txper, s)
|
||||
|
||||
// Correctness: cevm's resident root byte-matched the Go header.Root at EVERY
|
||||
// height, and every block was APPLIED via the resident handle.
|
||||
if s.DeclinedNoPreimage > 0 {
|
||||
t.Fatalf("applier declined %d blocks for missing preimages", 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) — resident 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)
|
||||
}
|
||||
if s.ResidentApplied != nBlocks {
|
||||
t.Fatalf("cevm resident-applied %d/%d blocks — some block did not go through the resident handle path",
|
||||
s.ResidentApplied, nBlocks)
|
||||
}
|
||||
|
||||
// THE WIN: across N blocks the full pre-state is dumped+seeded ~ONCE (genesis
|
||||
// + rare resyncs), not N times. For this clean transfer suite it is exactly 1
|
||||
// seed and 0 resyncs — every subsequent block applied against the resident
|
||||
// tries with NO dump. That is the declined-too-large-killer.
|
||||
if s.ResidentSeeds != 1 {
|
||||
t.Fatalf("cevm seeded the full pre-state %d times over %d blocks — want exactly 1 (the no-per-block-dump win)",
|
||||
s.ResidentSeeds, nBlocks)
|
||||
}
|
||||
if s.ResidentReseeds != 0 {
|
||||
t.Fatalf("cevm resynced %d times on a clean transfer suite — want 0", s.ResidentReseeds)
|
||||
}
|
||||
|
||||
t.Logf("PASS: cevm RESIDENT root byte-matched the Go header.Root on ALL %d/%d blocks (%d transfers), "+
|
||||
"applied via the resident handle, with %d full-state seed (not %d) — declined-too-large killed: dumps %d vs blocks %d",
|
||||
s.Agree, s.ResidentApplied, nBlocks*txper, s.ResidentSeeds, nBlocks, s.ResidentSeeds, nBlocks)
|
||||
}
|
||||
+219
-67
@@ -16,13 +16,24 @@ package parallel
|
||||
// after which block_validator recomputes IntermediateRoot over that
|
||||
// written-back state — which must equal cevm's root (== header.Root).
|
||||
//
|
||||
// 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 + per-tx results + logs + a
|
||||
// POST-STATE DELTA (ABI v2), (c) cross-checks cevm's root against header.Root
|
||||
// (the post-state root the Go EVM produced when the block was built), tallying
|
||||
// So this executor: (a) keeps a RESIDENT cevm StateDB per chain (held C-side
|
||||
// behind an opaque handle) and dumps + seeds the pre-block Go state into it only
|
||||
// ONCE per sync — the first block for a chain, or a resync after a drift — NOT
|
||||
// every block; (b) applies each block's transactions against that resident state
|
||||
// (no dump on the in-sync path) through cevm's real process_block, whose
|
||||
// incremental commit() re-hashes only the O(changed) paths, to get cevm's
|
||||
// independent keccak-MPT root + per-tx results + logs + a POST-STATE DELTA (ABI
|
||||
// v2); (c) cross-checks cevm's root against header.Root (the post-state root the
|
||||
// Go EVM produced when the block was built), tallying
|
||||
// agree/disagree/finalize-gap/declined counters.
|
||||
//
|
||||
// The resident state is what kills the per-block full-state dump (the
|
||||
// declined-too-large decline at scale): across N blocks cevm dumps the full
|
||||
// state ~ONCE, not N times. Correctness is unchanged — the resident root is
|
||||
// verified against the header at every height, and on any mismatch the resident
|
||||
// is re-seeded from the (correct) Go state and the block is declined to the Go
|
||||
// EVM, so a drifted resident can never commit wrong state.
|
||||
//
|
||||
// When the roots AGREE, it PROMOTES to applier under a four-way safety gate:
|
||||
// - write cevm's post-state delta into an ISOLATED COPY of the Go statedb;
|
||||
// - compute that copy's IntermediateRoot(IsEIP158) and rebuild the receipts;
|
||||
@@ -56,6 +67,7 @@ package parallel
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/holiman/uint256"
|
||||
@@ -71,9 +83,13 @@ import (
|
||||
"github.com/luxfi/geth/trie"
|
||||
)
|
||||
|
||||
// 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.
|
||||
// Seed caps. The resident path dumps the full pre-state only on a SEED/RESYNC
|
||||
// (not every block), so these bound that one dump: a chain whose state exceeds
|
||||
// them cannot be seeded and is declined (declined-too-large) rather than risk an
|
||||
// unbounded dump. Because seeds are rare (~once per chain), this cap no longer
|
||||
// fires per block the way the old dump-every-block shadow did — that is the
|
||||
// declined-too-large fix. As the SOLE mainnet engine cevm would drop the cap
|
||||
// entirely and own disk-backed state; here it stays a bounded correctness harness.
|
||||
const (
|
||||
maxShadowAccounts = 200_000
|
||||
maxShadowStorage = 1_000_000
|
||||
@@ -91,6 +107,14 @@ var (
|
||||
cevmErrored uint64 // cevm process_block failed / panicked
|
||||
cevmApplied uint64 // cevm APPLIED the block: four-way gate passed, receipts returned
|
||||
cevmRevertedMismatch uint64 // agreed on root but the gated writeback/gate failed → declined to Go
|
||||
|
||||
// Resident-applier counters. The applier holds a PERSISTENT cevm StateDB per
|
||||
// chain and applies each block against it (no per-block dump). These measure
|
||||
// the declined-too-large-killer: full-state seeds should be ~1 (genesis + rare
|
||||
// resyncs), NOT N.
|
||||
cevmResidentApplied uint64 // blocks applied via the resident handle (subset of Applied)
|
||||
cevmResidentSeeds uint64 // full-state dumps+seeds into the resident StateDB (initial + resyncs)
|
||||
cevmResidentReseeds uint64 // subset of seeds that were drift RESYNCS (not the first seed)
|
||||
)
|
||||
|
||||
// ShadowStats is a snapshot of the cevm shadow-verifier / applier counters.
|
||||
@@ -107,6 +131,10 @@ type ShadowStats struct {
|
||||
Errored uint64
|
||||
Applied uint64
|
||||
RevertedMismatch uint64
|
||||
// Resident-applier metrics.
|
||||
ResidentApplied uint64
|
||||
ResidentSeeds uint64 // full-state seeds (the declined-too-large-killer number)
|
||||
ResidentReseeds uint64
|
||||
}
|
||||
|
||||
// CevmShadowStats returns the current cevm shadow-verifier counters.
|
||||
@@ -122,18 +150,25 @@ func CevmShadowStats() ShadowStats {
|
||||
Errored: atomic.LoadUint64(&cevmErrored),
|
||||
Applied: atomic.LoadUint64(&cevmApplied),
|
||||
RevertedMismatch: atomic.LoadUint64(&cevmRevertedMismatch),
|
||||
ResidentApplied: atomic.LoadUint64(&cevmResidentApplied),
|
||||
ResidentSeeds: atomic.LoadUint64(&cevmResidentSeeds),
|
||||
ResidentReseeds: atomic.LoadUint64(&cevmResidentReseeds),
|
||||
}
|
||||
}
|
||||
|
||||
// ResetCevmShadowStats zeroes the counters (test helper).
|
||||
// ResetCevmShadowStats zeroes the counters and frees any resident cevm StateDB
|
||||
// handles (test helper). Clearing the resident registry makes each test start
|
||||
// from a clean "first block seeds once" state, independent of prior tests.
|
||||
func ResetCevmShadowStats() {
|
||||
for _, p := range []*uint64{
|
||||
&cevmProcessed, &cevmAgree, &cevmDisagree, &cevmFinalizeGap,
|
||||
&cevmDeclinedTooLarge, &cevmDeclinedNoPreimage, &cevmDeclinedTx, &cevmErrored,
|
||||
&cevmApplied, &cevmRevertedMismatch,
|
||||
&cevmResidentApplied, &cevmResidentSeeds, &cevmResidentReseeds,
|
||||
} {
|
||||
atomic.StoreUint64(p, 0)
|
||||
}
|
||||
resetResidents()
|
||||
}
|
||||
|
||||
// CevmShadowEnabled reports whether the real cevm bridge is linked (-tags cevm).
|
||||
@@ -185,42 +220,193 @@ func (cevmShadowExecutor) ExecuteBlock(
|
||||
return shadowApply(config, header, txs, statedb), nil
|
||||
}
|
||||
|
||||
// shadowApply performs the read-only snapshot, cevm replay, and root
|
||||
// cross-check, updating exactly one verifier counter. When cevm's root
|
||||
// byte-matches header.Root it PROMOTES to applier via applyAgreedBlock, returning
|
||||
// cevm's receipts (block APPLIED by cevm) or nil (declined; the Go EVM applies).
|
||||
// residentState tracks the PERSISTENT cevm StateDB for one chain: its opaque
|
||||
// handle and whether that resident state is known-good at a given block height.
|
||||
// The handle is created once and reused; the resident state advances one block
|
||||
// at a time as the applier applies blocks, and is re-seeded (a full-state dump)
|
||||
// only on the first block or after a drift/decline knocks it out of sync.
|
||||
type residentState struct {
|
||||
handle uint64
|
||||
haveHandle bool // handle allocated (StateCreate called)
|
||||
everSeeded bool // at least one full-state seed has happened (first seed vs resync)
|
||||
synced bool // resident state is verified-good at `height`
|
||||
height uint64 // block number the resident state currently holds (post-block)
|
||||
}
|
||||
|
||||
var (
|
||||
residentMu sync.Mutex
|
||||
residents = map[string]*residentState{}
|
||||
)
|
||||
|
||||
// getResident returns the per-chain resident state (keyed by chain ID), creating
|
||||
// the entry and lazily its cevm handle on first use. Block execution is serial
|
||||
// per chain, so the returned *residentState is mutated without a lock by the
|
||||
// single in-flight ExecuteBlock; only the registry map is guarded here.
|
||||
func getResident(config *ethparams.ChainConfig) *residentState {
|
||||
key := "?"
|
||||
if config != nil && config.ChainID != nil {
|
||||
key = config.ChainID.String()
|
||||
}
|
||||
residentMu.Lock()
|
||||
defer residentMu.Unlock()
|
||||
rs := residents[key]
|
||||
if rs == nil {
|
||||
rs = &residentState{}
|
||||
residents[key] = rs
|
||||
}
|
||||
if !rs.haveHandle {
|
||||
rs.handle = cevmbridge.StateCreate()
|
||||
rs.haveHandle = true
|
||||
}
|
||||
return rs
|
||||
}
|
||||
|
||||
// resetResidents frees all resident cevm StateDB handles and clears the registry
|
||||
// (test helper, called by ResetCevmShadowStats).
|
||||
func resetResidents() {
|
||||
residentMu.Lock()
|
||||
defer residentMu.Unlock()
|
||||
for _, rs := range residents {
|
||||
if rs.haveHandle {
|
||||
cevmbridge.StateFree(rs.handle)
|
||||
}
|
||||
}
|
||||
residents = map[string]*residentState{}
|
||||
}
|
||||
|
||||
// shadowApply cross-checks cevm's independent keccak-MPT root against the header
|
||||
// and, on a byte-match under the four-way gate, APPLIES cevm's post-state into
|
||||
// the live Go statedb. It runs cevm against a RESIDENT StateDB held C-side per
|
||||
// chain, so state lives ACROSS blocks: the pre-block Go state is dumped and
|
||||
// seeded into cevm only ONCE per sync (the first block for a chain, or a resync
|
||||
// after a drift/decline), NOT every block. On the common in-sync path it applies
|
||||
// the block against the resident tries with NO dump — cevm's incremental commit()
|
||||
// then costs O(changed), not O(state). Every outcome updates exactly one verifier
|
||||
// counter; a wrong resident root is caught by the root check (disagree) and the
|
||||
// four-way apply gate, so a drifted resident can never commit wrong state — it
|
||||
// only forces a resync on the next block.
|
||||
func shadowApply(
|
||||
config *ethparams.ChainConfig,
|
||||
header *types.Header,
|
||||
txs types.Transactions,
|
||||
statedb *state.StateDB,
|
||||
) []*types.Receipt {
|
||||
// (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 nil
|
||||
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)
|
||||
rs := getResident(config)
|
||||
|
||||
// Executed blocks have Number >= 1, so `parent` is the height cevm's resident
|
||||
// state must already hold to apply this block without a full reseed.
|
||||
var number, parent uint64
|
||||
if header.Number != nil {
|
||||
number = header.Number.Uint64()
|
||||
if number > 0 {
|
||||
parent = number - 1
|
||||
}
|
||||
}
|
||||
|
||||
// (a) (Re)seed the resident StateDB when it is not already at the parent
|
||||
// height. This is the ONLY full-state dump — once per sync, not per block.
|
||||
// DumpToCollector iterates the trie at statedb's original (parent) root, which
|
||||
// is exactly the pre-block state the block was applied to.
|
||||
if !rs.synced || rs.height != parent {
|
||||
snap := &shadowSnapshot{maxAccts: maxShadowAccounts, maxStorage: maxShadowStorage}
|
||||
statedb.DumpToCollector(snap, &state.DumpConfig{})
|
||||
switch {
|
||||
case snap.tooLarge:
|
||||
atomic.AddUint64(&cevmDeclinedTooLarge, 1)
|
||||
rs.synced = false
|
||||
return nil
|
||||
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)
|
||||
rs.synced = false
|
||||
return nil
|
||||
}
|
||||
cevmbridge.StateSeed(rs.handle, snap.accts, snap.storage)
|
||||
atomic.AddUint64(&cevmResidentSeeds, 1)
|
||||
if rs.everSeeded {
|
||||
atomic.AddUint64(&cevmResidentReseeds, 1)
|
||||
}
|
||||
rs.everSeeded = true
|
||||
rs.synced = true
|
||||
rs.height = parent // resident now holds the parent state
|
||||
}
|
||||
|
||||
// (b) Build the cevm tx array, retaining each recovered sender for
|
||||
// ContractAddress reconstruction on CREATE receipts.
|
||||
ctxs, froms, ok := buildCevmTxs(config, header, txs)
|
||||
if !ok {
|
||||
atomic.AddUint64(&cevmDeclinedTx, 1)
|
||||
rs.synced = false // Go will apply this block; resident falls behind → resync next
|
||||
return nil
|
||||
}
|
||||
|
||||
// (b) Build the cevm tx array from the block's transactions, retaining each
|
||||
// recovered sender for ContractAddress reconstruction on CREATE receipts.
|
||||
// Pessimistically flag the resident out-of-sync BEFORE the apply: any error,
|
||||
// panic, or root mismatch below then forces a resync next block. It is
|
||||
// re-marked synced only after cevm's root is verified against the header.
|
||||
rs.synced = false
|
||||
|
||||
// (c) Apply the block against the RESIDENT StateDB (no reseed): cevm's new
|
||||
// root + per-tx results + logs + post-state delta (from the commit change-set).
|
||||
res, err := cevmbridge.StateApplyBlock(rs.handle, ctxs, blockCtx(config, header))
|
||||
if err != nil || !res.OK {
|
||||
atomic.AddUint64(&cevmErrored, 1)
|
||||
return nil
|
||||
}
|
||||
atomic.AddUint64(&cevmProcessed, 1)
|
||||
|
||||
// (d) Cross-check cevm's resident root against the root the Go EVM committed.
|
||||
if common.BytesToHash(res.StateRoot[:]) != header.Root {
|
||||
// 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)
|
||||
} else {
|
||||
atomic.AddUint64(&cevmDisagree, 1)
|
||||
}
|
||||
// cevm's resident state advanced to a NON-matching state → stays unsynced,
|
||||
// so the next block re-seeds from the (correct) Go state.
|
||||
return nil
|
||||
}
|
||||
atomic.AddUint64(&cevmAgree, 1)
|
||||
|
||||
// Roots agree: promote to APPLIER under the four-way safety gate. On any
|
||||
// mismatch the gate declines with the live statedb untouched (Go EVM applies).
|
||||
receipts := applyAgreedBlock(config, header, txs, froms, statedb, res)
|
||||
if receipts == nil {
|
||||
atomic.AddUint64(&cevmRevertedMismatch, 1)
|
||||
// The root agreed, so cevm's resident state IS correct at this height; the
|
||||
// decline was only a receipt-reconstruction gap and the Go EVM reaches the
|
||||
// identical state, so the resident stays in sync.
|
||||
rs.synced = true
|
||||
rs.height = number
|
||||
return nil
|
||||
}
|
||||
atomic.AddUint64(&cevmApplied, 1)
|
||||
atomic.AddUint64(&cevmResidentApplied, 1)
|
||||
rs.synced = true
|
||||
rs.height = number // resident verified at height N
|
||||
return receipts
|
||||
}
|
||||
|
||||
// buildCevmTxs converts the block's transactions into the cevm bridge tx array,
|
||||
// returning the recovered senders (for CREATE-receipt ContractAddress
|
||||
// reconstruction) and ok=false if any sender is unrecoverable.
|
||||
func buildCevmTxs(
|
||||
config *ethparams.ChainConfig,
|
||||
header *types.Header,
|
||||
txs types.Transactions,
|
||||
) (ctxs []cevmbridge.Tx, froms []common.Address, ok bool) {
|
||||
signer := types.MakeSigner(config, header.Number, header.Time)
|
||||
ctxs := make([]cevmbridge.Tx, len(txs))
|
||||
froms := make([]common.Address, len(txs))
|
||||
ctxs = make([]cevmbridge.Tx, len(txs))
|
||||
froms = make([]common.Address, len(txs))
|
||||
for i, tx := range txs {
|
||||
from, err := types.Sender(signer, tx)
|
||||
if err != nil {
|
||||
atomic.AddUint64(&cevmDeclinedTx, 1)
|
||||
return nil
|
||||
return nil, nil, false
|
||||
}
|
||||
froms[i] = from
|
||||
var t cevmbridge.Tx
|
||||
@@ -255,41 +441,7 @@ func shadowApply(
|
||||
}
|
||||
ctxs[i] = t
|
||||
}
|
||||
|
||||
// (c) cevm's independent root + per-tx results + logs + post-state delta 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 nil
|
||||
}
|
||||
atomic.AddUint64(&cevmProcessed, 1)
|
||||
|
||||
// (d) Cross-check against the root the Go EVM committed for this block.
|
||||
if common.BytesToHash(res.StateRoot[:]) != header.Root {
|
||||
// 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)
|
||||
} else {
|
||||
atomic.AddUint64(&cevmDisagree, 1)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
atomic.AddUint64(&cevmAgree, 1)
|
||||
|
||||
// Roots agree: promote to APPLIER under the four-way safety gate. On any
|
||||
// mismatch the gate declines with the live statedb untouched (Go EVM applies).
|
||||
receipts := applyAgreedBlock(config, header, txs, froms, statedb, res)
|
||||
if receipts == nil {
|
||||
atomic.AddUint64(&cevmRevertedMismatch, 1)
|
||||
return nil
|
||||
}
|
||||
atomic.AddUint64(&cevmApplied, 1)
|
||||
return receipts
|
||||
return ctxs, froms, true
|
||||
}
|
||||
|
||||
// applyAgreedBlock is entered ONLY when cevm's root already byte-matches
|
||||
|
||||
@@ -21,6 +21,13 @@ package cevmbridge
|
||||
// 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).
|
||||
//
|
||||
// Two entry families share one marshalling pipeline (the marshal* / unmarshal*
|
||||
// helpers below):
|
||||
// - STATELESS: ProcessBlock -> cevm_process_block (fresh StateDB per call).
|
||||
// - RESIDENT: StateCreate/StateSeed/StateApplyBlock/StateRoot/StateFree ->
|
||||
// the cevm_state_* handle ABI (a PERSISTENT StateDB held C-side, so state
|
||||
// lives across blocks and no per-block dump crosses the FFI boundary).
|
||||
|
||||
/*
|
||||
#cgo CFLAGS: -I/Users/z/work/luxcpp/cevm/lib/evm
|
||||
@@ -41,83 +48,90 @@ 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)
|
||||
}
|
||||
}()
|
||||
// cAlloc accumulates C heap allocations (code/data/access-list payloads that
|
||||
// cgo forbids inside Go-memory structs) so they can be freed after the call.
|
||||
type cAlloc struct{ ptrs []unsafe.Pointer }
|
||||
|
||||
// --- accounts --------------------------------------------------------
|
||||
cAccts := make([]C.cevm_pb_acct, len(accts))
|
||||
func (c *cAlloc) free() {
|
||||
for _, p := range c.ptrs {
|
||||
C.free(p)
|
||||
}
|
||||
}
|
||||
|
||||
// bytes copies b into C memory tracked for free-on-return.
|
||||
func (c *cAlloc) bytes(b []byte) unsafe.Pointer {
|
||||
p := C.CBytes(b)
|
||||
c.ptrs = append(c.ptrs, p)
|
||||
return p
|
||||
}
|
||||
|
||||
// --- Go -> C marshalling (shared by the stateless + resident paths) ------
|
||||
|
||||
func marshalAccts(accts []Account, ca *cAlloc) []C.cevm_pb_acct {
|
||||
c := 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])
|
||||
c[i].address[j] = C.uint8_t(a.Address[j])
|
||||
}
|
||||
cAccts[i].nonce = C.uint64_t(a.Nonce)
|
||||
c[i].nonce = C.uint64_t(a.Nonce)
|
||||
for j := 0; j < 4; j++ {
|
||||
cAccts[i].balance[j] = C.uint64_t(a.Balance[j])
|
||||
c[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))
|
||||
c[i].code = (*C.uint8_t)(ca.bytes(a.Code))
|
||||
c[i].code_len = C.uint32_t(len(a.Code))
|
||||
}
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// --- storage ---------------------------------------------------------
|
||||
cStor := make([]C.cevm_pb_storage, len(storage))
|
||||
func marshalStorage(storage []Storage) []C.cevm_pb_storage {
|
||||
c := 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])
|
||||
c[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])
|
||||
c[i].key[j] = C.uint8_t(s.Key[j])
|
||||
c[i].value[j] = C.uint8_t(s.Value[j])
|
||||
}
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// --- transactions ----------------------------------------------------
|
||||
cTxs := make([]C.cevm_pb_tx, len(txs))
|
||||
func marshalTxs(txs []Tx, ca *cAlloc) []C.cevm_pb_tx {
|
||||
c := 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])
|
||||
c[i].sender[j] = C.uint8_t(t.Sender[j])
|
||||
c[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])
|
||||
c[i].value[j] = C.uint8_t(t.Value[j])
|
||||
c[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)
|
||||
c[i].gas_limit = C.uint64_t(t.GasLimit)
|
||||
c[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))
|
||||
c[i].data = (*C.uint8_t)(ca.bytes(t.Data))
|
||||
c[i].data_len = C.uint32_t(len(t.Data))
|
||||
}
|
||||
if t.IsCreate {
|
||||
cTxs[i].is_create = 1
|
||||
c[i].is_create = 1
|
||||
}
|
||||
|
||||
// --- EIP-2930 access list (may be nil) ---------------------------
|
||||
// The tuple array and each tuple's flattened key buffer are allocated
|
||||
// in C memory: cTxs is Go memory passed to C, and cgo forbids it from
|
||||
// in C memory: c is Go memory passed to C, and cgo forbids it from
|
||||
// containing Go pointers — so access_list / storage_keys must point at
|
||||
// C allocations (freed on return alongside the data payloads).
|
||||
if nal := len(t.AccessList); nal > 0 {
|
||||
tupBytes := C.size_t(nal) * C.size_t(unsafe.Sizeof(C.cevm_pb_access_tuple{}))
|
||||
tupMem := C.malloc(tupBytes)
|
||||
frees = append(frees, tupMem)
|
||||
ca.ptrs = append(ca.ptrs, tupMem)
|
||||
tuples := unsafe.Slice((*C.cevm_pb_access_tuple)(tupMem), nal)
|
||||
for j := range t.AccessList {
|
||||
at := &t.AccessList[j]
|
||||
@@ -131,56 +145,39 @@ func ProcessBlock(accts []Account, storage []Storage, txs []Tx, ctx BlockCtx) (R
|
||||
for k := range at.StorageKeys {
|
||||
copy(flat[k*32:(k+1)*32], at.StorageKeys[k][:])
|
||||
}
|
||||
kp := C.CBytes(flat)
|
||||
frees = append(frees, kp)
|
||||
tuples[j].storage_keys = (*C.uint8_t)(kp)
|
||||
tuples[j].storage_keys = (*C.uint8_t)(ca.bytes(flat))
|
||||
tuples[j].n_storage_keys = C.uint32_t(nk)
|
||||
}
|
||||
}
|
||||
cTxs[i].access_list = (*C.cevm_pb_access_tuple)(tupMem)
|
||||
cTxs[i].n_access_list = C.uint32_t(nal)
|
||||
c[i].access_list = (*C.cevm_pb_access_tuple)(tupMem)
|
||||
c[i].n_access_list = C.uint32_t(nal)
|
||||
}
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// --- context ---------------------------------------------------------
|
||||
var cCtx C.cevm_pb_ctx
|
||||
func marshalCtx(ctx BlockCtx) C.cevm_pb_ctx {
|
||||
var c C.cevm_pb_ctx
|
||||
for j := 0; j < 20; j++ {
|
||||
cCtx.coinbase[j] = C.uint8_t(ctx.Coinbase[j])
|
||||
c.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)
|
||||
c.block_number = C.int64_t(ctx.BlockNumber)
|
||||
c.block_timestamp = C.int64_t(ctx.BlockTime)
|
||||
c.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])
|
||||
c.chain_id[j] = C.uint8_t(ctx.ChainID[j])
|
||||
c.block_base_fee[j] = C.uint8_t(ctx.BaseFee[j])
|
||||
c.prev_randao[j] = C.uint8_t(ctx.PrevRandao[j])
|
||||
c.blob_base_fee[j] = C.uint8_t(ctx.BlobBaseFee[j])
|
||||
}
|
||||
cCtx.revision = C.uint8_t(ctx.Revision)
|
||||
c.revision = C.uint8_t(ctx.Revision)
|
||||
return c
|
||||
}
|
||||
|
||||
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)
|
||||
// --- C -> Go unmarshalling (shared) --------------------------------------
|
||||
// Reads the result's heap arrays; the CALLER owns freeing r via C.cevm_pb_free.
|
||||
|
||||
func unmarshalResult(r C.cevm_pb_result) (Result, error) {
|
||||
var res Result
|
||||
res.ABIVersion = uint32(r.abi_version)
|
||||
res.OK = r.ok != 0
|
||||
@@ -189,7 +186,7 @@ func ProcessBlock(accts []Account, storage []Storage, txs []Tx, ctx BlockCtx) (R
|
||||
res.StateRoot[j] = byte(r.state_root[j])
|
||||
}
|
||||
if r.ok == 0 {
|
||||
return res, fmt.Errorf("cevmbridge: cevm_process_block returned ok=0")
|
||||
return res, fmt.Errorf("cevmbridge: cevm 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)
|
||||
@@ -286,3 +283,107 @@ func ProcessBlock(accts []Account, storage []Storage, txs []Tx, ctx BlockCtx) (R
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// ProcessBlock (STATELESS) seeds a FRESH 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 + logs + post-state delta. It is a pure function: it does not
|
||||
// touch any Go state, and rebuilds the whole StateDB from the seed each call.
|
||||
func ProcessBlock(accts []Account, storage []Storage, txs []Tx, ctx BlockCtx) (Result, error) {
|
||||
var ca cAlloc
|
||||
defer ca.free()
|
||||
|
||||
cAccts := marshalAccts(accts, &ca)
|
||||
cStor := marshalStorage(storage)
|
||||
cTxs := marshalTxs(txs, &ca)
|
||||
cCtx := marshalCtx(ctx)
|
||||
|
||||
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)
|
||||
return unmarshalResult(r)
|
||||
}
|
||||
|
||||
// --- Resident (stateful) handle API --------------------------------------
|
||||
|
||||
// StateCreate allocates a resident cevm StateDB and returns its opaque non-zero
|
||||
// handle. Free it with StateFree when the chain is torn down.
|
||||
func StateCreate() uint64 { return uint64(C.cevm_state_create()) }
|
||||
|
||||
// StateSeed (re)seeds the resident StateDB behind handle h from accts+storage and
|
||||
// commits to establish its base root. A reseed FULLY RESETS the handle's state —
|
||||
// this is the one full-state dump per sync (genesis or a drift resync).
|
||||
func StateSeed(h uint64, accts []Account, storage []Storage) {
|
||||
var ca cAlloc
|
||||
defer ca.free()
|
||||
|
||||
cAccts := marshalAccts(accts, &ca)
|
||||
cStor := marshalStorage(storage)
|
||||
|
||||
var (
|
||||
acctPtr *C.cevm_pb_acct
|
||||
storPtr *C.cevm_pb_storage
|
||||
)
|
||||
if len(cAccts) > 0 {
|
||||
acctPtr = &cAccts[0]
|
||||
}
|
||||
if len(cStor) > 0 {
|
||||
storPtr = &cStor[0]
|
||||
}
|
||||
C.cevm_state_seed(C.uint64_t(h),
|
||||
acctPtr, C.uint32_t(len(cAccts)),
|
||||
storPtr, C.uint32_t(len(cStor)))
|
||||
}
|
||||
|
||||
// StateApplyBlock replays txs against the RESIDENT StateDB behind h (NO reseed),
|
||||
// returning the new MPT root + per-tx + logs + post-state delta. The resident
|
||||
// tries make commit() incremental, so this is O(changed), not O(state), and no
|
||||
// full-state dump crosses the FFI boundary.
|
||||
func StateApplyBlock(h uint64, txs []Tx, ctx BlockCtx) (Result, error) {
|
||||
var ca cAlloc
|
||||
defer ca.free()
|
||||
|
||||
cTxs := marshalTxs(txs, &ca)
|
||||
cCtx := marshalCtx(ctx)
|
||||
|
||||
var txPtr *C.cevm_pb_tx
|
||||
if len(cTxs) > 0 {
|
||||
txPtr = &cTxs[0]
|
||||
}
|
||||
r := C.cevm_state_apply_block(C.uint64_t(h), txPtr, C.uint32_t(len(cTxs)), &cCtx)
|
||||
defer C.cevm_pb_free(&r)
|
||||
return unmarshalResult(r)
|
||||
}
|
||||
|
||||
// StateRoot returns the current resident MPT root behind h (all-zero on an
|
||||
// unknown handle). Used for sync checks.
|
||||
func StateRoot(h uint64) [32]byte {
|
||||
var buf [32]C.uint8_t
|
||||
C.cevm_state_root(C.uint64_t(h), &buf[0])
|
||||
var root [32]byte
|
||||
for j := 0; j < 32; j++ {
|
||||
root[j] = byte(buf[j])
|
||||
}
|
||||
return root
|
||||
}
|
||||
|
||||
// StateFree releases the resident StateDB behind h (no-op on an unknown handle).
|
||||
func StateFree(h uint64) { C.cevm_state_free(C.uint64_t(h)) }
|
||||
|
||||
@@ -26,50 +26,59 @@ func rcpt(i uint32) [20]byte {
|
||||
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"},
|
||||
// goldenTransferBlock builds the exact golden N-transfer block the C driver
|
||||
// (cevm-cabi-block-parity) proves byte-identical to luxfi/geth: N funded senders
|
||||
// each doing one value=1 transfer to a distinct fresh recipient, 21000 gas, zero
|
||||
// gas price, Shanghai. Shared by the stateless and resident golden tests so both
|
||||
// drive byte-identical inputs.
|
||||
func goldenTransferBlock(n uint32) ([]Account, []Tx, BlockCtx) {
|
||||
accts := make([]Account, n)
|
||||
txs := make([]Tx, n)
|
||||
for i := uint32(0); i < 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
|
||||
return accts, txs, ctx
|
||||
}
|
||||
|
||||
// ABI v2 adds the post-state delta export (out_accts / out_storage) used by
|
||||
// the applier; the golden roots below are unchanged across v1→v2.
|
||||
// goldenRoots are the geth-golden state roots for goldenTransferBlock(n). ABI v2
|
||||
// adds the post-state delta export; the roots are unchanged across v1→v2.
|
||||
var goldenRoots = []struct {
|
||||
n uint32
|
||||
golden string
|
||||
}{
|
||||
{1, "b9d849f90993abb25f57172eb086e4b6ac15369954b0c556ae2280a1559805e9"},
|
||||
{100, "2028713fd7a12c1c73fb90f7b9de08be87c1d23b76acb9d2a4cfe3788577cf0f"},
|
||||
{1000, "57b7250733ad7f7a1be82d7f1e8d385496b00d2bb93a553b6730dd83dd020099"},
|
||||
}
|
||||
|
||||
// TestProcessBlockGoldenRoots drives the golden N-transfer blocks through the GO
|
||||
// marshalling in this package via the STATELESS cevm_process_block. A match here
|
||||
// proves the Go structs serialize into the C ABI identically to the reference C
|
||||
// driver (cevm-cabi-block-parity), which is byte-identical to luxfi/geth.
|
||||
func TestProcessBlockGoldenRoots(t *testing.T) {
|
||||
if ABIVersion() != 2 {
|
||||
t.Fatalf("unexpected ABI version %d (want 2)", 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
|
||||
for _, c := range goldenRoots {
|
||||
accts, txs, ctx := goldenTransferBlock(c.n)
|
||||
|
||||
res, err := ProcessBlock(accts, nil, txs, ctx)
|
||||
if err != nil {
|
||||
@@ -82,7 +91,7 @@ func TestProcessBlockGoldenRoots(t *testing.T) {
|
||||
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)
|
||||
t.Logf("N=%-4d cevm(stateless)=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)
|
||||
@@ -95,3 +104,174 @@ func TestProcessBlockGoldenRoots(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestStateApplyBlockGoldenRoots drives the SAME golden blocks through the
|
||||
// RESIDENT handle path (StateCreate → StateSeed → StateApplyBlock): seed the
|
||||
// accounts once, apply the block against the resident StateDB, and assert cevm's
|
||||
// resident MPT root byte-matches the geth golden root — and that StateRoot(h)
|
||||
// reports the same root. This proves the stateful C-ABI produces the identical
|
||||
// root to the stateless cevm_process_block (and thus luxfi/geth), so the resident
|
||||
// applier is a byte-exact substitute for the per-block-dump path.
|
||||
func TestStateApplyBlockGoldenRoots(t *testing.T) {
|
||||
if ABIVersion() != 2 {
|
||||
t.Fatalf("unexpected ABI version %d (want 2)", ABIVersion())
|
||||
}
|
||||
|
||||
for _, c := range goldenRoots {
|
||||
accts, txs, ctx := goldenTransferBlock(c.n)
|
||||
|
||||
h := StateCreate()
|
||||
if h == 0 {
|
||||
t.Fatalf("N=%d: StateCreate returned 0 (no resident handle)", c.n)
|
||||
}
|
||||
StateSeed(h, accts, nil)
|
||||
|
||||
res, err := StateApplyBlock(h, txs, ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("N=%d: StateApplyBlock error: %v", c.n, err)
|
||||
}
|
||||
if !res.OK {
|
||||
t.Fatalf("N=%d: StateApplyBlock ok=false", c.n)
|
||||
}
|
||||
got := hex.EncodeToString(res.StateRoot[:])
|
||||
if got != c.golden {
|
||||
t.Fatalf("N=%d: resident root mismatch\n got=0x%s\nwant=0x%s", c.n, got, c.golden)
|
||||
}
|
||||
|
||||
// The resident root accessor must report the same post-apply root.
|
||||
rootAfter := StateRoot(h)
|
||||
if got2 := hex.EncodeToString(rootAfter[:]); got2 != c.golden {
|
||||
t.Fatalf("N=%d: StateRoot(h) after apply = 0x%s, want 0x%s", c.n, got2, c.golden)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
StateFree(h)
|
||||
t.Logf("N=%-4d cevm(resident)=0x%s == geth PASS", c.n, got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestStateResidentAcrossBlocksMatchesStateless proves the KEY resident property
|
||||
// at the C-ABI: applying a second block against the RESIDENT StateDB (no reseed)
|
||||
// yields the byte-identical root to a STATELESS process_block seeded explicitly
|
||||
// with block-1's post-state. i.e. the resident tries carry the post-state across
|
||||
// blocks exactly, so no per-block dump is needed for correctness.
|
||||
func TestStateResidentAcrossBlocksMatchesStateless(t *testing.T) {
|
||||
const n = 100
|
||||
accts, txsA, ctx := goldenTransferBlock(n)
|
||||
|
||||
// Block B: each of the N recipients now sends 1 wei onward to a 2nd-hop
|
||||
// address, so block B touches a disjoint-ish set and depends on block-1
|
||||
// post-state (recipient balances, sender nonces). Recipients from block A are
|
||||
// rcpt(i); their nonce after receiving is 0, so a fresh send has nonce 0.
|
||||
txsB := make([]Tx, n)
|
||||
for i := uint32(0); i < n; i++ {
|
||||
hop2 := rcpt(i)
|
||||
hop2[15] = 0x02 // distinct 2nd-hop recipient
|
||||
var value [32]byte
|
||||
value[31] = 1
|
||||
txsB[i] = Tx{Sender: rcpt(i), Recipient: hop2, Value: value, GasLimit: 21000, Nonce: 0}
|
||||
}
|
||||
ctxB := ctx
|
||||
ctxB.BlockNumber = 2
|
||||
|
||||
// --- Resident: seed once, apply A then B (no reseed between) ---
|
||||
h := StateCreate()
|
||||
if h == 0 {
|
||||
t.Fatal("StateCreate returned 0")
|
||||
}
|
||||
defer StateFree(h)
|
||||
StateSeed(h, accts, nil)
|
||||
resA, err := StateApplyBlock(h, txsA, ctx)
|
||||
if err != nil || !resA.OK {
|
||||
t.Fatalf("resident apply A: err=%v ok=%v", err, resA.OK)
|
||||
}
|
||||
resB, err := StateApplyBlock(h, txsB, ctxB)
|
||||
if err != nil || !resB.OK {
|
||||
t.Fatalf("resident apply B: err=%v ok=%v", err, resB.OK)
|
||||
}
|
||||
residentRootB := hex.EncodeToString(resB.StateRoot[:])
|
||||
|
||||
// --- Stateless reference: reconstruct block-A post-state as an explicit
|
||||
// seed, then process_block(B) fresh. This is exactly what the OLD
|
||||
// dump-every-block path did — the resident path must match it byte-for-byte.
|
||||
accts1, storage1 := applyDeltaToSeed(accts, nil, resA)
|
||||
refB, err := ProcessBlock(accts1, storage1, txsB, ctxB)
|
||||
if err != nil || !refB.OK {
|
||||
t.Fatalf("stateless ref B: err=%v ok=%v", err, refB.OK)
|
||||
}
|
||||
statelessRootB := hex.EncodeToString(refB.StateRoot[:])
|
||||
|
||||
if residentRootB != statelessRootB {
|
||||
t.Fatalf("resident-across-blocks root != stateless-with-reseed root\n resident =0x%s\n stateless=0x%s",
|
||||
residentRootB, statelessRootB)
|
||||
}
|
||||
t.Logf("PASS: resident block-2 root 0x%s == stateless(seed=block-1-poststate) root — resident tries carry post-state across blocks byte-exactly",
|
||||
residentRootB)
|
||||
}
|
||||
|
||||
// applyDeltaToSeed reconstructs the post-state of a block as a fresh
|
||||
// (accts, storage) seed pair: seed0 transformed by the result's absolute
|
||||
// post-state delta (PostAccounts / PostStorage). Mirrors the Go applier's
|
||||
// writePostState, but into the bridge seed structs.
|
||||
func applyDeltaToSeed(seed []Account, seedStorage []Storage, res Result) ([]Account, []Storage) {
|
||||
accts := map[[20]byte]Account{}
|
||||
for _, a := range seed {
|
||||
accts[a.Address] = a
|
||||
}
|
||||
storage := map[[20]byte]map[[32]byte][32]byte{}
|
||||
for _, s := range seedStorage {
|
||||
if storage[s.Address] == nil {
|
||||
storage[s.Address] = map[[32]byte][32]byte{}
|
||||
}
|
||||
storage[s.Address][s.Key] = s.Value
|
||||
}
|
||||
|
||||
for _, pa := range res.PostAccounts {
|
||||
if pa.Deleted {
|
||||
delete(accts, pa.Address)
|
||||
delete(storage, pa.Address)
|
||||
continue
|
||||
}
|
||||
a := accts[pa.Address]
|
||||
a.Address = pa.Address
|
||||
a.Nonce = pa.Nonce
|
||||
a.Balance = pa.Balance
|
||||
if pa.CodeChanged {
|
||||
a.Code = append([]byte(nil), pa.Code...)
|
||||
}
|
||||
accts[pa.Address] = a
|
||||
}
|
||||
for _, ps := range res.PostStorage {
|
||||
var zero [32]byte
|
||||
if ps.Value == zero {
|
||||
if storage[ps.Address] != nil {
|
||||
delete(storage[ps.Address], ps.Key)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if storage[ps.Address] == nil {
|
||||
storage[ps.Address] = map[[32]byte][32]byte{}
|
||||
}
|
||||
storage[ps.Address][ps.Key] = ps.Value
|
||||
}
|
||||
|
||||
outAccts := make([]Account, 0, len(accts))
|
||||
for _, a := range accts {
|
||||
outAccts = append(outAccts, a)
|
||||
}
|
||||
var outStorage []Storage
|
||||
for addr, slots := range storage {
|
||||
for k, v := range slots {
|
||||
outStorage = append(outStorage, Storage{Address: addr, Key: k, Value: v})
|
||||
}
|
||||
}
|
||||
return outAccts, outStorage
|
||||
}
|
||||
|
||||
@@ -18,3 +18,24 @@ func ABIVersion() uint32 { return 0 }
|
||||
func ProcessBlock(_ []Account, _ []Storage, _ []Tx, _ BlockCtx) (Result, error) {
|
||||
return Result{}, ErrDisabled
|
||||
}
|
||||
|
||||
// --- Resident (stateful) handle API — no-op stubs ------------------------
|
||||
// Handle 0 is the "no resident state" sentinel; the applier treats a zero
|
||||
// handle as never-created and simply declines every block to the Go EVM.
|
||||
|
||||
// StateCreate returns 0 (no resident StateDB in the non-cevm build).
|
||||
func StateCreate() uint64 { return 0 }
|
||||
|
||||
// StateSeed is a no-op in the non-cevm build.
|
||||
func StateSeed(_ uint64, _ []Account, _ []Storage) {}
|
||||
|
||||
// StateApplyBlock always declines with ErrDisabled in the non-cevm build.
|
||||
func StateApplyBlock(_ uint64, _ []Tx, _ BlockCtx) (Result, error) {
|
||||
return Result{}, ErrDisabled
|
||||
}
|
||||
|
||||
// StateRoot returns the zero root in the non-cevm build.
|
||||
func StateRoot(_ uint64) [32]byte { return [32]byte{} }
|
||||
|
||||
// StateFree is a no-op in the non-cevm build.
|
||||
func StateFree(_ uint64) {}
|
||||
|
||||
Reference in New Issue
Block a user