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:
2026-07-21 10:15:47 -07:00
co-authored by Hanzo Dev
parent 2d93929048
commit 9faa90f10d
5 changed files with 787 additions and 185 deletions
+148
View File
@@ -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
View File
@@ -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
+180 -79
View File
@@ -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)) }
+219 -39
View File
@@ -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
}
+21
View File
@@ -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) {}