mirror of
https://github.com/luxfi/evm.git
synced 2026-09-24 00:27:44 +00:00
core/parallel: promote cevm from shadow-verifier to GATED APPLIER
cevm now ACTUALLY EXECUTES blocks (applies post-state) in luxfi/evm's block
executor, not just shadow-verifies the root — safely gated so it can never
diverge consensus.
Bridge (cevmbridge): ProcessBlock now surfaces the ABI-v2 post-state DELTA and
logs the C-ABI already exports — PostAccounts (address, absolute nonce/balance/
code, Deleted, CodeChanged), PostStorage (address, key, value), and Logs
(address, topics, data, tx_index) — alongside the root + per-tx results.
Executor (cevm_shadow.go): on the AGREE path (cevm root == header.Root) the
executor PROMOTES to applier under an APPLY-THEN-VERIFY-ELSE-DECLINE gate that
is consensus-safe BY CONSTRUCTION:
- reconstruct receipts from cevm's per-tx {status, cumulative gas, gasUsed,
logs} + txs (Type, TxHash, ContractAddress via crypto.CreateAddress for
CREATE, per-receipt Bloom) mirroring core/state_processor.go applyTransaction;
- replay cevm's post-state delta onto an ISOLATED statedb.Copy() and compute
its IntermediateRoot(IsEIP158);
- keep the writeback ONLY IF all FOUR quantities block_validator.ValidateState
checks byte-match the header: state root, aggregate bloom, receipt-trie hash
(DeriveSha), and cumulative gas. Then replay the identical delta onto the
LIVE statedb and return the receipts — the Go EVM does NOT re-run the block;
- on ANY mismatch, or an unrepresentable block (blob tx, cevm-rejected tx,
tx-result count skew, missing preimage, too-large, disagree, finalize-gap),
return (nil, nil) with the LIVE statedb UNTOUCHED so the Go EVM applies.
The trial runs on a deep Copy() rather than Snapshot()/RevertToSnapshot()
because IntermediateRoot -> Finalise -> clearJournalAndRefund invalidates the
journal, making a post-IntermediateRoot revert unreliable; verifying on a copy
leaves the live statedb pristine on every decline (nothing to revert). A wrong
or incomplete delta can therefore never reach the live state.
New honest counters: Applied (four-way gate passed, cevm EXECUTED the block) and
RevertedMismatch (agreed on root but the gated writeback/gate failed → declined
to Go). Invariant: Applied + RevertedMismatch == Agree.
Proven (-tags cevm, build-mpt CPU): the e2e test drives 256 real InsertChain
blocks (768 transfers) through Process -> ExecuteBlock and reports
Applied:256 RevertedMismatch:0 Disagree:0 — cevm executed every block in the
proven safe subset and InsertChain succeeded (ValidateState's four-way check
passed on the written-back state). Whole ./core suite stays GREEN: 9794 blocks
observed, all safely declining (preimages off => cannot seed cevm) and falling
through to Go, no regression. ./core/parallel suite green.
Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
+17
-9
@@ -14,13 +14,16 @@ import (
|
||||
)
|
||||
|
||||
// TestMain (cevm build) runs the full ./core suite and then prints the aggregate
|
||||
// cevm shadow-verifier counters. Because core/state_processor.go routes EVERY
|
||||
// block through parallel.DefaultExecutor().ExecuteBlock — which under -tags cevm
|
||||
// is the cevmShadowExecutor — these counters are the honest, whole-suite tally
|
||||
// of how many real test blocks cevm's independent real-MPT process_block root
|
||||
// byte-matched the Go EVM's header.Root. It NEVER applies state: the shadow
|
||||
// always returns (nil,nil), so the Go path applies every block and the suite
|
||||
// result is unaffected by cevm.
|
||||
// cevm shadow-verifier / applier counters. Because core/state_processor.go routes
|
||||
// EVERY block through parallel.DefaultExecutor().ExecuteBlock — which under -tags
|
||||
// cevm is the cevmShadowExecutor — these counters are the honest, whole-suite
|
||||
// tally of how many real test blocks cevm's independent real-MPT process_block
|
||||
// root byte-matched the Go EVM's header.Root, and among those how many cevm went
|
||||
// on to APPLY (four-way byte-match against the header, post-state written into the
|
||||
// Go statedb, receipts returned) versus declined back to Go. Declined blocks fall
|
||||
// through to the Go EVM with the live statedb untouched, so the suite result is
|
||||
// unaffected by cevm; applied blocks are byte-identical by construction (the gate
|
||||
// keeps a writeback only on a full state-root/bloom/receipt-hash/gas match).
|
||||
//
|
||||
// The default (!cevm) build keeps the goleak TestMain in main_test.go; this
|
||||
// variant drops goleak (orthogonal to the cevm reporting run) so the shadow
|
||||
@@ -29,11 +32,13 @@ func TestMain(m *testing.M) {
|
||||
code := m.Run()
|
||||
s := parallel.CevmShadowStats()
|
||||
total := s.Processed + s.DeclinedTooLarge + s.DeclinedNoPreimage + s.DeclinedTx + s.Errored
|
||||
fmt.Printf("\n===== CEVM SHADOW VERIFIER — whole ./core suite tally =====\n")
|
||||
fmt.Printf("\n===== CEVM SHADOW VERIFIER / APPLIER — whole ./core suite tally =====\n")
|
||||
fmt.Printf("cevm shadow enabled : %v\n", parallel.CevmShadowEnabled())
|
||||
fmt.Printf("blocks observed : %d (every block InsertChain/Process ran through ExecuteBlock)\n", total)
|
||||
fmt.Printf(" processed (cevm ran process_block) : %d\n", s.Processed)
|
||||
fmt.Printf(" agree (cevm root == Go header.Root, byte-exact) : %d\n", s.Agree)
|
||||
fmt.Printf(" applied (four-way gate passed; cevm EXECUTED the block) : %d\n", s.Applied)
|
||||
fmt.Printf(" reverted (gated writeback mismatch → declined to Go) : %d\n", s.RevertedMismatch)
|
||||
fmt.Printf(" disagree : %d\n", s.Disagree)
|
||||
fmt.Printf(" finalize-gap (withdrawals move header.Root) : %d\n", s.FinalizeGap)
|
||||
fmt.Printf(" declined-too-large (pre-state over snapshot caps) : %d\n", s.DeclinedTooLarge)
|
||||
@@ -43,6 +48,9 @@ func TestMain(m *testing.M) {
|
||||
if s.Processed > 0 {
|
||||
fmt.Printf("agreement rate over PROCESSED blocks : %d/%d\n", s.Agree, s.Processed)
|
||||
}
|
||||
fmt.Printf("===========================================================\n")
|
||||
if s.Agree > 0 {
|
||||
fmt.Printf("apply rate over AGREED blocks : %d/%d\n", s.Applied, s.Agree)
|
||||
}
|
||||
fmt.Printf("=====================================================================\n")
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
+270
-50
@@ -4,32 +4,50 @@
|
||||
package parallel
|
||||
|
||||
// cevm_shadow.go wires the Lux C++ EVM (cevm) into the block executor as a
|
||||
// byte-exact SHADOW VERIFIER, not an applier.
|
||||
// consensus-SAFE, apply-then-verify GATED APPLIER (a strict superset of the
|
||||
// original byte-exact shadow verifier).
|
||||
//
|
||||
// The map proved two facts that lock this design:
|
||||
// 1. parallel.RegisterExecutor(e) makes core/state_processor.go route EVERY
|
||||
// block through e.ExecuteBlock (no SetBackend needed).
|
||||
// 2. The CONSENSUS state root is taken from the GO statedb.IntermediateRoot()
|
||||
// (block_validator.go), NOT from cevm's returned root. So cevm as the
|
||||
// APPLIER would have to mutate the Go statedb via host callbacks (heavy,
|
||||
// deferred). As a VERIFIER it needs none of that.
|
||||
// (block_validator.go), NOT from cevm's returned root. So for cevm to be
|
||||
// the APPLIER it must write its post-execution state into the Go statedb,
|
||||
// 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, (c) cross-checks that root against
|
||||
// header.Root (the post-state root the Go EVM produced when the block was
|
||||
// built), tallying agree/disagree/finalize-gap/declined counters, and then
|
||||
// ALWAYS returns (nil, nil) so the Go sequential path is the one that actually
|
||||
// applies the block. cevm never fabricates receipts and never touches the live
|
||||
// state — the suite stays correct while cevm's real MPT is checked on every
|
||||
// block it can represent.
|
||||
// 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.
|
||||
//
|
||||
// Root-only verification sidesteps BOTH known cevm gaps: it needs no logs (the
|
||||
// receipts gap) and no state write-back (the applier gap). The proven safe
|
||||
// subset (pure value transfers + non-reverting calls into existing code) yields
|
||||
// a byte-exact match; everything else (top-level CREATE, REVERT/OOG, EIP-2930
|
||||
// access lists, EIP-3529 refunds, or a state too large to snapshot) is tallied
|
||||
// as a decline/disagree and the Go EVM's root stands unchallenged.
|
||||
// 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;
|
||||
// - keep the writeback ONLY IF all four consensus quantities byte-match the
|
||||
// header: state root, aggregate bloom, receipt-trie hash, and cumulative
|
||||
// gas. Then apply the identical delta to the LIVE statedb and return the
|
||||
// receipts — cevm has now EXECUTED the block and the Go EVM will NOT re-run
|
||||
// it. If ANY of the four mismatches (or a tx was rejected, or a blob tx is
|
||||
// present), the live statedb is NEVER touched and it returns (nil, nil) so
|
||||
// the Go EVM applies. A wrong or incomplete delta can therefore never be
|
||||
// committed — the gate is consensus-safe BY CONSTRUCTION.
|
||||
//
|
||||
// The trial writeback runs on statedb.Copy() (not the live statedb) precisely
|
||||
// because IntermediateRoot -> Finalise -> clearJournalAndRefund invalidates the
|
||||
// journal, which would make a post-hoc RevertToSnapshot unreliable. Verifying on
|
||||
// an isolated copy and only replaying onto the live statedb after a full match
|
||||
// sidesteps that entirely: on any mismatch there is nothing to revert.
|
||||
//
|
||||
// The proven safe subset (pure value transfers + non-reverting calls into
|
||||
// existing code) yields a byte-exact match and is APPLIED by cevm; everything
|
||||
// else (top-level CREATE, REVERT/OOG, EIP-158 empty-account divergence, EIP-2930
|
||||
// access lists, EIP-3529 refunds, withdrawals, or a state too large to snapshot)
|
||||
// is tallied as a decline/disagree/reverted-mismatch and the Go EVM applies it
|
||||
// unchallenged. Declined blocks fall through with the live state untouched, so
|
||||
// the suite stays correct.
|
||||
//
|
||||
// This file is intentionally NOT behind //go:build cevm: it always compiles and
|
||||
// always registers. When built without -tags cevm, cevmbridge.Enabled is false
|
||||
@@ -42,12 +60,15 @@ import (
|
||||
|
||||
"github.com/holiman/uint256"
|
||||
|
||||
"github.com/luxfi/crypto"
|
||||
"github.com/luxfi/evm/core/parallel/cevmbridge"
|
||||
"github.com/luxfi/evm/core/state"
|
||||
"github.com/luxfi/geth/common"
|
||||
"github.com/luxfi/geth/core/tracing"
|
||||
"github.com/luxfi/geth/core/types"
|
||||
"github.com/luxfi/geth/core/vm"
|
||||
ethparams "github.com/luxfi/geth/params"
|
||||
"github.com/luxfi/geth/trie"
|
||||
)
|
||||
|
||||
// Snapshot caps. A block whose pre-state exceeds these is declined (counted as
|
||||
@@ -68,9 +89,13 @@ var (
|
||||
cevmDeclinedNoPreimage uint64 // an account address was unrecoverable (preimages off) — cannot seed cevm
|
||||
cevmDeclinedTx uint64 // a tx sender could not be recovered
|
||||
cevmErrored uint64 // cevm process_block failed / panicked
|
||||
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
|
||||
)
|
||||
|
||||
// ShadowStats is a snapshot of the cevm shadow-verifier counters.
|
||||
// ShadowStats is a snapshot of the cevm shadow-verifier / applier counters.
|
||||
// Invariant: Applied + RevertedMismatch == Agree — every agreeing block is put
|
||||
// through the four-way apply gate, which either applies it or declines to Go.
|
||||
type ShadowStats struct {
|
||||
Processed uint64
|
||||
Agree uint64
|
||||
@@ -80,6 +105,8 @@ type ShadowStats struct {
|
||||
DeclinedNoPreimage uint64
|
||||
DeclinedTx uint64
|
||||
Errored uint64
|
||||
Applied uint64
|
||||
RevertedMismatch uint64
|
||||
}
|
||||
|
||||
// CevmShadowStats returns the current cevm shadow-verifier counters.
|
||||
@@ -93,6 +120,8 @@ func CevmShadowStats() ShadowStats {
|
||||
DeclinedNoPreimage: atomic.LoadUint64(&cevmDeclinedNoPreimage),
|
||||
DeclinedTx: atomic.LoadUint64(&cevmDeclinedTx),
|
||||
Errored: atomic.LoadUint64(&cevmErrored),
|
||||
Applied: atomic.LoadUint64(&cevmApplied),
|
||||
RevertedMismatch: atomic.LoadUint64(&cevmRevertedMismatch),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,6 +130,7 @@ func ResetCevmShadowStats() {
|
||||
for _, p := range []*uint64{
|
||||
&cevmProcessed, &cevmAgree, &cevmDisagree, &cevmFinalizeGap,
|
||||
&cevmDeclinedTooLarge, &cevmDeclinedNoPreimage, &cevmDeclinedTx, &cevmErrored,
|
||||
&cevmApplied, &cevmRevertedMismatch,
|
||||
} {
|
||||
atomic.StoreUint64(p, 0)
|
||||
}
|
||||
@@ -120,42 +150,51 @@ func init() {
|
||||
}
|
||||
}
|
||||
|
||||
// cevmShadowExecutor is a BlockExecutor that only ever verifies: it returns
|
||||
// (nil, nil) unconditionally so the Go sequential path applies the block.
|
||||
// cevmShadowExecutor is a BlockExecutor that verifies cevm's independent
|
||||
// keccak-MPT root against the header and, when they byte-match under the
|
||||
// four-way gate, APPLIES cevm's post-state into the live statedb and returns its
|
||||
// receipts; on any mismatch or decline reason it returns (nil, nil) so the Go
|
||||
// EVM applies the block, leaving the live statedb untouched.
|
||||
type cevmShadowExecutor struct{}
|
||||
|
||||
// ExecuteBlock runs the cevm shadow cross-check and ALWAYS declines. It never
|
||||
// returns receipts or an error, so it can never break block processing — a
|
||||
// mismatch or an internal failure only moves a counter.
|
||||
// ExecuteBlock runs the cevm cross-check and, on a four-way byte-match, APPLIES
|
||||
// cevm's execution by writing its post-state delta into the live statedb and
|
||||
// returning the reconstructed receipts. On any mismatch, decline reason, or
|
||||
// internal failure it returns (nil, nil) with the live statedb untouched, so the
|
||||
// Go EVM applies the block — cevm can therefore never destabilise consensus.
|
||||
func (cevmShadowExecutor) ExecuteBlock(
|
||||
config *ethparams.ChainConfig,
|
||||
header *types.Header,
|
||||
txs types.Transactions,
|
||||
statedb *state.StateDB,
|
||||
_ vm.Config,
|
||||
) ([]*types.Receipt, error) {
|
||||
) (receipts []*types.Receipt, err error) {
|
||||
if !cevmbridge.Enabled || config == nil || header == nil || statedb == nil || len(txs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
// The verifier must NEVER destabilise consensus: any panic in the snapshot
|
||||
// or bridge is swallowed and counted, and we still decline.
|
||||
// A panic anywhere in the snapshot, bridge, or gate must NEVER destabilise
|
||||
// consensus: swallow it, count it, and decline. All risky work (bridge cgo,
|
||||
// trial writeback, root/receipt hashing) runs BEFORE the single live
|
||||
// writeback, so a panic always leaves the live statedb pristine.
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
atomic.AddUint64(&cevmErrored, 1)
|
||||
receipts, err = nil, nil
|
||||
}
|
||||
}()
|
||||
shadowVerify(config, header, txs, statedb)
|
||||
return nil, nil
|
||||
return shadowApply(config, header, txs, statedb), nil
|
||||
}
|
||||
|
||||
// shadowVerify performs the read-only snapshot, cevm replay, and root
|
||||
// cross-check, updating exactly one counter.
|
||||
func shadowVerify(
|
||||
// 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).
|
||||
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.
|
||||
@@ -164,23 +203,26 @@ func shadowVerify(
|
||||
switch {
|
||||
case snap.tooLarge:
|
||||
atomic.AddUint64(&cevmDeclinedTooLarge, 1)
|
||||
return
|
||||
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)
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
// (b) Build the cevm tx array from the block's transactions.
|
||||
// (b) Build the cevm tx array from the block's transactions, retaining each
|
||||
// recovered sender for ContractAddress reconstruction on CREATE receipts.
|
||||
signer := types.MakeSigner(config, header.Number, header.Time)
|
||||
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
|
||||
return nil
|
||||
}
|
||||
froms[i] = from
|
||||
var t cevmbridge.Tx
|
||||
copy(t.Sender[:], from[:])
|
||||
if to := tx.To(); to != nil {
|
||||
@@ -198,29 +240,207 @@ func shadowVerify(
|
||||
ctxs[i] = t
|
||||
}
|
||||
|
||||
// (c) cevm's independent root over the same pre-state + txs.
|
||||
// (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
|
||||
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 {
|
||||
atomic.AddUint64(&cevmAgree, 1)
|
||||
return
|
||||
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
|
||||
}
|
||||
// Known-alignment gap: cevm credits the coinbase per-tx (gas*gasPrice, which
|
||||
// matches evm's full-fee-to-coinbase state_transition) but does NOT run
|
||||
// engine.Finalize. On Lux that Finalize is a state no-op EXCEPT for
|
||||
// EIP-4895 withdrawals, which move header.Root. Attribute such mismatches to
|
||||
// the finalize gap rather than blaming cevm.
|
||||
if header.WithdrawalsHash != nil && *header.WithdrawalsHash != types.EmptyWithdrawalsHash {
|
||||
atomic.AddUint64(&cevmFinalizeGap, 1)
|
||||
return
|
||||
atomic.AddUint64(&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
|
||||
}
|
||||
|
||||
// applyAgreedBlock is entered ONLY when cevm's root already byte-matches
|
||||
// header.Root. It promotes cevm from verifier to APPLIER under the four-way
|
||||
// consensus gate — the identical quantities block_validator.ValidateState
|
||||
// checks: state root, aggregate bloom, receipt-trie hash, and cumulative gas.
|
||||
//
|
||||
// It reconstructs the receipts, replays cevm's post-state delta onto an ISOLATED
|
||||
// COPY of the statedb, and keeps the writeback ONLY IF all four byte-match the
|
||||
// header. On a full match it replays the identical delta onto the LIVE statedb
|
||||
// and returns the receipts (cevm has executed the block); on ANY mismatch — or an
|
||||
// unrepresentable block (blob tx, cevm-rejected tx, tx-result count skew) — it
|
||||
// returns nil WITHOUT touching the live statedb, so the Go EVM applies.
|
||||
//
|
||||
// The trial runs on statedb.Copy() rather than a Snapshot()/RevertToSnapshot()
|
||||
// pair because IntermediateRoot → Finalise → clearJournalAndRefund invalidates
|
||||
// the journal, making a post-IntermediateRoot revert unreliable. Verifying on a
|
||||
// deep copy leaves the live statedb pristine on every decline (nothing to
|
||||
// revert) and is consensus-safe by construction: a wrong or incomplete delta can
|
||||
// never reach the live state.
|
||||
func applyAgreedBlock(
|
||||
config *ethparams.ChainConfig,
|
||||
header *types.Header,
|
||||
txs types.Transactions,
|
||||
froms []common.Address,
|
||||
statedb *state.StateDB,
|
||||
res cevmbridge.Result,
|
||||
) []*types.Receipt {
|
||||
n := len(txs)
|
||||
// Unrepresentable-block pre-checks: decline before any state work.
|
||||
if len(res.TxResults) != n {
|
||||
return nil
|
||||
}
|
||||
for i := range txs {
|
||||
if txs[i].Type() == types.BlobTxType {
|
||||
return nil // blob receipts carry BlobGasUsed/Price we do not reconstruct
|
||||
}
|
||||
if res.TxResults[i].Rejected {
|
||||
return nil // block includes a tx cevm deems non-includable
|
||||
}
|
||||
}
|
||||
|
||||
receipts := buildCevmReceipts(header, txs, froms, res)
|
||||
|
||||
// Trial writeback + verification on an isolated deep copy — the live statedb
|
||||
// is NOT touched here.
|
||||
trial := statedb.Copy()
|
||||
writePostState(trial, res)
|
||||
root := trial.IntermediateRoot(config.IsEIP158(header.Number))
|
||||
|
||||
// FOUR-WAY consensus gate. Any mismatch ⇒ decline; live statedb untouched.
|
||||
if root != header.Root {
|
||||
return nil
|
||||
}
|
||||
var rbloom types.Bloom
|
||||
for _, r := range receipts {
|
||||
b := types.CreateBloom(r)
|
||||
for k := range rbloom {
|
||||
rbloom[k] |= b[k]
|
||||
}
|
||||
}
|
||||
if rbloom != header.Bloom {
|
||||
return nil
|
||||
}
|
||||
if types.DeriveSha(types.Receipts(receipts), trie.NewStackTrie(nil)) != header.ReceiptHash {
|
||||
return nil
|
||||
}
|
||||
if receipts[n-1].CumulativeGasUsed != header.GasUsed {
|
||||
return nil
|
||||
}
|
||||
|
||||
// All four byte-match: cevm's execution reproduces the header exactly. Replay
|
||||
// the identical delta onto the LIVE statedb and hand back cevm's receipts —
|
||||
// state_processor will NOT re-run the Go EVM for this block.
|
||||
writePostState(statedb, res)
|
||||
return receipts
|
||||
}
|
||||
|
||||
// buildCevmReceipts reconstructs the block's receipts from cevm's per-tx results
|
||||
// + emitted logs, mirroring core/state_processor.go applyTransaction (post-
|
||||
// Byzantium: PostState=nil, Status from the per-tx flag, ContractAddress via
|
||||
// crypto.CreateAddress for CREATE, logs attached by tx index with a block-wide
|
||||
// running index, per-receipt Bloom). The result is gate-checked against the
|
||||
// header before any receipt is returned.
|
||||
func buildCevmReceipts(
|
||||
header *types.Header,
|
||||
txs types.Transactions,
|
||||
froms []common.Address,
|
||||
res cevmbridge.Result,
|
||||
) []*types.Receipt {
|
||||
blockHash := header.Hash()
|
||||
blockNum := header.Number
|
||||
receipts := make([]*types.Receipt, len(txs))
|
||||
var logIndex uint
|
||||
for i, tx := range txs {
|
||||
tr := res.TxResults[i]
|
||||
r := &types.Receipt{
|
||||
Type: tx.Type(),
|
||||
Status: uint64(tr.Status),
|
||||
CumulativeGasUsed: tr.CumulativeGasUsed,
|
||||
TxHash: tx.Hash(),
|
||||
GasUsed: tr.GasUsed,
|
||||
BlockHash: blockHash,
|
||||
BlockNumber: blockNum,
|
||||
TransactionIndex: uint(i),
|
||||
}
|
||||
if tx.To() == nil {
|
||||
var from crypto.Address
|
||||
copy(from[:], froms[i][:])
|
||||
created := crypto.CreateAddress(from, tx.Nonce())
|
||||
r.ContractAddress = common.BytesToAddress(created[:])
|
||||
}
|
||||
var txLogs []*types.Log
|
||||
for k := range res.Logs {
|
||||
lg := &res.Logs[k]
|
||||
if lg.TxIndex != uint32(i) {
|
||||
continue
|
||||
}
|
||||
l := &types.Log{
|
||||
Address: common.BytesToAddress(lg.Address[:]),
|
||||
Topics: make([]common.Hash, len(lg.Topics)),
|
||||
Data: append([]byte(nil), lg.Data...),
|
||||
BlockNumber: blockNum.Uint64(),
|
||||
TxHash: tx.Hash(),
|
||||
TxIndex: uint(i),
|
||||
BlockHash: blockHash,
|
||||
BlockTimestamp: header.Time,
|
||||
Index: logIndex,
|
||||
}
|
||||
for t := range lg.Topics {
|
||||
l.Topics[t] = common.BytesToHash(lg.Topics[t][:])
|
||||
}
|
||||
txLogs = append(txLogs, l)
|
||||
logIndex++
|
||||
}
|
||||
r.Logs = txLogs
|
||||
r.Bloom = types.CreateBloom(r)
|
||||
receipts[i] = r
|
||||
}
|
||||
return receipts
|
||||
}
|
||||
|
||||
// writePostState replays cevm's post-state DELTA onto sdb using ABSOLUTE setters.
|
||||
// It is the ONE writeback, applied first to an isolated Copy for verification
|
||||
// and, only after the four-way gate passes, to the live statedb — so the copy
|
||||
// and the live state receive byte-identical mutations. cevm emits ABSOLUTE post
|
||||
// values (SetNonce/SetBalance/SetCode/SetState), a Deleted flag for
|
||||
// selfdestructed accounts (Finalise drops the object and its storage under the
|
||||
// promoted deletion), and omits storage rows for deleted accounts.
|
||||
func writePostState(sdb *state.StateDB, res cevmbridge.Result) {
|
||||
for i := range res.PostAccounts {
|
||||
pa := &res.PostAccounts[i]
|
||||
addr := common.BytesToAddress(pa.Address[:])
|
||||
if pa.Deleted {
|
||||
sdb.SelfDestruct(addr)
|
||||
continue
|
||||
}
|
||||
sdb.SetNonce(addr, pa.Nonce, tracing.NonceChangeUnspecified)
|
||||
sdb.SetBalance(addr, &uint256.Int{pa.Balance[0], pa.Balance[1], pa.Balance[2], pa.Balance[3]}, tracing.BalanceChangeUnspecified)
|
||||
if pa.CodeChanged {
|
||||
sdb.SetCode(addr, pa.Code, tracing.CodeChangeUnspecified)
|
||||
}
|
||||
}
|
||||
for i := range res.PostStorage {
|
||||
ps := &res.PostStorage[i]
|
||||
sdb.SetState(common.BytesToAddress(ps.Address[:]), common.BytesToHash(ps.Key[:]), common.BytesToHash(ps.Value[:]))
|
||||
}
|
||||
atomic.AddUint64(&cevmDisagree, 1)
|
||||
}
|
||||
|
||||
// effectiveGasPrice mirrors evm/core/state_transition.go: the price used for the
|
||||
|
||||
@@ -173,5 +173,84 @@ func ProcessBlock(accts []Account, storage []Storage, txs []Tx, ctx BlockCtx) (R
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- logs (ABI v2) ---------------------------------------------------
|
||||
// Marshal the emitted logs so the applier can rebuild receipt bloom +
|
||||
// receipt-trie hash (statedb.GetLogs is empty when cevm — not the Go EVM —
|
||||
// executed). Topics are flat n_topics*32 big-endian.
|
||||
if n := int(r.n_logs); n > 0 && r.logs != nil {
|
||||
clogs := unsafe.Slice((*C.cevm_pb_log)(unsafe.Pointer(r.logs)), n)
|
||||
res.Logs = make([]Log, n)
|
||||
for i := 0; i < n; i++ {
|
||||
lg := Log{TxIndex: uint32(clogs[i].tx_index)}
|
||||
for j := 0; j < 20; j++ {
|
||||
lg.Address[j] = byte(clogs[i].address[j])
|
||||
}
|
||||
if nt := int(clogs[i].n_topics); nt > 0 && clogs[i].topics != nil {
|
||||
tb := unsafe.Slice((*C.uint8_t)(unsafe.Pointer(clogs[i].topics)), nt*32)
|
||||
lg.Topics = make([][32]byte, nt)
|
||||
for t := 0; t < nt; t++ {
|
||||
for b := 0; b < 32; b++ {
|
||||
lg.Topics[t][b] = byte(tb[t*32+b])
|
||||
}
|
||||
}
|
||||
}
|
||||
if dl := int(clogs[i].data_len); dl > 0 && clogs[i].data != nil {
|
||||
db := unsafe.Slice((*C.uint8_t)(unsafe.Pointer(clogs[i].data)), dl)
|
||||
lg.Data = make([]byte, dl)
|
||||
for b := 0; b < dl; b++ {
|
||||
lg.Data[b] = byte(db[b])
|
||||
}
|
||||
}
|
||||
res.Logs[i] = lg
|
||||
}
|
||||
}
|
||||
|
||||
// --- post-state account deltas (ABI v2) ------------------------------
|
||||
if n := int(r.n_out_accts); n > 0 && r.out_accts != nil {
|
||||
ca := unsafe.Slice((*C.cevm_pb_out_account)(unsafe.Pointer(r.out_accts)), n)
|
||||
res.PostAccounts = make([]PostAccount, n)
|
||||
for i := 0; i < n; i++ {
|
||||
pa := PostAccount{
|
||||
Nonce: uint64(ca[i].nonce),
|
||||
Deleted: ca[i].deleted != 0,
|
||||
CodeChanged: ca[i].code_changed != 0,
|
||||
}
|
||||
for j := 0; j < 20; j++ {
|
||||
pa.Address[j] = byte(ca[i].address[j])
|
||||
}
|
||||
for j := 0; j < 4; j++ {
|
||||
pa.Balance[j] = uint64(ca[i].balance[j])
|
||||
}
|
||||
for j := 0; j < 32; j++ {
|
||||
pa.CodeHash[j] = byte(ca[i].code_hash[j])
|
||||
}
|
||||
if cl := int(ca[i].code_len); cl > 0 && ca[i].code != nil {
|
||||
cb := unsafe.Slice((*C.uint8_t)(unsafe.Pointer(ca[i].code)), cl)
|
||||
pa.Code = make([]byte, cl)
|
||||
for b := 0; b < cl; b++ {
|
||||
pa.Code[b] = byte(cb[b])
|
||||
}
|
||||
}
|
||||
res.PostAccounts[i] = pa
|
||||
}
|
||||
}
|
||||
|
||||
// --- post-state storage deltas (ABI v2) ------------------------------
|
||||
if n := int(r.n_out_storage); n > 0 && r.out_storage != nil {
|
||||
cs := unsafe.Slice((*C.cevm_pb_out_storage)(unsafe.Pointer(r.out_storage)), n)
|
||||
res.PostStorage = make([]PostStorage, n)
|
||||
for i := 0; i < n; i++ {
|
||||
var ps PostStorage
|
||||
for j := 0; j < 20; j++ {
|
||||
ps.Address[j] = byte(cs[i].address[j])
|
||||
}
|
||||
for j := 0; j < 32; j++ {
|
||||
ps.Key[j] = byte(cs[i].key[j])
|
||||
ps.Value[j] = byte(cs[i].value[j])
|
||||
}
|
||||
res.PostStorage[i] = ps
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
@@ -86,13 +86,55 @@ type TxResult struct {
|
||||
NLogs uint32 // logs this tx emitted (0 => within the proven safe subset)
|
||||
}
|
||||
|
||||
// Result is the whole-block outcome.
|
||||
// Log is one EVM log (LOG0..LOG4) cevm emitted. The three fields are the
|
||||
// consensus fields (Address, Topics, Data); TxIndex identifies the emitting tx
|
||||
// so the applier can attach the log to the right receipt. Logs arrive in tx
|
||||
// order (the flat index is the block-wide log index).
|
||||
type Log struct {
|
||||
Address [20]byte
|
||||
Topics [][32]byte // each 32-byte big-endian topic
|
||||
Data []byte
|
||||
TxIndex uint32
|
||||
}
|
||||
|
||||
// PostAccount is one post-state account delta (DIFF final-vs-seed): an account
|
||||
// the block CREATED or whose nonce/balance/code CHANGED, or (Deleted) a seed
|
||||
// account the block REMOVED (selfdestruct). Values are ABSOLUTE post values —
|
||||
// the applier writes them with SetNonce/SetBalance/SetCode. Code is present
|
||||
// only when CodeChanged; otherwise the applier keeps the seed code.
|
||||
type PostAccount struct {
|
||||
Address [20]byte
|
||||
Nonce uint64
|
||||
Balance [4]uint64 // little-endian limbs, [0] = low 64 bits (absolute)
|
||||
CodeHash [32]byte // final keccak256(code)
|
||||
Code []byte // non-nil iff CodeChanged
|
||||
Deleted bool // true => remove the account (selfdestruct)
|
||||
CodeChanged bool // true => Code differs from seed; apply SetCode
|
||||
}
|
||||
|
||||
// PostStorage is one post-state storage delta: a slot whose POST value differs
|
||||
// from the seed. Value all-zero encodes a CLEARED slot (SetState(key,0)). Slots
|
||||
// of a selfdestructed account are covered by its PostAccount.Deleted and are not
|
||||
// listed here.
|
||||
type PostStorage struct {
|
||||
Address [20]byte
|
||||
Key [32]byte // big-endian slot key
|
||||
Value [32]byte // big-endian post value (all-zero => cleared)
|
||||
}
|
||||
|
||||
// Result is the whole-block outcome. StateRoot + TxResults + TotalGas are what a
|
||||
// shadow VERIFIER needs; Logs + PostAccounts + PostStorage are the additional
|
||||
// ABI-v2 export an APPLIER needs to write cevm's post-state into the Go StateDB
|
||||
// and rebuild the receipts.
|
||||
type Result struct {
|
||||
StateRoot [32]byte // REAL keccak Merkle-Patricia-Trie root
|
||||
TxResults []TxResult // length == len(txs) on success
|
||||
TotalGas uint64
|
||||
OK bool
|
||||
ABIVersion uint32
|
||||
StateRoot [32]byte // REAL keccak Merkle-Patricia-Trie root
|
||||
TxResults []TxResult // length == len(txs) on success
|
||||
Logs []Log // all txs' logs, in tx order (nil if none)
|
||||
PostAccounts []PostAccount // post-state account deltas (nil if none)
|
||||
PostStorage []PostStorage // post-state storage deltas (nil if none)
|
||||
TotalGas uint64
|
||||
OK bool
|
||||
ABIVersion uint32
|
||||
}
|
||||
|
||||
// evmc_revision values (mirror go_process_block.h CEVM_PB_REV_*).
|
||||
|
||||
Reference in New Issue
Block a user