From e48b5e54e67f4fccb3c264c30fbcf088d9cf1a01 Mon Sep 17 00:00:00 2001 From: Giulio Rebuffo Date: Fri, 8 May 2026 16:25:07 +0200 Subject: [PATCH 1/2] execution/vm: add --use-gevm flag to swap interpreter to GEVM MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an opt-in `--use-gevm` flag on both `cmd/erigon` and `cmd/integration` that selects GEVM (github.com/Giulio2002/gevm) as the EVM driving block execution. Default unset — legacy interpreter unchanged. Wiring - node/cli, node/ethconfig, node/eth/backend, cmd/utils: --use-gevm flag plumbed into ethconfig and through into the staged-sync executor's vm.Config. - cmd/integration/commands/{flags,stages}.go: --use-gevm on every stage subcommand (stage_exec is what V03/V05 measure). - execution/vm/interpreter.go: vm.Config gains a UseGevm bool. Adapter - execution/vm/gevm/ (new): BlockExecutor wrapping gevmhost.Evm. Per-block lifecycle: NewBlockExecutor in InitializeBlock; ExecuteTx per tx; FinalizeBlock once. Reads via state.NewReaderV3(domains.AsGetter(tx)) — exact same path legacy IBS uses, no adapter cache. Writes through Erigon's existing stateWriter; GEVM's journal flushes once per block at FinalizeBlock. - execution/stagedsync/exec3_gevm.go (new): per-block dispatch shim used by exec3_serial.go's executeBlockGevm path. - execution/tests/testutil/gevm.go (new): plumbs USE_GEVM=1 through the EEST + state-test harness so the GEVM path is exercised in tests. Staged-sync - execution/stagedsync/exec3_serial.go: executeBlockGevm branch — when cfg.vmConfig.UseGevm, route to BlockExecutor; otherwise legacy TxnExecutor unchanged. - execution/stagedsync/{exec3,stage_execute,stageloop}.go: force parallel=false on the GEVM path (BlockExecutor is serial-only by design, races on block-scoped state if EXEC3_PARALLEL=true). - execution/state/{rw_v3,history_reader_v3}.go: minor per-block-scoped helpers used by the adapter; legacy paths unchanged. Tests - execution/execmodule/execmoduletester/exec_module_tester.go, execution/tests/testutil/{block,state}_test_util.go: testutil reads USE_GEVM env var and propagates UseGevm into vm.Config so EEST + state-tests run on either interpreter. - execution/builder/exec.go, execution/exec/block_assembler.go, execution/tracing/calltracer/calltracer.go, execution/commitment/commitmentdb/commitment_context.go, cl/spectest/spectest/suite.go: minor signature plumbing. Module - go.mod / go.sum: require github.com/Giulio2002/gevm pinned at commit 92fa74d (companion PR https://github.com/Giulio2002/gevm/pull/2). Co-Authored-By: Claude Sonnet 4.6 --- cl/spectest/spectest/suite.go | 11 +- cmd/integration/commands/flags.go | 6 + cmd/integration/commands/stages.go | 3 +- execution/builder/exec.go | 38 +- .../commitmentdb/commitment_context.go | 2 +- execution/exec/block_assembler.go | 254 ++++++ .../execmoduletester/exec_module_tester.go | 15 +- execution/stagedsync/exec3.go | 3 + execution/stagedsync/exec3_gevm.go | 276 ++++++ execution/stagedsync/exec3_serial.go | 10 +- execution/stagedsync/stage_execute.go | 6 +- execution/stagedsync/stageloop/stageloop.go | 6 +- execution/state/history_reader_v3.go | 33 +- execution/state/rw_v3.go | 49 +- execution/tests/testutil/block_test_util.go | 20 +- execution/tests/testutil/gevm.go | 15 + execution/tests/testutil/state_test_util.go | 200 +++++ execution/tracing/calltracer/calltracer.go | 4 + execution/vm/gevm/adapter.go | 783 ++++++++++++++++++ execution/vm/gevm/adapter_test.go | 74 ++ execution/vm/interpreter.go | 1 + go.mod | 5 + go.sum | 6 + node/cli/default_flags.go | 1 + node/cli/flags.go | 9 + node/eth/backend.go | 4 +- node/ethconfig/config.go | 1 + 27 files changed, 1789 insertions(+), 46 deletions(-) create mode 100644 execution/stagedsync/exec3_gevm.go create mode 100644 execution/tests/testutil/gevm.go create mode 100644 execution/vm/gevm/adapter.go create mode 100644 execution/vm/gevm/adapter_test.go diff --git a/cl/spectest/spectest/suite.go b/cl/spectest/spectest/suite.go index d0f86ad8441..96306e57a6d 100644 --- a/cl/spectest/spectest/suite.go +++ b/cl/spectest/spectest/suite.go @@ -14,6 +14,15 @@ import ( func RunCases(t *testing.T, app Appendix, machineImpl machine.Interface, root fs.FS) { cases, err := ReadTestCases(root) require.NoError(t, err, "reading cases") + excludedForks := map[string]struct{}{ + "eip6110": {}, + "eip7441": {}, + "eip7594": {}, + "eip7732": {}, + "eip7805": {}, + "gloas": {}, + "whisk": {}, + } // prepare for gore..... type ( K1 = string @@ -40,7 +49,7 @@ func RunCases(t *testing.T, app Appendix, machineImpl machine.Interface, root fs t.Run(s, func(t *testing.T) { t.Parallel() m.Range0(func(key string, value TestCase) bool { - if value.ForkPhaseName == "whisk" || value.ForkPhaseName == "eip7594" { + if _, ok := excludedForks[value.ForkPhaseName]; ok { t.Skipf("skipping %s", value.ForkPhaseName) return true } diff --git a/cmd/integration/commands/flags.go b/cmd/integration/commands/flags.go index 84c4e61de18..70784df4831 100644 --- a/cmd/integration/commands/flags.go +++ b/cmd/integration/commands/flags.go @@ -34,6 +34,7 @@ var ( batchSizeStr string domain string reset, noCommit, squeeze, yes bool + useGevm bool bucket string datadirCli, toChaindata string migration string @@ -101,6 +102,10 @@ func withNoCommit(cmd *cobra.Command) { cmd.Flags().BoolVar(&noCommit, "no-commit", false, "run everything in 1 transaction, but doesn't commit it") } +func withUseGevm(cmd *cobra.Command) { + cmd.Flags().BoolVar(&useGevm, "use-gevm", false, "Use GEVM for block execution") +} + func withPruneTo(cmd *cobra.Command) { cmd.Flags().Uint64Var(&pruneTo, "prune.to", 0, "how much blocks unwind on each iteration") } @@ -223,6 +228,7 @@ func withStageBase(cmd *cobra.Command) { // withTraceFlags applies flags shared by exec-style tracing commands. func withTraceFlags(cmd *cobra.Command) { withNoCommit(cmd) + withUseGevm(cmd) withBatchSize(cmd) withTxTrace(cmd) withWorkers(cmd) diff --git a/cmd/integration/commands/stages.go b/cmd/integration/commands/stages.go index 736a41d34bf..a9263980013 100644 --- a/cmd/integration/commands/stages.go +++ b/cmd/integration/commands/stages.go @@ -629,7 +629,7 @@ func stageSenders(db kv.TemporalRwDB, ctx context.Context, logger log.Logger) er func stageExec(db kv.TemporalRwDB, ctx context.Context, logger log.Logger) error { if _, ok := os.LookupEnv("EXEC3_PARALLEL"); !ok { - dbg.Exec3Parallel = true // default for integration tool + dbg.Exec3Parallel = !useGevm } if chainTipMode && noCommit { return errors.New("--sync.mode.chaintip cannot work with --no-commit to be false") @@ -640,6 +640,7 @@ func stageExec(db kv.TemporalRwDB, ctx context.Context, logger log.Logger) error } _, engine, vmConfig, sync := newSync(ctx, db, nil /* miningConfig */, logger) + vmConfig.UseGevm = useGevm defer engine.Close() must(sync.SetCurrentStage(stages.Execution)) if reset { diff --git a/execution/builder/exec.go b/execution/builder/exec.go index 4ab73e407c0..32b93ee99aa 100644 --- a/execution/builder/exec.go +++ b/execution/builder/exec.go @@ -88,15 +88,6 @@ func StageBuilderExecCfg( // execBlock builds a block by executing transactions from the txpool, // then computes the state root from the accumulated domain writes. -// -// State changes flow through a single execution path: -// 1. IBS executes transactions using a NoopWriter / in-memory buffer (no per-tx writes to sd) -// 2. FinalizeBlockExecution applies the accumulated IBS changes to sd via the block assembler writer -// 3. ComputeCommitment(sd) produces the state root -// 4. sd writes are discarded when Builder.Build returns (tx is read-only, sd is never flushed) -// -// TODO: -// - resubmitAdjustCh - variable is not implemented func execBlock(ctx context0.Context, sd *execctx.SharedDomains, tx kv.TemporalTx, executionAt uint64, cfg BuilderExecCfg, execCfg stagedsync.ExecuteBlockCfg, logger log.Logger) (err error) { const logPrefix = "BuilderExec" @@ -137,9 +128,12 @@ func execBlock(ctx context0.Context, sd *execctx.SharedDomains, tx kv.TemporalTx filterWriter := state.NewWriter(filterSd.AsPutDel(filterMb), nil, txNum) filterReader := state.NewReaderV3(filterSd.AsGetter(filterMb)) - ibs := state.New(stateReader) - defer ibs.Release(false) - ibs.SetTxContext(current.Header.Number.Uint64(), -1) + var ibs *state.IntraBlockState + if !cfg.vmConfig.UseGevm { + ibs = state.New(stateReader) + defer ibs.Release(false) + ibs.SetTxContext(current.Header.Number.Uint64(), -1) + } current.PayloadId = cfg.payloadId ba := exec.NewBlockAssembler(exec.AssemblerCfg{ @@ -148,9 +142,13 @@ func execBlock(ctx context0.Context, sd *execctx.SharedDomains, tx kv.TemporalTx BlockReader: cfg.blockReader, ExperimentalBAL: execCfg.IsExperimentalBAL(), }, current) + defer ba.Release() ba.SetStateWriter(stateWriter) + if cfg.vmConfig.UseGevm { + ba.SetGevmStateReader(stateReader) + } - if ba.HasBAL() { + if ba.HasBAL() && ibs != nil { ibs.SetVersionMap(state.NewVersionMap(nil)) } @@ -163,12 +161,18 @@ func execBlock(ctx context0.Context, sd *execctx.SharedDomains, tx kv.TemporalTx return execCfg.BlockReader().Header(ctx, tx, hash, number) } - if err := ba.Initialize(ibs, tx, logger); err != nil { - return err - } - coinbase := accounts.InternAddress(cfg.builderState.BuilderConfig.Etherbase) + if cfg.vmConfig.UseGevm { + if err := ba.InitializeGevm(getHeader, coinbase); err != nil { + return err + } + } else { + if err := ba.Initialize(ibs, tx, logger); err != nil { + return err + } + } + yielded := mapset.NewSet[[32]byte]() interrupt := cfg.interrupt diff --git a/execution/commitment/commitmentdb/commitment_context.go b/execution/commitment/commitmentdb/commitment_context.go index 86dc3494a7b..55c2dcdc571 100644 --- a/execution/commitment/commitmentdb/commitment_context.go +++ b/execution/commitment/commitmentdb/commitment_context.go @@ -308,7 +308,7 @@ func (sdc *SharedDomainsCommitmentContext) ComputeCommitment(ctx context.Context if took > 0 { keysPerSec = uint64(float64(updateCount) / took.Seconds()) } - log.Debug("[commitment] processed", "block", blockNum, "txNum", txNum, "keys", common.PrettyCounter(updateCount), "keys/s", common.PrettyCounter(keysPerSec), "mode", sdc.updates.Mode(), "spent", took, "rootHash", hex.EncodeToString(rootHash)) + log.Info("[commitment] processed", "block", blockNum, "txNum", txNum, "keys", common.PrettyCounter(updateCount), "keys/s", common.PrettyCounter(keysPerSec), "mode", sdc.updates.Mode(), "spent", took, "rootHash", hex.EncodeToString(rootHash)) }() if updateCount == 0 { rootHash, err = sdc.patriciaTrie.RootHash() diff --git a/execution/exec/block_assembler.go b/execution/exec/block_assembler.go index aa7de9bbf65..0a6db1b415e 100644 --- a/execution/exec/block_assembler.go +++ b/execution/exec/block_assembler.go @@ -8,6 +8,7 @@ import ( "time" "github.com/erigontech/erigon/common" + "github.com/erigontech/erigon/common/hexutil" "github.com/erigontech/erigon/common/log/v3" "github.com/erigontech/erigon/db/kv" "github.com/erigontech/erigon/execution/chain" @@ -21,6 +22,7 @@ import ( "github.com/erigontech/erigon/execution/types/accounts" "github.com/erigontech/erigon/execution/vm" "github.com/erigontech/erigon/execution/vm/evmtypes" + gevmexec "github.com/erigontech/erigon/execution/vm/gevm" "github.com/erigontech/erigon/db/services" ) @@ -103,6 +105,11 @@ type BlockAssembler struct { balIO *state.VersionedIO stateWriter state.StateWriter // optional: if set, domain writes go here instead of NoopWriter gasUsed protocol.GasUsed // EIP-8037: cumulative per-dimension gas across AddTransactions calls + + gevmStateReader state.StateReader + gevmBlockExec *gevmexec.BlockExecutor + gevmInitialized bool + gevmFinalized bool } func (ba *BlockAssembler) CumulativeGasUsed() protocol.GasUsed { return ba.gasUsed } @@ -143,6 +150,17 @@ func (ba *BlockAssembler) BalIO() *state.VersionedIO { return ba.balIO } +func (ba *BlockAssembler) Release() { + if ba.gevmBlockExec != nil { + ba.gevmBlockExec.Release() + ba.gevmBlockExec = nil + } +} + +func (ba *BlockAssembler) SetGevmStateReader(reader state.StateReader) { + ba.gevmStateReader = reader +} + func (ba *BlockAssembler) Initialize(ibs *state.IntraBlockState, tx kv.TemporalTx, logger log.Logger) error { // Use NoopWriter for FinalizeTx during initialization. Intermediate state // writes to SharedDomains would become stale if later system calls (e.g. @@ -171,6 +189,9 @@ func (ba *BlockAssembler) AddTransactions( interrupt *atomic.Bool, logPrefix string, logger log.Logger) (types.Logs, bool, error) { + if vmConfig != nil && vmConfig.UseGevm { + return ba.addTransactionsGevm(ctx, getHeader, txns, coinbase, vmConfig, interrupt, logPrefix, logger) + } // Use len(ba.Txns) instead of ibs.TxnIndex()+1 to avoid gaps in the // BAL access index sequence. When a batch ends with a failed tx, @@ -361,7 +382,214 @@ LOOP: return coalescedLogs, done, nil } +func (ba *BlockAssembler) ensureGevmExecutor(getHeader func(hash common.Hash, number uint64) (*types.Header, error), coinbase accounts.Address) error { + if ba.gevmBlockExec == nil { + if ba.gevmStateReader == nil { + return errors.New("missing GEVM state reader for block assembly") + } + blockContext := protocol.NewEVMBlockContext(ba.Header, protocol.GetHashFn(ba.Header, getHeader), ba.cfg.Engine, coinbase, ba.cfg.ChainConfig) + ba.gevmBlockExec = gevmexec.NewBlockExecutorWithReader(ba.cfg.ChainConfig, ba.Header, blockContext, ba.gevmStateReader) + } + if ba.gevmInitialized { + return nil + } + if ba.cfg.ChainConfig.IsCancun(ba.Header.Time) { + ba.gevmBlockExec.ApplyBeaconRoot(ba.Header.ParentBeaconBlockRoot) + } + if ba.cfg.ChainConfig.IsPrague(ba.Header.Time) { + if err := ba.gevmBlockExec.StoreParentHash(ba.Header); err != nil { + return err + } + } + ba.gevmBlockExec.CommitTx() + ba.gevmInitialized = true + return nil +} + +func (ba *BlockAssembler) InitializeGevm(getHeader func(hash common.Hash, number uint64) (*types.Header, error), coinbase accounts.Address) error { + return ba.ensureGevmExecutor(getHeader, coinbase) +} + +func (ba *BlockAssembler) addTransactionsGevm( + ctx context.Context, + getHeader func(hash common.Hash, number uint64) (*types.Header, error), + txns types.Transactions, + coinbase accounts.Address, + vmConfig *vm.Config, + interrupt *atomic.Bool, + logPrefix string, + logger log.Logger, +) (types.Logs, bool, error) { + if err := ba.ensureGevmExecutor(getHeader, coinbase); err != nil { + return nil, false, err + } + + txnIdx := len(ba.Txns) + header := ba.AssembledBlock.Header + gasPool := new(protocol.GasPool).AddGas(header.GasLimit - ba.gasUsed.BlockRegular) + if header.BlobGasUsed != nil { + gasPool.AddBlobGas(ba.cfg.ChainConfig.GetMaxBlobGasPerBlock(header.Time) - *header.BlobGasUsed) + } + signer := types.MakeSigner(ba.cfg.ChainConfig, header.Number.Uint64(), header.Time) + blockContext := protocol.NewEVMBlockContext(header, protocol.GetHashFn(header, getHeader), ba.cfg.Engine, coinbase, ba.cfg.ChainConfig) + rules := blockContext.Rules(ba.cfg.ChainConfig) + gasUsed := &ba.gasUsed + + var coalescedLogs types.Logs + var stopped *time.Ticker + defer func() { + if stopped != nil { + stopped.Stop() + } + }() + + done := false + for _, txn := range txns { + if stopped != nil { + select { + case <-stopped.C: + done = true + return coalescedLogs, done, nil + default: + } + } + if err := common.Stopped(ctx.Done()); err != nil { + return nil, true, err + } + if interrupt != nil && interrupt.Load() && stopped == nil { + logger.Debug("Transaction adding was requested to stop", "payload", ba.PayloadId) + stopped = time.NewTicker(500 * time.Millisecond) + } + if gasPool.Gas() < params.TxGas { + logger.Debug(fmt.Sprintf("[%s] Not enough gas for further transactions", logPrefix), "have", gasPool, "want", params.TxGas) + done = true + break + } + + rlpSpacePostTxn := ba.AvailableRlpSpace(ba.cfg.ChainConfig, txn) + if rlpSpacePostTxn < 0 { + rlpSpacePreTxn := ba.AvailableRlpSpace(ba.cfg.ChainConfig) + logger.Debug( + fmt.Sprintf("[%s] Skipping transaction since it does not fit in available rlp space", logPrefix), + "hash", txn.Hash(), + "pre", rlpSpacePreTxn, + "post", rlpSpacePostTxn, + ) + continue + } + + from, err := txn.Sender(*signer) + if err != nil { + logger.Warn(fmt.Sprintf("[%s] Could not recover transaction sender", logPrefix), "hash", txn.Hash(), "err", err) + continue + } + if txn.Protected() && !ba.cfg.ChainConfig.IsSpuriousDragon(header.Number.Uint64()) { + logger.Debug(fmt.Sprintf("[%s] Ignoring replay protected transaction", logPrefix), "hash", txn.Hash(), "eip155", ba.cfg.ChainConfig.SpuriousDragonBlock) + continue + } + if txn.GetGasLimit() > gasPool.Gas() { + logger.Debug(fmt.Sprintf("[%s] Gas limit exceeded for env block", logPrefix), "hash", txn.Hash(), "sender", from) + continue + } + if err := gasPool.SubBlobGas(txn.GetBlobGas()); err != nil { + logger.Debug(fmt.Sprintf("[%s] Blob gas limit exceeded for env block", logPrefix), "hash", txn.Hash(), "sender", from, "err", err) + continue + } + + msg, err := txn.AsMessage(*signer, header.BaseFee, rules) + if err != nil { + gasPool.AddBlobGas(txn.GetBlobGas()) + logger.Debug(fmt.Sprintf("[%s] Skipping transaction", logPrefix), "hash", txn.Hash(), "sender", from, "err", err) + continue + } + out, err := ba.gevmBlockExec.ExecuteTx(txn, msg, nil) + if err != nil { + gasPool.AddBlobGas(txn.GetBlobGas()) + logger.Debug(fmt.Sprintf("[%s] Skipping transaction", logPrefix), "hash", txn.Hash(), "sender", from, "err", err) + continue + } + if out.ValidationError { + gasPool.AddBlobGas(txn.GetBlobGas()) + logger.Debug(fmt.Sprintf("[%s] Skipping transaction", logPrefix), "hash", txn.Hash(), "sender", from, "err", out.Result.Err) + continue + } + if gasUsed.BlockRegular+out.Result.BlockRegularGasUsed > header.GasLimit { + gasPool.AddBlobGas(txn.GetBlobGas()) + logger.Debug(fmt.Sprintf("[%s] Gas limit exceeded for env block", logPrefix), "hash", txn.Hash(), "sender", from) + continue + } + if err := gasPool.SubGas(out.Result.BlockRegularGasUsed); err != nil { + gasPool.AddBlobGas(txn.GetBlobGas()) + logger.Debug(fmt.Sprintf("[%s] Gas limit exceeded for env block", logPrefix), "hash", txn.Hash(), "sender", from, "err", err) + continue + } + + gasUsed.Receipt += out.Result.ReceiptGasUsed + gasUsed.BlockRegular += out.Result.BlockRegularGasUsed + gasUsed.BlockState += out.Result.BlockStateGasUsed + gasUsed.Blob += txn.GetBlobGas() + protocol.SetGasUsed(header, gasUsed) + + receipt, err := makeGevmReceipt(header, txn, msg, txnIdx, gasUsed.Receipt, out.Result, out.Logs, ba.Receipts) + if err != nil { + return nil, false, err + } + ba.AddTxn(txn) + ba.Receipts = append(ba.Receipts, receipt) + logger.Trace(fmt.Sprintf("[%s] Added transaction", logPrefix), "hash", txn.Hash(), "sender", from, "nonce", txn.GetNonce(), "payload", ba.PayloadId) + coalescedLogs = append(coalescedLogs, receipt.Logs...) + txnIdx++ + } + + return coalescedLogs, done, nil +} + +func makeGevmReceipt(header *types.Header, txn types.Transaction, msg *types.Message, txIndex int, cumulativeGasUsed uint64, result evmtypes.ExecutionResult, logs types.Logs, previous types.Receipts) (*types.Receipt, error) { + var firstLogIndex uint32 + if len(previous) > 0 { + last := previous[len(previous)-1] + firstLogIndex = last.FirstLogIndexWithinBlock + uint32(len(last.Logs)) + } + logIndex := firstLogIndex + for _, l := range logs { + l.Index = hexutil.Uint(logIndex) + logIndex++ + } + receipt := &types.Receipt{ + BlockNumber: &header.Number, + BlockHash: header.Hash(), + TransactionIndex: uint(txIndex), + Type: txn.Type(), + GasUsed: result.ReceiptGasUsed, + CumulativeGasUsed: cumulativeGasUsed, + TxHash: txn.Hash(), + Logs: logs, + FirstLogIndexWithinBlock: firstLogIndex, + } + for _, l := range receipt.Logs { + l.TxHash = receipt.TxHash + l.BlockNumber = hexutil.Uint64(header.Number.Uint64()) + l.BlockHash = receipt.BlockHash + } + if result.Failed() { + receipt.Status = types.ReceiptStatusFailed + } else { + receipt.Status = types.ReceiptStatusSuccessful + } + if txn.Type() == types.BlobTxType { + receipt.BlobGasUsed = txn.GetBlobGas() + } + if msg.To().IsNil() { + receipt.ContractAddress = types.CreateAddress(msg.From().Value(), txn.GetNonce()) + } + receipt.Bloom = types.CreateBloom(types.Receipts{receipt}) + return receipt, nil +} + func (ba *BlockAssembler) AssembleBlock(stateReader state.StateReader, ibs *state.IntraBlockState, tx kv.TemporalTx, logger log.Logger) (block *types.Block, err error) { + if ba.gevmBlockExec != nil { + return ba.assembleBlockGevm(logger) + } chainReader := NewChainReader(ba.cfg.ChainConfig, tx, ba.cfg.BlockReader, logger) if err := ba.cfg.Engine.Prepare(chainReader, ba.Header, ibs); err != nil { @@ -404,3 +632,29 @@ func (ba *BlockAssembler) AssembleBlock(stateReader state.StateReader, ibs *stat return block, nil } + +func (ba *BlockAssembler) assembleBlockGevm(logger log.Logger) (*types.Block, error) { + if ba.gevmFinalized { + return types.NewBlockForAsembling(ba.Header, ba.Txns, ba.Uncles, ba.Receipts, ba.Withdrawals), nil + } + requests, err := ba.gevmBlockExec.FinalizeBlock(ba.cfg.Engine, ba.cfg.ChainConfig, ba.Header, ba.Uncles, ba.Receipts, ba.Withdrawals) + if err != nil { + return nil, fmt.Errorf("cannot finalize GEVM block execution: %s", err) + } + ba.Requests = requests + if err := ba.gevmBlockExec.ApplyState(ba.writer()); err != nil { + return nil, err + } + ba.gevmFinalized = true + block := types.NewBlockForAsembling(ba.Header, ba.Txns, ba.Uncles, ba.Receipts, ba.Withdrawals) + header := block.HeaderNoCopy() + if ba.cfg.ChainConfig.IsPrague(header.Time) && header.RequestsHash != nil { + requestsHash := common.Hash{} + if len(ba.Requests) > 0 { + requestsHash = *ba.Requests.Hash() + } + header.RequestsHash = &requestsHash + } + logger.Trace("Assembled block with GEVM", "block", header.Number.Uint64(), "txs", len(ba.Txns)) + return block, nil +} diff --git a/execution/execmodule/execmoduletester/exec_module_tester.go b/execution/execmodule/execmoduletester/exec_module_tester.go index b77e1245700..f59b6736b96 100644 --- a/execution/execmodule/execmoduletester/exec_module_tester.go +++ b/execution/execmodule/execmoduletester/exec_module_tester.go @@ -343,6 +343,12 @@ func WithFcuBackgroundPrune() Option { } } +func WithUseGevm() Option { + return func(opts *options) { + opts.useGevm = true + } +} + type options struct { stepSize *uint64 experimentalBAL bool @@ -356,6 +362,7 @@ type options struct { enableDomains []kv.Domain fcuBackgroundCommit bool fcuBackgroundPrune bool + useGevm bool } func applyOptions(opts []Option) options { @@ -428,6 +435,8 @@ func New(tb testing.TB, opts ...Option) *ExecModuleTester { cfg.TxPool.Disable = !withTxPool cfg.Dirs = dirs cfg.AlwaysGenerateChangesets = true + cfg.UseGevm = opt.useGevm + vmConfig := &vm.Config{UseGevm: opt.useGevm} cfg.PersistReceiptsCacheV2 = true cfg.ChaosMonkey = false cfg.Snapshot.ChainName = gspec.Config.ChainName @@ -629,7 +638,7 @@ func New(tb testing.TB, opts ...Option) *ExecModuleTester { cfg.BatchSize, mock.ChainConfig, mock.Engine, - &vm.Config{}, + vmConfig, mock.Notifications, cfg.StateStream, false, /*badBlockHalt*/ @@ -641,7 +650,7 @@ func New(tb testing.TB, opts ...Option) *ExecModuleTester { false, /*experimentalBAL*/ ), nil, /*notifier*/ - &vm.Config{}, + vmConfig, dirs.Tmp, mock.TxPool, miningCancel, @@ -666,7 +675,7 @@ func New(tb testing.TB, opts ...Option) *ExecModuleTester { cfg.BatchSize, mock.ChainConfig, mock.Engine, - &vm.Config{}, + vmConfig, mock.Notifications, cfg.StateStream, false, /*badBlockHalt*/ diff --git a/execution/stagedsync/exec3.go b/execution/stagedsync/exec3.go index d0eb96df489..2f3a96de747 100644 --- a/execution/stagedsync/exec3.go +++ b/execution/stagedsync/exec3.go @@ -112,6 +112,9 @@ func ExecV3(ctx context.Context, parallel bool, //nolint maxBlockNum uint64, logger log.Logger) (execErr error) { + if cfg.vmConfig != nil && cfg.vmConfig.UseGevm { + parallel = false + } isForkValidation := execStage.SyncMode() == stages.ModeForkValidation isApplyingBlocks := execStage.SyncMode() == stages.ModeApplyingBlocks diff --git a/execution/stagedsync/exec3_gevm.go b/execution/stagedsync/exec3_gevm.go new file mode 100644 index 00000000000..fd2326ff3ba --- /dev/null +++ b/execution/stagedsync/exec3_gevm.go @@ -0,0 +1,276 @@ +package stagedsync + +import ( + "context" + "fmt" + "os" + "time" + + "github.com/erigontech/erigon/common" + "github.com/erigontech/erigon/db/rawdb" + "github.com/erigontech/erigon/db/rawdb/rawtemporaldb" + "github.com/erigontech/erigon/execution/exec" + "github.com/erigontech/erigon/execution/protocol" + "github.com/erigontech/erigon/execution/protocol/rules" + "github.com/erigontech/erigon/execution/state" + "github.com/erigontech/erigon/execution/tracing/calltracer" + "github.com/erigontech/erigon/execution/types" + "github.com/erigontech/erigon/execution/vm/gevm" +) + +func (se *serialExecutor) executeBlockGevm(ctx context.Context, tasks []exec.Task, isInitialCycle bool) (bool, error) { + if se.taskExecMetrics == nil { + se.taskExecMetrics = exec.NewWorkerMetrics() + } + if se.blockExecMetrics == nil { + se.blockExecMetrics = newBlockExecMetrics() + } + defer func(t time.Time) { + se.blockExecMetrics.BlockCount.Add(1) + se.blockExecMetrics.Duration.Add(time.Since(t)) + }(time.Now()) + + if len(tasks) == 0 { + return true, nil + } + firstTask := tasks[0].(*exec.TxTask) + startTxIndex := len(firstTask.Txs) + for _, task := range tasks { + txIndex := task.(*exec.TxTask).TxIndex + if txIndex >= 0 && txIndex < len(firstTask.Txs) { + startTxIndex = txIndex + break + } + } + hasTxTask := startTxIndex < len(firstTask.Txs) + + blockReceipts := make([]*types.Receipt, 0, len(firstTask.Txs)) + gasPool := protocol.NewGasPool(firstTask.BlockGasLimit(), se.cfg.chainConfig.GetMaxBlobGasPerBlock(firstTask.BlockTime())) + blockExec := gevm.NewBlockExecutor(se.cfg.chainConfig, firstTask.Header, firstTask.EvmBlockContext, se.applyTx, se.doms) + defer blockExec.Release() + appliedGevmState := false + debugTx := os.Getenv("GEVM_DEBUG_TX") == "1" + + for _, task := range tasks { + txTask := task.(*exec.TxTask) + txTask.Config = se.cfg.chainConfig + txTask.Engine = se.cfg.engine + txTask.ResetGasPool(gasPool) + + var result *exec.TxResult + switch { + case txTask.TxIndex == -1: + if startTxIndex == 0 && (hasTxTask || len(txTask.Txs) == 0) && se.cfg.chainConfig.IsCancun(txTask.Header.Time) { + blockExec.ApplyBeaconRoot(txTask.Header.ParentBeaconBlockRoot) + appliedGevmState = true + } + if startTxIndex == 0 && (hasTxTask || len(txTask.Txs) == 0) && se.cfg.chainConfig.IsPrague(txTask.Header.Time) { + if err := blockExec.StoreParentHash(txTask.Header); err != nil { + return false, err + } + appliedGevmState = true + } + blockExec.CommitTx() + result = &exec.TxResult{Task: txTask} + se.onBlockStart(ctx, txTask.BlockNumber(), txTask.BlockHash()) + case txTask.IsBlockEnd(): + if txTask.BlockNumber() == 0 { + stateWriter := state.NewWriter(se.doms.AsPutDel(se.applyTx), se.accumulator, txTask.TxNum) + if err := blockExec.ApplyGenesisState(se.cfg.genesis, stateWriter); err != nil { + return false, err + } + result = &exec.TxResult{Task: txTask} + break + } + finalizeReceipts := blockReceipts + if startTxIndex > 0 && len(txTask.Txs) > 0 { + blockStartTxNum := firstTask.TxNum - uint64(firstTask.TxIndex) + priorReceipts, priorErr := se.derivePriorReceiptsGevm(ctx, txTask, startTxIndex, blockStartTxNum) + if priorErr != nil { + se.logger.Warn(fmt.Sprintf("[%s] failed to reconstruct prior receipts for partial block", se.logPrefix), + "block", txTask.BlockNumber(), "startTxIndex", startTxIndex, "err", priorErr) + } else { + finalizeReceipts = append(priorReceipts, blockReceipts...) + } + } + if _, err := blockExec.FinalizeBlock(se.cfg.engine, se.cfg.chainConfig, txTask.Header, txTask.Uncles, finalizeReceipts, txTask.Withdrawals); err != nil { + return false, fmt.Errorf("%w, txnIdx=%d, %w", rules.ErrInvalidBlock, txTask.TxIndex, err) + } + if txTask.BlockNumber() > 0 || txTask.Withdrawals != nil || se.cfg.chainConfig.IsPrague(txTask.Header.Time) { + appliedGevmState = true + } + result = &exec.TxResult{Task: txTask} + checkBloom := !se.cfg.vmConfig.StatelessExec && !se.cfg.vmConfig.NoReceipts + checkReceipts := checkBloom && se.cfg.chainConfig.IsByzantium(txTask.BlockNumber()) + if txTask.BlockNumber() > 0 && startTxIndex == 0 && (hasTxTask || len(txTask.Txs) == 0) { + if err := protocol.BlockPostValidation(se.blockGasUsed, se.blobGasUsed, checkReceipts, checkBloom, blockReceipts, txTask.Header, txTask.Txs, se.cfg.chainConfig, se.logger); err != nil { + return false, fmt.Errorf("%w, txnIdx=%d, %w", rules.ErrInvalidBlock, txTask.TxIndex, err) + } + } + stateWriter := state.NewWriter(se.doms.AsPutDel(se.applyTx), se.accumulator, txTask.TxNum) + if appliedGevmState { + if err := blockExec.ApplyState(stateWriter); err != nil { + return false, err + } + } + if se.cfg.notifications != nil && !isInitialCycle { + se.cfg.notifications.RecentReceipts.Add(blockReceipts, txTask.Txs, txTask.Header) + } + default: + msg, err := txTask.TxMessage() + if err != nil { + return false, fmt.Errorf("%w, txnIdx=%d, %v", rules.ErrInvalidBlock, txTask.TxIndex, err) + } + var callTracer *calltracer.CallTracer + if hooks := txTask.TracingHooks(); hooks != nil { + callTracer = calltracer.NewCallTracer(hooks) + } + out, err := blockExec.ExecuteTx(txTask.Tx(), msg, callTracer) + if err != nil { + return false, fmt.Errorf("%w, txnIdx=%d, %v", rules.ErrInvalidBlock, txTask.TxIndex, err) + } + result = &exec.TxResult{ + Task: txTask, + ExecutionResult: out.Result, + Logs: out.Logs, + TraceFroms: out.TraceFroms, + TraceTos: out.TraceTos, + } + if callTracer != nil { + result.TraceFroms = callTracer.Froms() + result.TraceTos = callTracer.Tos() + } + se.txCount++ + se.blockGasUsed += result.ExecutionResult.BlockRegularGasUsed + if txTask.Tx() != nil { + se.blobGasUsed += txTask.Tx().GetBlobGas() + } + var receipt *types.Receipt + if len(blockReceipts) > 0 { + receipt, err = result.CreateNextReceipt(blockReceipts[len(blockReceipts)-1]) + } else if txTask.TxIndex > 0 { + cumGasUsed, _, logIndexAfterTx, err := rawtemporaldb.ReceiptAsOf(se.applyTx, txTask.TxNum-1) + if err != nil { + return false, err + } + receipt, err = result.CreateReceipt(txTask.TxIndex, cumGasUsed+result.ExecutionResult.ReceiptGasUsed, logIndexAfterTx) + } else { + receipt, err = result.CreateNextReceipt(nil) + } + if err != nil { + return false, err + } + blockReceipts = append(blockReceipts, receipt) + if debugTx { + fmt.Printf("GEVM_TX block=%d idx=%d gas=%d cumulative=%d status=%d err=%v hash=%x\n", txTask.BlockNumber(), txTask.TxIndex, result.ExecutionResult.ReceiptGasUsed, receipt.CumulativeGasUsed, receipt.Status, result.Err, txTask.Tx().Hash()) + } + appliedGevmState = true + if hooks := result.TracingHooks(); hooks != nil && hooks.OnTxEnd != nil { + hooks.OnTxEnd(receipt, result.Err) + } + } + + var applyReceipt *types.Receipt + if txTask.TxIndex >= 0 && txTask.TxIndex-startTxIndex >= 0 && txTask.TxIndex-startTxIndex < len(blockReceipts) { + applyReceipt = blockReceipts[txTask.TxIndex-startTxIndex] + } + if !txTask.HistoryExecution { + if err := se.rs.ApplyStateWrites(ctx, se.applyTx, txTask.BlockNumber(), txTask.TxNum, nil, nil, txTask.Rules(), nil); err != nil { + return false, err + } + if err := se.rs.ApplyTxIndexes(se.applyTx, txTask.TxNum, applyReceipt, se.blobGasUsed, result.Logs, result.TraceFroms, result.TraceTos); err != nil { + return false, err + } + if err := se.rs.CommitStepBoundary(ctx, se.applyTx, txTask.BlockNumber(), txTask.TxNum); err != nil { + return false, err + } + } + + se.doms.SetTxNum(txTask.TxNum) + se.lastBlockResult = &blockResult{BlockNum: txTask.BlockNumber(), lastTxNum: txTask.TxNum} + se.lastExecutedTxNum.Store(int64(txTask.TxNum)) + se.lastExecutedBlockNum.Store(int64(txTask.BlockNumber())) + + if txTask.IsBlockEnd() { + se.executedGas.Add(int64(se.blockGasUsed)) + se.blockGasUsed = 0 + se.blockStateGasUsed = 0 + se.blobGasUsed = 0 + gasPool = nil + } + } + return true, nil +} + +func (se *serialExecutor) derivePriorReceiptsGevm(ctx context.Context, txTask *exec.TxTask, startTxIndex int, blockStartTxNum uint64) (types.Receipts, error) { + if startTxIndex <= 0 { + return nil, nil + } + blockHash := txTask.Header.Hash() + blockNum := txTask.Header.Number.Uint64() + cached := make(types.Receipts, 0, startTxIndex) + allCached := true + for i := 0; i < startTxIndex && i < len(txTask.Txs); i++ { + receipt, ok, err := rawdb.ReadReceiptCacheV2(se.applyTx, rawdb.RCacheV2Query{ + BlockNum: blockNum, + BlockHash: blockHash, + TxnHash: txTask.Txs[i].Hash(), + TxNum: blockStartTxNum + uint64(i), + DontCalcBloom: true, + }) + if err != nil || !ok { + allCached = false + break + } + cached = append(cached, receipt) + } + if allCached && len(cached) == startTxIndex { + return cached, nil + } + + reader := state.NewHistoryReaderV3(se.applyTx, blockStartTxNum) + historyBlockCtx := txTask.EvmBlockContext + historyBlockCtx.GetHash = protocol.GetHashFn(txTask.Header, func(hash common.Hash, number uint64) (*types.Header, error) { + return se.cfg.blockReader.Header(ctx, se.applyTx, hash, number) + }) + priorExec := gevm.NewBlockExecutorWithReader(se.cfg.chainConfig, txTask.Header, historyBlockCtx, reader) + defer priorExec.Release() + priorGp := protocol.NewGasPool(txTask.Header.GasLimit, se.cfg.chainConfig.GetMaxBlobGasPerBlock(txTask.Header.Time)) + receipts := make(types.Receipts, 0, startTxIndex) + for i := 0; i < startTxIndex && i < len(txTask.Txs); i++ { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + task := *txTask + task.ResetTx(blockStartTxNum+uint64(i), i) + task.ResetGasPool(priorGp) + msg, err := task.TxMessage() + if err != nil { + return nil, fmt.Errorf("derive prior GEVM receipt tx %d: %w", i, err) + } + out, err := priorExec.ExecuteTx(task.Tx(), msg, nil) + if err != nil { + return nil, fmt.Errorf("derive prior GEVM receipt tx %d: %w", i, err) + } + result := &exec.TxResult{ + Task: &task, + ExecutionResult: out.Result, + Logs: out.Logs, + TraceFroms: out.TraceFroms, + TraceTos: out.TraceTos, + } + var receipt *types.Receipt + if len(receipts) > 0 { + receipt, err = result.CreateNextReceipt(receipts[len(receipts)-1]) + } else { + receipt, err = result.CreateNextReceipt(nil) + } + if err != nil { + return nil, fmt.Errorf("derive prior GEVM receipt tx %d: %w", i, err) + } + receipts = append(receipts, receipt) + } + return receipts, nil +} diff --git a/execution/stagedsync/exec3_serial.go b/execution/stagedsync/exec3_serial.go index 10b1b656ff1..522a44d040b 100644 --- a/execution/stagedsync/exec3_serial.go +++ b/execution/stagedsync/exec3_serial.go @@ -57,7 +57,11 @@ func (se *serialExecutor) exec(ctx context.Context, execStage *StageState, u Unw initialTxNum uint64, inputTxNum uint64, initialCycle bool, rwTx kv.TemporalRwTx, accumulator *shards.Accumulator, readAhead chan uint64, logEvery *time.Ticker) (*types.Header, kv.TemporalRwTx, error) { - se.resetWorkers(ctx, se.rs, se.applyTx) + if se.cfg.vmConfig == nil || !se.cfg.vmConfig.UseGevm { + se.resetWorkers(ctx, se.rs, se.applyTx) + } else { + se.applyTx = rwTx + } havePartialBlock := false blockNum := startBlockNum @@ -330,6 +334,10 @@ func (se *serialExecutor) resetWorkers(ctx context.Context, rs *state.StateV3Buf } func (se *serialExecutor) executeBlock(ctx context.Context, tasks []exec.Task, isInitialCycle bool, profile bool) (cont bool, err error) { + if se.cfg.vmConfig != nil && se.cfg.vmConfig.UseGevm { + return se.executeBlockGevm(ctx, tasks, isInitialCycle) + } + blockReceipts := make([]*types.Receipt, 0, len(tasks)) var startTxIndex int diff --git a/execution/stagedsync/stage_execute.go b/execution/stagedsync/stage_execute.go index b16ddc6db6d..2970af7287d 100644 --- a/execution/stagedsync/stage_execute.go +++ b/execution/stagedsync/stage_execute.go @@ -388,7 +388,11 @@ func SpawnExecuteBlocksStage(s *StageState, u Unwinder, doms *execctx.SharedDoma return nil } - if err := ExecV3(ctx, s, u, cfg, doms, rwTx, dbg.Exec3Parallel || cfg.experimentalBAL, to, logger); err != nil { + parallel := dbg.Exec3Parallel || cfg.experimentalBAL + if cfg.vmConfig != nil && cfg.vmConfig.UseGevm { + parallel = false + } + if err := ExecV3(ctx, s, u, cfg, doms, rwTx, parallel, to, logger); err != nil { return err } return nil diff --git a/execution/stagedsync/stageloop/stageloop.go b/execution/stagedsync/stageloop/stageloop.go index 14a93e555cb..4d117efdce8 100644 --- a/execution/stagedsync/stageloop/stageloop.go +++ b/execution/stagedsync/stageloop/stageloop.go @@ -752,7 +752,7 @@ func NewDefaultStages(ctx context.Context, stagedsync.StageBlockHashesCfg(dirs.Tmp, blockWriter), stagedsync.StageBodiesCfg(controlServer.Bd, controlServer.SendBodyRequest, controlServer.Penalize, controlServer.BroadcastNewBlock, cfg.Sync.BodyDownloadTimeoutSeconds, controlServer.ChainConfig, blockReader, blockWriter), stagedsync.StageSendersCfg(controlServer.ChainConfig, cfg.Sync, dbg.BadBlockHalt, dirs.Tmp, cfg.Prune, blockReader, controlServer.Hd), - stagedsync.StageExecuteBlocksCfg(db, cfg.Prune, cfg.BatchSize, controlServer.ChainConfig, controlServer.Engine, &vm.Config{Tracer: tracingHooks}, notifications, cfg.StateStream, dbg.BadBlockHalt, dirs, blockReader, controlServer.Hd, cfg.Genesis, cfg.Sync, cfg.ExperimentalBAL), + stagedsync.StageExecuteBlocksCfg(db, cfg.Prune, cfg.BatchSize, controlServer.ChainConfig, controlServer.Engine, &vm.Config{Tracer: tracingHooks, UseGevm: cfg.UseGevm}, notifications, cfg.StateStream, dbg.BadBlockHalt, dirs, blockReader, controlServer.Hd, cfg.Genesis, cfg.Sync, cfg.ExperimentalBAL), stagedsync.StageTxLookupCfg(cfg.Prune, dirs.Tmp, blockReader), stagedsync.StageFinishCfg(), ) @@ -786,7 +786,7 @@ func NewPipelineStages(ctx context.Context, stagedsync.StageSnapshotsCfg(db, controlServer.ChainConfig, cfg.Sync, dirs, blockRetire, snapDownloader, blockReader, notifications, cfg.InternalCL && cfg.CaplinConfig.ArchiveBlocks, cfg.CaplinConfig.ArchiveBlobs, cfg.CaplinConfig.ArchiveStates, cfg.Prune, afterSnapshotDownload, cfg.Snapshot.ManifestReady), stagedsync.StageBlockHashesCfg(dirs.Tmp, blockWriter), stagedsync.StageSendersCfg(controlServer.ChainConfig, cfg.Sync, dbg.BadBlockHalt, dirs.Tmp, cfg.Prune, blockReader, controlServer.Hd), - stagedsync.StageExecuteBlocksCfg(db, cfg.Prune, cfg.BatchSize, controlServer.ChainConfig, controlServer.Engine, &vm.Config{Tracer: tracingHooks}, notifications, cfg.StateStream, dbg.BadBlockHalt, dirs, blockReader, controlServer.Hd, cfg.Genesis, cfg.Sync, cfg.ExperimentalBAL), + stagedsync.StageExecuteBlocksCfg(db, cfg.Prune, cfg.BatchSize, controlServer.ChainConfig, controlServer.Engine, &vm.Config{Tracer: tracingHooks, UseGevm: cfg.UseGevm}, notifications, cfg.StateStream, dbg.BadBlockHalt, dirs, blockReader, controlServer.Hd, cfg.Genesis, cfg.Sync, cfg.ExperimentalBAL), stagedsync.StageTxLookupCfg(cfg.Prune, dirs.Tmp, blockReader), stagedsync.StageFinishCfg(), stagedsync.StageWitnessProcessingCfg(controlServer.ChainConfig, controlServer.WitnessBuffer), @@ -810,7 +810,7 @@ func NewInMemoryExecution( stagedsync.StageBodiesCfg(controlServer.Bd, controlServer.SendBodyRequest, controlServer.Penalize, controlServer.BroadcastNewBlock, cfg.Sync.BodyDownloadTimeoutSeconds, controlServer.ChainConfig, blockReader, blockWriter), stagedsync.StageBlockHashesCfg(cfg.Dirs.Tmp, blockWriter), stagedsync.StageSendersCfg(controlServer.ChainConfig, cfg.Sync, true /* badBlockHalt */, cfg.Dirs.Tmp, cfg.Prune, blockReader, controlServer.Hd), - stagedsync.StageExecuteBlocksCfg(db, cfg.Prune, cfg.BatchSize, controlServer.ChainConfig, controlServer.Engine, &vm.Config{}, notifications, cfg.StateStream, true, cfg.Dirs, blockReader, controlServer.Hd, cfg.Genesis, cfg.Sync, cfg.ExperimentalBAL), + stagedsync.StageExecuteBlocksCfg(db, cfg.Prune, cfg.BatchSize, controlServer.ChainConfig, controlServer.Engine, &vm.Config{UseGevm: cfg.UseGevm}, notifications, cfg.StateStream, true, cfg.Dirs, blockReader, controlServer.Hd, cfg.Genesis, cfg.Sync, cfg.ExperimentalBAL), ), stagedsync.StateUnwindOrder, nil, /* pruneOrder */ diff --git a/execution/state/history_reader_v3.go b/execution/state/history_reader_v3.go index e46205f7d6f..be4da9a7a7c 100644 --- a/execution/state/history_reader_v3.go +++ b/execution/state/history_reader_v3.go @@ -191,21 +191,26 @@ func (hr *HistoryReaderV3) DiscardReadList() {} func (hr *HistoryReaderV3) ReadAccountData(address accounts.Address) (*accounts.Account, error) { addressValue := address.Value() - enc, ok, err := hr.getAsOf(kv.AccountsDomain, addressValue[:]) + _, acc, err := hr.ReadAccountDataRaw(addressValue) + return acc, err +} + +func (hr *HistoryReaderV3) ReadAccountDataRaw(address common.Address) ([]byte, *accounts.Account, error) { + enc, ok, err := hr.getAsOf(kv.AccountsDomain, address[:]) if err != nil || !ok || len(enc) == 0 { if hr.trace { fmt.Printf("%sReadAccountData (hist)[%x] => []\n", hr.tracePrefix, address) } - return nil, err + return nil, nil, err } var a accounts.Account if err := accounts.DeserialiseV3(&a, enc); err != nil { - return nil, fmt.Errorf("%sread account data (hist)(%x): %w", hr.tracePrefix, address, err) + return nil, nil, fmt.Errorf("%sread account data (hist)(%x): %w", hr.tracePrefix, address, err) } if hr.trace { fmt.Printf("%sReadAccountData (hist)[%x] => [nonce: %d, balance: %d, codeHash: %x]\n", hr.tracePrefix, address, a.Nonce, &a.Balance, a.CodeHash) } - return &a, nil + return enc, &a, nil } // ReadAccountDataForDebug - is like ReadAccountData, but without adding key to `readList`. @@ -217,7 +222,11 @@ func (hr *HistoryReaderV3) ReadAccountDataForDebug(address accounts.Address) (*a func (hr *HistoryReaderV3) ReadAccountStorage(address accounts.Address, key accounts.StorageKey) (uint256.Int, bool, error) { addressValue := address.Value() keyValue := key.Value() - hr.composite = append(append(hr.composite[:0], addressValue[:]...), keyValue[:]...) + return hr.ReadAccountStorageRaw(addressValue, keyValue) +} + +func (hr *HistoryReaderV3) ReadAccountStorageRaw(address common.Address, key common.Hash) (uint256.Int, bool, error) { + hr.composite = append(append(hr.composite[:0], address[:]...), key[:]...) enc, ok, err := hr.getAsOf(kv.StorageDomain, hr.composite) if hr.trace { fmt.Printf("%sReadAccountStorage (hist)[%x] [%x] => [%x]\n", hr.tracePrefix, address, key, enc) @@ -231,12 +240,16 @@ func (hr *HistoryReaderV3) ReadAccountStorage(address accounts.Address, key acco func (hr *HistoryReaderV3) HasStorage(address accounts.Address) (bool, error) { addressValue := address.Value() - to, ok := kv.NextSubtree(addressValue[:]) + return hr.HasStorageRaw(addressValue) +} + +func (hr *HistoryReaderV3) HasStorageRaw(address common.Address) (bool, error) { + to, ok := kv.NextSubtree(address[:]) if !ok { to = nil } - it, err := hr.ttx.RangeAsOf(kv.StorageDomain, addressValue[:], to, hr.txNum, order.Asc, kv.Unlim) + it, err := hr.ttx.RangeAsOf(kv.StorageDomain, address[:], to, hr.txNum, order.Asc, kv.Unlim) if err != nil { return false, err } @@ -264,7 +277,11 @@ func (hr *HistoryReaderV3) ReadAccountCode(address accounts.Address) ([]byte, er // must pass key2=Nil here: because Erigon4 does concatinate key1+key2 under the hood //code, _, err := hr.ttx.GetAsOf(kv.CodeDomain, address.Bytes(), codeHash.Bytes(), hr.txNum) addressValue := address.Value() - code, _, err := hr.getAsOf(kv.CodeDomain, addressValue[:]) + return hr.ReadAccountCodeRaw(addressValue) +} + +func (hr *HistoryReaderV3) ReadAccountCodeRaw(address common.Address) ([]byte, error) { + code, _, err := hr.getAsOf(kv.CodeDomain, address[:]) if hr.trace { lenc, cs := printCode(code) fmt.Printf("%sReadAccountCode (hist)[%x] => [%d:%s]\n", hr.tracePrefix, address, lenc, cs) diff --git a/execution/state/rw_v3.go b/execution/state/rw_v3.go index 8f5ef33ca54..637f924c91b 100644 --- a/execution/state/rw_v3.go +++ b/execution/state/rw_v3.go @@ -948,6 +948,25 @@ func (w *Writer) WriteAccountStorage(address accounts.Address, incarnation uint6 return w.tx.DomainPut(kv.StorageDomain, composite, v, w.txNum, nil) } +func (w *Writer) WriteAccountStorageRaw(address common.Address, incarnation uint64, key common.Hash, original, value uint256.Int) error { + if original == value { + return nil + } + var composite [20 + 32]byte + copy(composite[:20], address[:]) + copy(composite[20:], key[:]) + if value.IsZero() { + return w.tx.DomainDel(kv.StorageDomain, composite[:], w.txNum, nil) + } + var valueBuf [32]byte + value.WriteToSlice(valueBuf[:]) + valueBytes := valueBuf[32-value.ByteLen():] + if w.accumulator != nil { + w.accumulator.ChangeStorage(address, incarnation, key, valueBytes) + } + return w.tx.DomainPut(kv.StorageDomain, composite[:], valueBytes, w.txNum, nil) +} + var fastCreate = dbg.EnvBool("FAST_CREATE", false) func (w *Writer) CreateContract(address accounts.Address) error { @@ -1381,12 +1400,16 @@ func (r *ReaderV3) HasStorage(address accounts.Address) (bool, error) { if !address.IsNil() { value = address.Value() } + return r.HasStorageRaw(value) +} + +func (r *ReaderV3) HasStorageRaw(address common.Address) (bool, error) { // this is an optimization, but also checks the account is checked in the domain // for being deleted on unwind before we try to access the storage - if enc, _, err := r.getter.GetLatest(kv.AccountsDomain, value[:]); len(enc) == 0 { + if enc, _, err := r.getter.GetLatest(kv.AccountsDomain, address[:]); len(enc) == 0 { return false, err } - _, _, hasStorage, err := r.getter.HasPrefix(kv.StorageDomain, value[:]) + _, _, hasStorage, err := r.getter.HasPrefix(kv.StorageDomain, address[:]) return hasStorage, err } @@ -1400,7 +1423,11 @@ func (r *ReaderV3) readAccountData(address accounts.Address) ([]byte, *accounts. if !address.IsNil() { value = address.Value() } - enc, _, err := r.getter.GetLatest(kv.AccountsDomain, value[:]) + return r.ReadAccountDataRaw(value) +} + +func (r *ReaderV3) ReadAccountDataRaw(address common.Address) ([]byte, *accounts.Account, error) { + enc, _, err := r.getter.GetLatest(kv.AccountsDomain, address[:]) if err != nil { return nil, nil, err } @@ -1426,7 +1453,6 @@ func (r *ReaderV3) ReadAccountDataForDebug(address accounts.Address) (*accounts. } func (r *ReaderV3) ReadAccountStorage(address accounts.Address, key accounts.StorageKey) (uint256.Int, bool, error) { - var composite [20 + 32]byte var addressValue common.Address if !address.IsNil() { addressValue = address.Value() @@ -1435,8 +1461,13 @@ func (r *ReaderV3) ReadAccountStorage(address accounts.Address, key accounts.Sto if !key.IsNil() { keyValue = key.Value() } - copy(composite[0:20], addressValue[0:20]) - copy(composite[20:], keyValue[:]) + return r.ReadAccountStorageRaw(addressValue, keyValue) +} + +func (r *ReaderV3) ReadAccountStorageRaw(address common.Address, key common.Hash) (uint256.Int, bool, error) { + var composite [20 + 32]byte + copy(composite[0:20], address[0:20]) + copy(composite[20:], key[:]) enc, _, err := r.getter.GetLatest(kv.StorageDomain, composite[:]) if err != nil { return uint256.Int{}, false, err @@ -1464,7 +1495,11 @@ func (r *ReaderV3) ReadAccountCode(address accounts.Address) ([]byte, error) { if !address.IsNil() { addressValue = address.Value() } - enc, _, err := r.getter.GetLatest(kv.CodeDomain, addressValue[:]) + return r.ReadAccountCodeRaw(addressValue) +} + +func (r *ReaderV3) ReadAccountCodeRaw(address common.Address) ([]byte, error) { + enc, _, err := r.getter.GetLatest(kv.CodeDomain, address[:]) if err != nil { return nil, err } diff --git a/execution/tests/testutil/block_test_util.go b/execution/tests/testutil/block_test_util.go index dda0b685f55..979866e7e5b 100644 --- a/execution/tests/testutil/block_test_util.go +++ b/execution/tests/testutil/block_test_util.go @@ -224,6 +224,13 @@ func (bt *BlockTest) Run(t *testing.T) error { if bt.ExperimentalBAL { mOpts = append(mOpts, execmoduletester.WithExperimentalBAL()) } + useGevm := UseGevm() + if useGevm && !gevmTesterSupported(config) { + useGevm = false + } + if useGevm { + mOpts = append(mOpts, execmoduletester.WithUseGevm()) + } m := execmoduletester.New(t, mOpts...) bt.br = m.BlockReader @@ -265,7 +272,18 @@ func (bt *BlockTest) RunCLI() error { return testforks.UnsupportedForkError{Name: bt.json.Network} } engine := rulesconfig.CreateRulesEngineBareBones(context.Background(), config, log.New()) - m := execmoduletester.New(nil, execmoduletester.WithGenesisSpec(bt.genesis(config)), execmoduletester.WithEngine(engine)) + mOpts := []execmoduletester.Option{ + execmoduletester.WithGenesisSpec(bt.genesis(config)), + execmoduletester.WithEngine(engine), + } + useGevm := UseGevm() + if useGevm && !gevmTesterSupported(config) { + useGevm = false + } + if useGevm { + mOpts = append(mOpts, execmoduletester.WithUseGevm()) + } + m := execmoduletester.New(nil, mOpts...) defer m.DB.Close() bt.br = m.BlockReader diff --git a/execution/tests/testutil/gevm.go b/execution/tests/testutil/gevm.go new file mode 100644 index 00000000000..a26aa57adfd --- /dev/null +++ b/execution/tests/testutil/gevm.go @@ -0,0 +1,15 @@ +package testutil + +import ( + "os" + + "github.com/erigontech/erigon/execution/chain" +) + +func UseGevm() bool { + return os.Getenv("USE_GEVM") == "1" +} + +func gevmTesterSupported(config *chain.Config) bool { + return config != nil && config.AmsterdamTime == nil +} diff --git a/execution/tests/testutil/state_test_util.go b/execution/tests/testutil/state_test_util.go index e85f937f204..4f5e747d0de 100644 --- a/execution/tests/testutil/state_test_util.go +++ b/execution/tests/testutil/state_test_util.go @@ -53,6 +53,7 @@ import ( "github.com/erigontech/erigon/execution/types" "github.com/erigontech/erigon/execution/types/accounts" "github.com/erigontech/erigon/execution/vm" + gevmexec "github.com/erigontech/erigon/execution/vm/gevm" "github.com/erigontech/erigon/rpc/rpchelper" ) @@ -175,6 +176,24 @@ func (t *StateTest) Subtests() []StateSubtest { // Run executes a specific subtest and verifies the post-state and logs func (t *StateTest) Run(tb testing.TB, tx kv.TemporalRwTx, subtest StateSubtest, vmconfig vm.Config, dirs datadir.Dirs) (*state.IntraBlockState, common.Hash, error) { + useGevm, err := stateTestUseGevm(subtest.Fork, vmconfig) + if err != nil { + return nil, empty.RootHash, err + } + if useGevm { + st, root, _, logs, err := t.runNoVerifyGevm(tx, subtest, vmconfig, dirs) + if err != nil { + return st, empty.RootHash, err + } + post := t.Json.Post[subtest.Fork][subtest.Index] + if root != common.Hash(post.Root) { + return st, root, fmt.Errorf("post state root mismatch: got %x, want %x", root, post.Root) + } + if logHash := rlpHash(logs); logHash != common.Hash(post.Logs) { + return st, root, fmt.Errorf("post state logs hash mismatch: got %x, want %x", logHash, post.Logs) + } + return st, root, nil + } state, root, _, err := t.RunNoVerify(tb, tx, subtest, vmconfig, dirs) if err != nil { return state, empty.RootHash, err @@ -193,6 +212,14 @@ func (t *StateTest) Run(tb testing.TB, tx kv.TemporalRwTx, subtest StateSubtest, // RunNoVerify runs a specific subtest and returns the statedb, post-state root and gas used. func (t *StateTest) RunNoVerify(tb testing.TB, tx kv.TemporalRwTx, subtest StateSubtest, vmconfig vm.Config, dirs datadir.Dirs) (*state.IntraBlockState, common.Hash, uint64, error) { + useGevm, err := stateTestUseGevm(subtest.Fork, vmconfig) + if err != nil { + return nil, common.Hash{}, 0, err + } + if useGevm { + st, root, gasUsed, _, err := t.runNoVerifyGevm(tx, subtest, vmconfig, dirs) + return st, root, gasUsed, err + } config, eips, err := GetChainConfig(subtest.Fork) if err != nil { return nil, common.Hash{}, 0, testforks.UnsupportedForkError{Name: subtest.Fork} @@ -261,6 +288,7 @@ func (t *StateTest) RunNoVerify(tb testing.TB, tx kv.TemporalRwTx, subtest State return nil, common.Hash{}, 0, err } } + vmconfig.UseGevm = false evm := vm.NewEVM(context, txContext, statedb, config, vmconfig) if vmconfig.Tracer != nil && vmconfig.Tracer.OnTxStart != nil { vmconfig.Tracer.OnTxStart(evm.GetVMContext(), nil, accounts.ZeroAddress) @@ -298,6 +326,126 @@ func (t *StateTest) RunNoVerify(tb testing.TB, tx kv.TemporalRwTx, subtest State return statedb, common.BytesToHash(rootBytes), gasUsed, nil } +func stateTestUseGevm(fork string, vmconfig vm.Config) (bool, error) { + useGevm := vmconfig.UseGevm || UseGevm() + if !useGevm { + return false, nil + } + config, _, err := GetChainConfig(fork) + if err != nil { + return false, testforks.UnsupportedForkError{Name: fork} + } + if useGevm && !gevmTesterSupported(config) { + useGevm = false + } + return useGevm, nil +} + +func (t *StateTest) runNoVerifyGevm(tx kv.TemporalRwTx, subtest StateSubtest, vmconfig vm.Config, dirs datadir.Dirs) (*state.IntraBlockState, common.Hash, uint64, []*types.Log, error) { + config, eips, err := GetChainConfig(subtest.Fork) + if err != nil { + return nil, common.Hash{}, 0, nil, testforks.UnsupportedForkError{Name: subtest.Fork} + } + vmconfig.ExtraEips = eips + block, _, err := genesiswrite.GenesisToBlock(nil, t.genesis(config), dirs, log.Root()) + if err != nil { + return nil, common.Hash{}, 0, nil, testforks.UnsupportedForkError{Name: subtest.Fork} + } + + readBlockNr := block.NumberU64() + if err := MakePreStateGevm(&chain.Rules{}, tx, t.Json.Pre, readBlockNr); err != nil { + return nil, common.Hash{}, 0, nil, testforks.UnsupportedForkError{Name: subtest.Fork} + } + + domains, err := execctx.NewSharedDomains(context.Background(), tx, log.New()) + if err != nil { + return nil, common.Hash{}, 0, nil, testforks.UnsupportedForkError{Name: subtest.Fork} + } + defer domains.Close() + + var baseFee *uint256.Int + if config.IsLondon(0) { + baseFee = t.Json.Env.BaseFee + if baseFee == nil { + baseFee = uint256.NewInt(0x0a) + } + } + post := t.Json.Post[subtest.Fork][subtest.Index] + msg, err := toMessage(t.Json.Tx, post, baseFee) + if err != nil { + return nil, common.Hash{}, 0, nil, err + } + + header := block.HeaderNoCopy() + blockCtx := protocol.NewEVMBlockContext(header, protocol.GetHashFn(header, nil), nil, accounts.InternAddress(t.Json.Env.Coinbase), config) + blockCtx.GetHash = vmTestBlockHash + if baseFee != nil { + blockCtx.BaseFee.Set(baseFee) + } + if t.Json.Env.Difficulty != nil { + blockCtx.Difficulty.Set(t.Json.Env.Difficulty) + } + if config.IsLondon(0) && t.Json.Env.Random != nil { + rnd := common.Hash(t.Json.Env.Random.Bytes32()) + blockCtx.PrevRanDao = &rnd + blockCtx.Difficulty.Clear() + } + if config.IsCancun(block.Time()) && t.Json.Env.ExcessBlobGas != nil { + blockCtx.BlobBaseFee, err = misc.GetBlobGasPrice(config, *t.Json.Env.ExcessBlobGas, header.Time) + if err != nil { + return nil, common.Hash{}, 0, nil, err + } + } + + var decodedTx types.Transaction + if len(post.Tx) > 0 { + decodedTx, err = types.DecodeTransaction(post.Tx) + if err != nil { + return nil, common.Hash{}, 0, nil, err + } + } + + blockExec := gevmexec.NewBlockExecutor(config, header, blockCtx, tx, domains) + defer blockExec.Release() + var out gevmexec.TxOutput + if decodedTx != nil { + out, err = blockExec.ExecuteTx(decodedTx, msg, nil) + } else { + out, err = blockExec.ExecuteMessage(stateTestTxType(t.Json.Tx), msg, nil) + } + if err != nil { + return nil, common.Hash{}, 0, nil, err + } + if out.ValidationError { + return nil, common.Hash{}, out.Result.ReceiptGasUsed, out.Logs, out.Result.Err + } + + w := state.NewWriter(domains.AsPutDel(tx), nil, 1) + if err := blockExec.ApplyState(w); err != nil { + return nil, common.Hash{}, out.Result.ReceiptGasUsed, out.Logs, err + } + rootBytes, err := domains.ComputeCommitment(context2.Background(), tx, true, readBlockNr, 1, "", nil) + if err != nil { + return nil, common.Hash{}, out.Result.ReceiptGasUsed, out.Logs, fmt.Errorf("ComputeCommitment: %w", err) + } + return nil, common.BytesToHash(rootBytes), out.Result.ReceiptGasUsed, out.Logs, nil +} + +func stateTestTxType(tx stTransaction) byte { + switch { + case len(tx.Authorizations) > 0: + return types.SetCodeTxType + case len(tx.BlobVersionedHashes) > 0: + return types.BlobTxType + case tx.MaxFeePerGas != nil || tx.MaxPriorityFeePerGas != nil: + return types.DynamicFeeTxType + case len(tx.AccessLists) > 0: + return types.AccessListTxType + default: + return types.LegacyTxType + } +} + func MakePreState(rules *chain.Rules, tx kv.TemporalRwTx, alloc types.GenesisAlloc, blockNr uint64) (*state.IntraBlockState, error) { r := rpchelper.NewLatestStateReader(tx) statedb := state.New(r) @@ -353,6 +501,58 @@ func MakePreState(rules *chain.Rules, tx kv.TemporalRwTx, alloc types.GenesisAll return statedb, nil } +func MakePreStateGevm(rules *chain.Rules, tx kv.TemporalRwTx, alloc types.GenesisAlloc, blockNr uint64) error { + domains, err := execctx.NewSharedDomains(context.Background(), tx, log.New()) + if err != nil { + return err + } + defer domains.Close() + latestTxNum, latestBlockNum, err := domains.SeekCommitment(context.Background(), tx) + if err != nil { + return err + } + + w := rpchelper.NewLatestStateWriter(tx, domains, (*freezeblocks.BlockReader)(nil), blockNr-1) + emptyAccount := accounts.Account{} + for addr, a := range alloc { + address := accounts.InternAddress(addr) + var account accounts.Account + account.Nonce = a.Nonce + account.CodeHash = accounts.EmptyCodeHash + if a.Balance != nil { + _ = account.Balance.SetFromBig(a.Balance) + } + if len(a.Code) > 0 { + account.CodeHash = accounts.InternCodeHash(common.BytesToHash(crypto.Keccak256(a.Code))) + account.Incarnation = state.FirstContractIncarnation + } + if len(a.Storage) > 0 && account.Incarnation == 0 { + account.Incarnation = state.FirstContractIncarnation + } + if err := w.UpdateAccountData(address, &emptyAccount, &account); err != nil { + return err + } + if len(a.Code) > 0 { + if err := w.UpdateAccountCode(address, account.Incarnation, account.CodeHash, a.Code); err != nil { + return err + } + } + for k, v := range a.Storage { + key := accounts.InternKey(k) + val := uint256.NewInt(0).SetBytes(v.Bytes()) + if err := w.WriteAccountStorage(address, account.Incarnation, key, uint256.Int{}, *val); err != nil { + return err + } + } + } + + _, err = domains.ComputeCommitment(context.Background(), tx, true, latestBlockNum, latestTxNum, "flush-commitment", nil) + if err != nil { + return err + } + return domains.Flush(context2.Background(), tx) +} + func (t *StateTest) genesis(config *chain.Config) *types.Genesis { return &types.Genesis{ Config: config, diff --git a/execution/tracing/calltracer/calltracer.go b/execution/tracing/calltracer/calltracer.go index 744c6bc8a54..82ffd676c94 100644 --- a/execution/tracing/calltracer/calltracer.go +++ b/execution/tracing/calltracer/calltracer.go @@ -67,6 +67,10 @@ func (ct *CallTracer) Tracer() *tracers.Tracer { func (ct *CallTracer) Reset() { ct.froms, ct.tos = nil, nil } +func (ct *CallTracer) ResetKeepMaps() { + clear(ct.froms) + clear(ct.tos) +} func (ct *CallTracer) Froms() map[accounts.Address]struct{} { return ct.froms } func (ct *CallTracer) Tos() map[accounts.Address]struct{} { return ct.tos } diff --git a/execution/vm/gevm/adapter.go b/execution/vm/gevm/adapter.go new file mode 100644 index 00000000000..fe9b03b7c28 --- /dev/null +++ b/execution/vm/gevm/adapter.go @@ -0,0 +1,783 @@ +package gevm + +import ( + "fmt" + + gevmhost "github.com/Giulio2002/gevm/host" + gevmspec "github.com/Giulio2002/gevm/spec" + gevmstate "github.com/Giulio2002/gevm/state" + gevmtypes "github.com/Giulio2002/gevm/types" + gevmvm "github.com/Giulio2002/gevm/vm" + "github.com/holiman/uint256" + + "github.com/erigontech/erigon/common" + "github.com/erigontech/erigon/common/empty" + "github.com/erigontech/erigon/db/kv" + "github.com/erigontech/erigon/db/state/execctx" + "github.com/erigontech/erigon/execution/chain" + "github.com/erigontech/erigon/execution/protocol" + "github.com/erigontech/erigon/execution/protocol/misc" + "github.com/erigontech/erigon/execution/protocol/params" + consensusrules "github.com/erigontech/erigon/execution/protocol/rules" + erigonstate "github.com/erigontech/erigon/execution/state" + "github.com/erigontech/erigon/execution/tracing" + "github.com/erigontech/erigon/execution/tracing/calltracer" + "github.com/erigontech/erigon/execution/types" + "github.com/erigontech/erigon/execution/types/accounts" + "github.com/erigontech/erigon/execution/vm" + "github.com/erigontech/erigon/execution/vm/evmtypes" +) + +type BlockExecutor struct { + evm *gevmhost.Evm + rules *chain.Rules + header *types.Header + tx gevmhost.Transaction + runner gevmvm.TracingRunner + gevmHooks gevmvm.Hooks + tracer *calltracer.CallTracer + tracingHooks *tracing.Hooks + accessList []gevmhost.AccessListItem + accessListKeys []uint256.Int + blobHashes []uint256.Int + authorizations []gevmhost.Authorization + traceAddresses map[gevmtypes.Address]accounts.Address + traceFroms map[accounts.Address]struct{} + traceTos map[accounts.Address]struct{} +} + +type TxOutput struct { + Result evmtypes.ExecutionResult + ValidationError bool + Logs []*types.Log + TraceFroms map[accounts.Address]struct{} + TraceTos map[accounts.Address]struct{} +} + +func NewBlockExecutor(chainConfig *chain.Config, header *types.Header, blockCtx evmtypes.BlockContext, tx kv.TemporalTx, domains *execctx.SharedDomains) *BlockExecutor { + reader := erigonstate.NewReaderV3(domains.AsGetter(tx)) + return NewBlockExecutorWithReader(chainConfig, header, blockCtx, reader) +} + +func NewBlockExecutorWithReader(chainConfig *chain.Config, header *types.Header, blockCtx evmtypes.BlockContext, reader erigonstate.StateReader) *BlockExecutor { + rules := blockCtx.Rules(chainConfig) + fid := forkID(rules) + if fid == gevmspec.London && blockCtx.PrevRanDao != nil { + fid = gevmspec.Merge + } + evm := gevmhost.NewEvmWithReaderOps(reader, readerOps(reader), fid, blockEnv(blockCtx), cfgEnv(chainConfig)) + b := &BlockExecutor{evm: evm, rules: rules, header: header} + b.runner.DebugGasTable = gevmvm.DebugGasTableForFork(evm.ForkID) + b.gevmHooks.OnEnter = b.onEnter + return b +} + +type rawReader interface { + ReadAccountDataRaw(common.Address) ([]byte, *accounts.Account, error) + ReadAccountStorageRaw(common.Address, common.Hash) (uint256.Int, bool, error) + HasStorageRaw(common.Address) (bool, error) + ReadAccountCodeRaw(common.Address) ([]byte, error) +} + +func readerOps(reader erigonstate.StateReader) gevmstate.ReaderOps { + raw, hasRaw := reader.(rawReader) + return gevmstate.ReaderOps{ + Basic: func(address gevmtypes.Address) (gevmstate.AccountInfo, bool, error) { + var acc *accounts.Account + var err error + if hasRaw { + _, acc, err = raw.ReadAccountDataRaw(common.Address(address)) + } else { + acc, err = reader.ReadAccountData(toErigonAddress(address)) + } + if err != nil || acc == nil { + return gevmstate.AccountInfo{}, false, err + } + info := gevmstate.AccountInfo{ + Balance: acc.Balance, + Nonce: acc.Nonce, + Root: gevmtypes.B256(acc.Root), + Incarnation: acc.Incarnation, + CodeHash: gevmtypes.B256(acc.CodeHash.Value()), + } + if info.Incarnation == 0 && info.CodeHash != gevmtypes.B256Zero && info.CodeHash != gevmtypes.KeccakEmpty { + info.Incarnation = 1 + } + return info, true, nil + }, + Storage: func(address gevmtypes.Address, key uint256.Int) (uint256.Int, error) { + if hasRaw { + value, _, err := raw.ReadAccountStorageRaw(common.Address(address), common.Hash(key.Bytes32())) + return value, err + } + value, _, err := reader.ReadAccountStorage(toErigonAddress(address), storageKey(key)) + return value, err + }, + HasStorage: func(address gevmtypes.Address) (bool, error) { + if hasRaw { + return raw.HasStorageRaw(common.Address(address)) + } + return reader.HasStorage(toErigonAddress(address)) + }, + Code: func(address gevmtypes.Address) (gevmtypes.Bytes, error) { + var code []byte + var err error + if hasRaw { + code, err = raw.ReadAccountCodeRaw(common.Address(address)) + } else { + code, err = reader.ReadAccountCode(toErigonAddress(address)) + } + return gevmtypes.Bytes(code), err + }, + } +} + +func (b *BlockExecutor) Release() { + if b.evm != nil { + b.evm.ReleaseEvm() + b.evm = nil + } +} + +func (b *BlockExecutor) ExecuteTx(tx types.Transaction, msg protocol.Message, tracer *calltracer.CallTracer) (TxOutput, error) { + if tx != nil && tx.Type() == types.AccountAbstractionTxType { + return TxOutput{}, fmt.Errorf("account abstraction transaction is not supported by GEVM") + } + typ := txTypeFromMessage(msg, b.rules) + if tx != nil { + typ = txType(tx.Type()) + } + return b.executeTx(typ, msg, tracer) +} + +func (b *BlockExecutor) ExecuteMessage(txType byte, msg protocol.Message, tracer *calltracer.CallTracer) (TxOutput, error) { + if txType == types.AccountAbstractionTxType { + return TxOutput{}, fmt.Errorf("account abstraction transaction is not supported by GEVM") + } + return b.executeTx(gevmTxType(txType), msg, tracer) +} + +func (b *BlockExecutor) executeTx(typ gevmhost.TxType, msg protocol.Message, tracer *calltracer.CallTracer) (TxOutput, error) { + if tracer != nil { + b.tracer = tracer + b.tracingHooks = tracer.TracingHooks() + if b.tracingHooks != nil && b.tracingHooks.OnExit != nil { + b.gevmHooks.OnExit = b.onExit + } else { + b.gevmHooks.OnExit = nil + } + b.evm.SetHooks(&b.gevmHooks) + b.evm.Set(nil) + } else { + b.tracer = nil + b.tracingHooks = nil + b.gevmHooks.OnExit = nil + b.clearTraceMaps() + b.evm.SetHooks(&b.gevmHooks) + b.evm.Set(nil) + } + res := b.evm.TransactBorrowed(b.transaction(typ, msg)) + b.evm.Journal.CommitTx() + out := TxOutput{ + Result: evmtypes.ExecutionResult{ + ReceiptGasUsed: res.GasUsed, + BlockRegularGasUsed: res.GasUsed, + MaxGasUsed: res.GasUsed + uint64(max(res.GasRefund, 0)), + ReturnData: []byte(res.Output), + Reverted: res.Kind == gevmhost.ResultRevert, + }, + ValidationError: res.ValidationError, + Logs: logs(res.Logs), + TraceFroms: b.traceFroms, + TraceTos: b.traceTos, + } + switch { + case res.Kind == gevmhost.ResultRevert: + out.Result.Err = vm.ErrExecutionReverted + case res.Kind == gevmhost.ResultHalt: + out.Result.Err = res.Reason + } + return out, nil +} + +func (b *BlockExecutor) onEnter(depth int, typ byte, from, to gevmtypes.Address, input []byte, gas uint64, value uint256.Int) { + fromAddr := b.traceAddress(from) + toAddr := b.traceAddress(to) + if b.tracer != nil { + b.tracer.OnEnter(depth, typ, fromAddr, toAddr, false, input, gas, value, nil) + } else { + if b.traceFroms == nil { + b.clearTraceMaps() + } + b.traceFroms[fromAddr] = struct{}{} + b.traceTos[toAddr] = struct{}{} + } + if b.tracingHooks != nil && b.tracingHooks.OnEnter != nil { + b.tracingHooks.OnEnter(depth, typ, fromAddr, toAddr, false, input, gas, value, nil) + } +} + +func (b *BlockExecutor) clearTraceMaps() { + if b.traceFroms == nil { + b.traceFroms = make(map[accounts.Address]struct{}, 8) + b.traceTos = make(map[accounts.Address]struct{}, 8) + return + } + clear(b.traceFroms) + clear(b.traceTos) +} + +func (b *BlockExecutor) traceAddress(addr gevmtypes.Address) accounts.Address { + if b.traceAddresses == nil { + b.traceAddresses = make(map[gevmtypes.Address]accounts.Address, 64) + } else if out, ok := b.traceAddresses[addr]; ok { + return out + } + out := toErigonAddress(addr) + b.traceAddresses[addr] = out + return out +} + +func (b *BlockExecutor) onExit(depth int, output []byte, gasUsed uint64, err error, reverted bool) { + if b.tracingHooks != nil && b.tracingHooks.OnExit != nil { + b.tracingHooks.OnExit(depth, output, gasUsed, err, reverted) + } +} + +func (b *BlockExecutor) ApplyState(writer erigonstate.StateWriter) error { + return applyState(b.evm.Journal.State, b.evm.Journal.StateAddresses(), writer, b.rules) +} + +func (b *BlockExecutor) ApplyGenesisState(genesis *types.Genesis, writer erigonstate.StateWriter) error { + if genesis == nil { + return nil + } + for address, account := range genesis.Alloc { + if len(account.Constructor) > 0 { + return fmt.Errorf("genesis constructor allocation is not supported by GEVM") + } + var balance uint256.Int + if account.Balance != nil { + overflow := balance.SetFromBig(account.Balance) + if overflow { + return fmt.Errorf("genesis balance overflow for %x", address) + } + } + code := gevmtypes.Bytes(account.Code) + codeHash := gevmtypes.KeccakEmpty + incarnation := uint64(0) + if len(code) > 0 { + codeHash = gevmtypes.Keccak256(code) + incarnation = erigonstate.FirstContractIncarnation + } + storage := make(map[uint256.Int]uint256.Int, len(account.Storage)) + for key, value := range account.Storage { + var k, v uint256.Int + k.SetBytes32(key.Bytes()) + v.SetBytes32(value.Bytes()) + storage[k] = v + if incarnation == 0 { + incarnation = erigonstate.FirstContractIncarnation + } + } + b.evm.Journal.PutAccount(gevmtypes.Address(address), gevmstate.AccountInfo{ + Balance: balance, + Nonce: account.Nonce, + Incarnation: incarnation, + CodeHash: codeHash, + Code: code, + }, storage) + } + return applyState(b.evm.Journal.State, b.evm.Journal.StateAddresses(), writer, &chain.Rules{}) +} + +func (b *BlockExecutor) CommitTx() { + b.evm.Journal.CommitTx() +} + +func (b *BlockExecutor) ApplyBeaconRoot(root *common.Hash) { + if root == nil { + return + } + _, _ = b.systemCall(params.BeaconRootsAddress, root.Bytes()) +} + +func (b *BlockExecutor) StoreParentHash(header *types.Header) error { + codeSize, err := b.codeSize(params.HistoryStorageAddress) + if err != nil || codeSize == 0 || header.Number.Sign() == 0 { + return err + } + slot := uint256.NewInt((header.Number.Uint64() - 1) % params.BlockHashHistoryServeWindow) + key := accounts.InternKey(common.BytesToHash(slot.Bytes())) + value := *uint256.NewInt(0).SetBytes32(header.ParentHash.Bytes()) + return b.writeStorage(params.HistoryStorageAddress, key, value) +} + +func (b *BlockExecutor) AddBalance(address common.Address, amount uint256.Int) error { + return b.evm.Journal.BalanceIncr(toGevmAddress(accounts.InternAddress(address)), amount) +} + +func (b *BlockExecutor) FinalizeBlock(engine consensusrules.Engine, chainConfig *chain.Config, header *types.Header, uncles []*types.Header, receipts types.Receipts, withdrawals types.Withdrawals) (types.FlatRequests, error) { + if engine != nil { + rewards, err := engine.CalculateRewards(chainConfig, header, uncles, b.systemCall) + if err != nil { + return nil, err + } + for _, reward := range rewards { + if err := b.AddBalance(reward.Beneficiary.Value(), reward.Amount); err != nil { + return nil, err + } + } + } + + for _, w := range withdrawals { + amount := new(uint256.Int).Mul(uint256.NewInt(w.Amount), uint256.NewInt(common.GWei)) + if err := b.AddBalance(w.Address, *amount); err != nil { + return nil, err + } + } + + if !chainConfig.IsPrague(header.Time) { + return nil, nil + } + + requests := make(types.FlatRequests, 0, 3) + var allLogs types.Logs + for _, receipt := range receipts { + if receipt == nil { + return nil, fmt.Errorf("nil receipt: block %d", header.Number.Uint64()) + } + allLogs = append(allLogs, receipt.Logs...) + } + deposits, err := misc.ParseDepositLogs(allLogs, chainConfig.DepositContract) + if err != nil { + return nil, fmt.Errorf("error: could not parse requests logs: %v", err) + } + if deposits != nil { + requests = append(requests, *deposits) + } + + withdrawalReq, err := b.dequeueRequest(chainConfig.GetWithdrawalRequestContract(), types.WithdrawalRequestType) + if err != nil { + return nil, err + } + if withdrawalReq != nil { + requests = append(requests, *withdrawalReq) + } + consolidationReq, err := b.dequeueRequest(chainConfig.GetConsolidationRequestContract(), types.ConsolidationRequestType) + if err != nil { + return nil, err + } + if consolidationReq != nil { + requests = append(requests, *consolidationReq) + } + if header.RequestsHash != nil { + rh := requests.Hash() + if *header.RequestsHash != *rh { + return nil, fmt.Errorf("error: invalid requests root hash in header, expected: %v, got:%v", header.RequestsHash, rh) + } + } + return requests, nil +} + +func (b *BlockExecutor) systemCall(contract accounts.Address, input []byte) ([]byte, error) { + res := b.evm.SystemCall(toGevmAddress(params.SystemAddress), toGevmAddress(contract), input) + if res.Kind == gevmhost.ResultHalt { + return nil, res.Reason + } + if res.Kind == gevmhost.ResultRevert { + return nil, fmt.Errorf("system contract call reverted") + } + return []byte(res.Output), nil +} + +func (b *BlockExecutor) dequeueRequest(contract accounts.Address, requestType byte) (*types.FlatRequest, error) { + codeSize, err := b.codeSize(contract) + if err != nil { + return nil, err + } + if codeSize == 0 { + return nil, fmt.Errorf("request contract has empty code: %x", contract) + } + res, err := b.systemCall(contract, nil) + if err != nil { + return nil, err + } + if res != nil { + return &types.FlatRequest{Type: requestType, RequestData: res}, nil + } + return nil, nil +} + +func (b *BlockExecutor) codeSize(address accounts.Address) (int, error) { + load, err := b.evm.Journal.LoadAccount(toGevmAddress(address)) + if err != nil { + return 0, err + } + if load.Data.Info.Code == nil && load.Data.Info.CodeHash != gevmtypes.KeccakEmpty && !load.Data.Info.CodeHash.IsZero() { + code, err := b.evm.Journal.ReadCode(toGevmAddress(address)) + if err != nil { + return 0, err + } + load.Data.Info.Code = code + } + return len(load.Data.Info.Code), nil +} + +func (b *BlockExecutor) writeStorage(address accounts.Address, key accounts.StorageKey, value uint256.Int) error { + addr := toGevmAddress(address) + k := *new(uint256.Int).SetBytes32(key.Value().Bytes()) + _, err := b.evm.Journal.SStore(addr, k, value) + return err +} + +func storageKey(index uint256.Int) accounts.StorageKey { + return accounts.InternKey(common.Hash(index.Bytes32())) +} + +func applyState(st gevmstate.EvmState, addrs []gevmtypes.Address, writer erigonstate.StateWriter, rules *chain.Rules) error { + rawStorageWriter, hasRawStorageWriter := writer.(interface { + WriteAccountStorageRaw(common.Address, uint64, common.Hash, uint256.Int, uint256.Int) error + }) + for _, addr := range addrs { + acc := st[addr] + if acc == nil { + continue + } + emptyRemoval := rules != nil && rules.IsSpuriousDragon && acc.IsEmpty() && (acc.IsTouched() || acc.IsCreated()) + if acc.IsSelfdestructed() || emptyRemoval { + address := toErigonAddress(addr) + original := erigonAccount(acc.BlockOriginalInfo) + if err := writer.DeleteAccount(address, &original); err != nil { + return err + } + if acc.IsSelfdestructed() && !acc.Info.Balance.IsZero() { + current := erigonAccount(gevmstate.AccountInfo{ + Balance: acc.Info.Balance, + CodeHash: gevmtypes.KeccakEmpty, + }) + if err := writer.UpdateAccountData(address, &accounts.Account{}, ¤t); err != nil { + return err + } + } + continue + } + if acc.IsStorageCleared() && !acc.IsCreated() { + address := toErigonAddress(addr) + original := erigonAccount(acc.BlockOriginalInfo) + if err := writer.DeleteAccount(address, &original); err != nil { + return err + } + } + accountChanged := accountInfoChanged(acc.BlockOriginalInfo, acc.Info) || acc.IsCreated() || + (rules != nil && !rules.IsSpuriousDragon && acc.IsLoadedAsNotExisting() && acc.IsTouched()) || + acc.IsPreserveEmpty() + if accountChanged { + address := toErigonAddress(addr) + original := erigonOriginalAccount(acc) + current := erigonAccount(acc.Info) + if err := writer.UpdateAccountData(address, &original, ¤t); err != nil { + return err + } + } + if (acc.BlockOriginalInfo.CodeHash != acc.Info.CodeHash || acc.IsCreated()) && len(acc.Info.Code) > 0 && gevmtypes.Keccak256(acc.Info.Code) == acc.Info.CodeHash { + address := toErigonAddress(addr) + current := erigonAccount(acc.Info) + if err := writer.UpdateAccountCode(address, current.Incarnation, current.CodeHash, []byte(acc.Info.Code)); err != nil { + return err + } + } + if acc.IsCreated() { + address := toErigonAddress(addr) + if err := writer.CreateContract(address); err != nil { + return err + } + } + incarnation := erigonIncarnation(acc.Info) + for key, slot := range acc.Storage { + if slot.BlockOriginalValue == slot.PresentValue { + continue + } + keyHash := common.Hash(key.Bytes32()) + if hasRawStorageWriter { + if err := rawStorageWriter.WriteAccountStorageRaw(common.Address(addr), incarnation, keyHash, slot.BlockOriginalValue, slot.PresentValue); err != nil { + return err + } + } else { + address := toErigonAddress(addr) + storageKey := accounts.InternKey(keyHash) + if err := writer.WriteAccountStorage(address, incarnation, storageKey, slot.BlockOriginalValue, slot.PresentValue); err != nil { + return err + } + } + } + } + return nil +} + +func accountInfoChanged(a, b gevmstate.AccountInfo) bool { + return a.Nonce != b.Nonce || + a.Incarnation != b.Incarnation || + a.CodeHash != b.CodeHash || + a.Balance != b.Balance +} + +func erigonIncarnation(info gevmstate.AccountInfo) uint64 { + if info.Incarnation != 0 { + return info.Incarnation + } + if info.CodeHash != gevmtypes.B256Zero && info.CodeHash != gevmtypes.KeccakEmpty { + return 1 + } + return 0 +} + +func erigonAccount(info gevmstate.AccountInfo) accounts.Account { + root := empty.RootHash + if info.Root != gevmtypes.B256Zero { + root = common.Hash(info.Root) + } + return accounts.Account{ + Nonce: info.Nonce, + Balance: info.Balance, + Root: root, + CodeHash: accounts.InternCodeHash(common.Hash(info.CodeHash)), + Incarnation: erigonIncarnation(info), + } +} + +func erigonOriginalAccount(acc *gevmstate.Account) accounts.Account { + if acc.IsLoadedAsNotExisting() { + return accounts.Account{} + } + return erigonAccount(acc.BlockOriginalInfo) +} + +func blockEnv(ctx evmtypes.BlockContext) gevmhost.BlockEnv { + var prevrandao *uint256.Int + if ctx.PrevRanDao != nil { + prevrandao = new(uint256.Int).SetBytes32(ctx.PrevRanDao.Bytes()) + } + return gevmhost.BlockEnv{ + Beneficiary: toGevmAddress(ctx.Coinbase), + Timestamp: *uint256.NewInt(ctx.Time), + Number: *uint256.NewInt(ctx.BlockNumber), + Difficulty: ctx.Difficulty, + Prevrandao: prevrandao, + GasLimit: *uint256.NewInt(ctx.GasLimit), + BaseFee: ctx.BaseFee, + BlobGasPrice: ctx.BlobBaseFee, + SlotNum: *uint256.NewInt(ctx.SlotNumber), + GetHash: func(number uint64) (gevmtypes.B256, error) { + h, err := ctx.GetHash(number) + return toGevmHash(h), err + }, + } +} + +func cfgEnv(config *chain.Config) gevmhost.CfgEnv { + var chainID uint256.Int + if config != nil && config.ChainID != nil { + chainID.SetFromBig(config.ChainID) + } + return gevmhost.CfgEnv{ChainId: chainID} +} + +func (b *BlockExecutor) transaction(typ gevmhost.TxType, msg protocol.Message) *gevmhost.Transaction { + gtx := &b.tx + *gtx = gevmhost.Transaction{ + Kind: gevmhost.TxKindCall, + TxType: typ, + Caller: toGevmAddress(msg.From()), + Value: *msg.Value(), + Input: msg.Data(), + GasLimit: msg.Gas(), + GasPrice: *msg.GasPrice(), + MaxFeePerGas: *msg.FeeCap(), + MaxPriorityFeePerGas: *msg.TipCap(), + MaxFeePerBlobGas: *msg.MaxFeePerBlobGas(), + Nonce: msg.Nonce(), + AccessList: b.convertAccessList(msg.AccessList()), + BlobHashes: b.convertHashes(msg.BlobHashes()), + AuthorizationList: b.convertAuthorizations(msg.Authorizations()), + } + if msg.To().IsNil() { + gtx.Kind = gevmhost.TxKindCreate + } else { + gtx.To = toGevmAddress(msg.To()) + } + if !b.rules.IsLondon { + gtx.MaxFeePerGas = gtx.GasPrice + gtx.MaxPriorityFeePerGas = gtx.GasPrice + } + return gtx +} + +func txTypeFromMessage(msg protocol.Message, rules *chain.Rules) gevmhost.TxType { + switch { + case len(msg.Authorizations()) > 0: + return gevmhost.TxTypeEIP7702 + case len(msg.BlobHashes()) > 0: + return gevmhost.TxTypeEIP4844 + case rules.IsLondon && msg.FeeCap() != nil && msg.TipCap() != nil: + return gevmhost.TxTypeEIP1559 + case len(msg.AccessList()) > 0: + return gevmhost.TxTypeEIP2930 + default: + return gevmhost.TxTypeLegacy + } +} + +func txType(typ byte) gevmhost.TxType { + return gevmTxType(typ) +} + +func gevmTxType(typ byte) gevmhost.TxType { + switch typ { + case types.AccessListTxType: + return gevmhost.TxTypeEIP2930 + case types.DynamicFeeTxType: + return gevmhost.TxTypeEIP1559 + case types.BlobTxType: + return gevmhost.TxTypeEIP4844 + case types.SetCodeTxType: + return gevmhost.TxTypeEIP7702 + default: + return gevmhost.TxTypeLegacy + } +} + +func (b *BlockExecutor) convertAccessList(list types.AccessList) []gevmhost.AccessListItem { + if len(list) == 0 { + b.accessList = b.accessList[:0] + b.accessListKeys = b.accessListKeys[:0] + return nil + } + if cap(b.accessList) < len(list) { + b.accessList = make([]gevmhost.AccessListItem, len(list)) + } else { + b.accessList = b.accessList[:len(list)] + } + keys := 0 + for _, item := range list { + keys += len(item.StorageKeys) + } + if cap(b.accessListKeys) < keys { + b.accessListKeys = make([]uint256.Int, keys) + } else { + b.accessListKeys = b.accessListKeys[:keys] + } + keyOffset := 0 + for i, item := range list { + b.accessList[i].Address = gevmtypes.Address(item.Address) + b.accessList[i].StorageKeys = b.accessListKeys[keyOffset : keyOffset+len(item.StorageKeys)] + for j, key := range item.StorageKeys { + b.accessList[i].StorageKeys[j].SetBytes32(key.Bytes()) + } + keyOffset += len(item.StorageKeys) + } + return b.accessList +} + +func (b *BlockExecutor) convertHashes(in []common.Hash) []uint256.Int { + if len(in) == 0 { + b.blobHashes = b.blobHashes[:0] + return nil + } + if cap(b.blobHashes) < len(in) { + b.blobHashes = make([]uint256.Int, len(in)) + } else { + b.blobHashes = b.blobHashes[:len(in)] + } + for i, h := range in { + b.blobHashes[i].SetBytes32(h.Bytes()) + } + return b.blobHashes +} + +func (b *BlockExecutor) convertAuthorizations(in []types.Authorization) []gevmhost.Authorization { + if len(in) == 0 { + b.authorizations = b.authorizations[:0] + return nil + } + if cap(b.authorizations) < len(in) { + b.authorizations = make([]gevmhost.Authorization, len(in)) + } else { + b.authorizations = b.authorizations[:len(in)] + } + for i, a := range in { + b.authorizations[i] = gevmhost.Authorization{ + ChainId: a.ChainID, + Address: gevmtypes.Address(a.Address), + Nonce: a.Nonce, + YParity: a.YParity, + R: gevmtypes.B256(a.R.Bytes32()), + S: gevmtypes.B256(a.S.Bytes32()), + } + } + return b.authorizations +} + +func logs(in []gevmstate.Log) []*types.Log { + if len(in) == 0 { + return nil + } + out := make([]*types.Log, len(in)) + for i := range in { + topics := make([]common.Hash, in[i].NumTopics) + for j, topic := range in[i].TopicSlice() { + topics[j] = common.Hash(topic) + } + out[i] = &types.Log{ + Address: common.Address(in[i].Address), + Topics: topics, + Data: append([]byte(nil), in[i].Data...), + } + } + return out +} + +func forkID(r *chain.Rules) gevmspec.ForkID { + switch { + case r.IsOsaka: + return gevmspec.Osaka + case r.IsPrague: + return gevmspec.Prague + case r.IsCancun: + return gevmspec.Cancun + case r.IsShanghai: + return gevmspec.Shanghai + case r.IsLondon: + return gevmspec.London + case r.IsBerlin: + return gevmspec.Berlin + case r.IsIstanbul: + return gevmspec.Istanbul + case r.IsPetersburg: + return gevmspec.Petersburg + case r.IsConstantinople: + return gevmspec.Constantinople + case r.IsByzantium: + return gevmspec.Byzantium + case r.IsSpuriousDragon: + return gevmspec.SpuriousDragon + case r.IsTangerineWhistle: + return gevmspec.Tangerine + case r.IsHomestead: + return gevmspec.Homestead + default: + return gevmspec.Frontier + } +} + +func toGevmAddress(addr accounts.Address) gevmtypes.Address { + return gevmtypes.Address(addr.Value()) +} + +func toErigonAddress(addr gevmtypes.Address) accounts.Address { + return accounts.InternAddress(common.Address(addr)) +} + +func toGevmHash(h common.Hash) gevmtypes.B256 { + return gevmtypes.B256(h) +} diff --git a/execution/vm/gevm/adapter_test.go b/execution/vm/gevm/adapter_test.go new file mode 100644 index 00000000000..043183d18d8 --- /dev/null +++ b/execution/vm/gevm/adapter_test.go @@ -0,0 +1,74 @@ +package gevm + +import ( + "testing" + + gevmstate "github.com/Giulio2002/gevm/state" + gevmtypes "github.com/Giulio2002/gevm/types" + "github.com/holiman/uint256" + + "github.com/erigontech/erigon/common" + "github.com/erigontech/erigon/execution/chain" + erigonstate "github.com/erigontech/erigon/execution/state" + "github.com/erigontech/erigon/execution/types/accounts" +) + +type recordingWriter struct { + deletes []accounts.Address + updates []accounts.Account +} + +func (w *recordingWriter) UpdateAccountData(_ accounts.Address, _, account *accounts.Account) error { + w.updates = append(w.updates, *account) + return nil +} + +func (w *recordingWriter) UpdateAccountCode(accounts.Address, uint64, accounts.CodeHash, []byte) error { + return nil +} + +func (w *recordingWriter) DeleteAccount(address accounts.Address, _ *accounts.Account) error { + w.deletes = append(w.deletes, address) + return nil +} + +func (w *recordingWriter) WriteAccountStorage(accounts.Address, uint64, accounts.StorageKey, uint256.Int, uint256.Int) error { + return nil +} + +func (w *recordingWriter) CreateContract(accounts.Address) error { + return nil +} + +var _ erigonstate.StateWriter = (*recordingWriter)(nil) + +func TestApplyStatePreservesSelfdestructResidualBalance(t *testing.T) { + addr := gevmtypes.Address{19: 0x01} + balance := *uint256.NewInt(99) + acc := gevmstate.NewAccountFromInfo(gevmstate.AccountInfo{ + Balance: *uint256.NewInt(1), + Nonce: 7, + Incarnation: 1, + CodeHash: gevmtypes.Keccak256([]byte{0x60, 0x00}), + }) + acc.MarkSelfdestruct() + acc.Info.Balance = balance + + writer := &recordingWriter{} + if err := applyState(gevmstate.EvmState{addr: acc}, []gevmtypes.Address{addr}, writer, &chain.Rules{IsSpuriousDragon: true}); err != nil { + t.Fatal(err) + } + if len(writer.deletes) != 1 { + t.Fatalf("deletes: got %d, want 1", len(writer.deletes)) + } + if len(writer.updates) != 1 { + t.Fatalf("updates: got %d, want 1", len(writer.updates)) + } + update := writer.updates[0] + if update.Balance != balance { + t.Fatalf("balance: got %v, want %v", &update.Balance, &balance) + } + if update.Nonce != 0 || update.Incarnation != 0 || update.CodeHash.Value() != common.Hash(gevmtypes.KeccakEmpty) { + t.Fatalf("residual account should be balance-only, got nonce=%d incarnation=%d codeHash=%x", update.Nonce, update.Incarnation, update.CodeHash.Value()) + } +} diff --git a/execution/vm/interpreter.go b/execution/vm/interpreter.go index 159b4e22346..808069a5d5d 100644 --- a/execution/vm/interpreter.go +++ b/execution/vm/interpreter.go @@ -46,6 +46,7 @@ type Config struct { ReadOnly bool // Do no perform any block finalisation StatelessExec bool // true is certain conditions (like state trie root hash matching) need to be relaxed for stateless EVM execution RestoreState bool // Revert all changes made to the state (useful for constant system calls) + UseGevm bool // Use the GEVM interpreter for block execution. ExtraEips []int // Additional EIPS that are to be enabled } diff --git a/go.mod b/go.mod index b889b52dbc0..12e8ef39ca4 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,10 @@ go 1.25.7 replace github.com/holiman/bloomfilter/v2 => github.com/AskAlexSharov/bloomfilter/v2 v2.0.9 +replace github.com/Giulio2002/gevm => github.com/Giulio2002/gevm v0.0.0-20260508142314-92fa74d27f4c + require ( + github.com/Giulio2002/gevm v0.0.0-00010101000000-000000000000 github.com/erigontech/erigon-snapshot v1.3.1-0.20260402120223-7bb412bc89cd github.com/erigontech/go-libdeflate v0.1.0 github.com/erigontech/mdbx-go v0.39.17 @@ -147,6 +150,7 @@ require ( github.com/Antonboom/testifylint v1.6.4 // indirect github.com/BurntSushi/toml v1.6.0 // indirect github.com/Djarvur/go-err113 v0.1.1 // indirect + github.com/Giulio2002/fastkeccak v0.0.0-20260211162430-e46f5da3471e // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/MirrexOne/unqueryvet v1.5.4 // indirect @@ -210,6 +214,7 @@ require ( github.com/containerd/log v0.1.0 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect + github.com/crate-crypto/go-kzg-4844 v1.1.0 // indirect github.com/curioswitch/go-reassign v0.3.0 // indirect github.com/daixiang0/gci v0.13.7 // indirect github.com/dave/dst v0.27.3 // indirect diff --git a/go.sum b/go.sum index 943f1fa217b..45ea431cca0 100644 --- a/go.sum +++ b/go.sum @@ -55,6 +55,10 @@ github.com/Djarvur/go-err113 v0.1.1 h1:eHfopDqXRwAi+YmCUas75ZE0+hoBHJ2GQNLYRSxao github.com/Djarvur/go-err113 v0.1.1/go.mod h1:IaWJdYFLg76t2ihfflPZnM1LIQszWOsFDh2hhhAVF6k= github.com/FastFilter/xorfilter v0.5.1 h1:UjtPttI1SnKWUmeQKu8Y+4ZvMJv7KP6/NZTsGBPQY08= github.com/FastFilter/xorfilter v0.5.1/go.mod h1:h+9l02/leuyyhepO30BKr25MkZdy7LHcfPRBDRuflXw= +github.com/Giulio2002/fastkeccak v0.0.0-20260211162430-e46f5da3471e h1:RrlPQT2lAmlGWldQ7PIhmxnusr7V5cFXynPWhktgP30= +github.com/Giulio2002/fastkeccak v0.0.0-20260211162430-e46f5da3471e/go.mod h1:c8xKFYwwaqZu58HExXsizNY59YXkX8jWnpG8Llk9wZU= +github.com/Giulio2002/gevm v0.0.0-20260508142314-92fa74d27f4c h1:pAg2GYjRqODVzg0cKHGFJL7IbEeXuy2I8R/V7h1yoXI= +github.com/Giulio2002/gevm v0.0.0-20260508142314-92fa74d27f4c/go.mod h1:Mz4nQgnOmhniJ+JZmB7UhPJOxT0eEqtbZxpbt6u3vP0= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= @@ -282,6 +286,8 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/crate-crypto/go-kzg-4844 v1.1.0 h1:EN/u9k2TF6OWSHrCCDBBU6GLNMq88OspHHlMnHfoyU4= +github.com/crate-crypto/go-kzg-4844 v1.1.0/go.mod h1:JolLjpSff1tCCJKaJx4psrlEdlXuJEC996PL3tTAFks= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/curioswitch/go-reassign v0.3.0 h1:dh3kpQHuADL3cobV/sSGETA8DOv457dwl+fbBAhrQPs= github.com/curioswitch/go-reassign v0.3.0/go.mod h1:nApPCCTtqLJN/s8HfItCcKV0jIPwluBOvZP+dsJGA88= diff --git a/node/cli/default_flags.go b/node/cli/default_flags.go index d0e5be7d619..61c28941315 100644 --- a/node/cli/default_flags.go +++ b/node/cli/default_flags.go @@ -66,6 +66,7 @@ var DefaultFlags = []cli.Flag{ &utils.TLSCACertFlag, &StateStreamDisableFlag, &ExperimentalBALFlag, + &UseGevmFlag, &SyncLoopThrottleFlag, &BadBlockFlag, diff --git a/node/cli/flags.go b/node/cli/flags.go index e429068eec8..ec301260351 100644 --- a/node/cli/flags.go +++ b/node/cli/flags.go @@ -100,6 +100,11 @@ var ( Usage: "generate block access list", Value: false, } + UseGevmFlag = cli.BoolFlag{ + Name: "use-gevm", + Usage: "Use GEVM for block execution", + Value: false, + } // Throttling Flags SyncLoopThrottleFlag = cli.StringFlag{ @@ -270,6 +275,7 @@ func applyRemainingEthFlags(ctx *cli.Context, cfg *ethconfig.Config, logger log. cfg.StateStream = !ctx.Bool(StateStreamDisableFlag.Name) cfg.ExperimentalBAL = ctx.Bool(ExperimentalBALFlag.Name) + cfg.UseGevm = ctx.Bool(UseGevmFlag.Name) if bodyCacheLim := ctx.String(BodyCacheLimitFlag.Name); bodyCacheLim != "" { if err := cfg.Sync.BodyCacheLimit.UnmarshalText([]byte(bodyCacheLim)); err != nil { utils.Fatalf("Invalid bodyCacheLimit provided: %v", err) @@ -363,6 +369,9 @@ func ApplyFlagsForEthConfigCobra(f *pflag.FlagSet, cfg *ethconfig.Config) { if v := f.Bool(ExperimentalBALFlag.Name, false, ExperimentalBALFlag.Usage); v != nil { cfg.ExperimentalBAL = *v } + if v := f.Bool(UseGevmFlag.Name, false, UseGevmFlag.Usage); v != nil { + cfg.UseGevm = *v + } if v, _ := f.GetBool(utils.ChaosMonkeyFlag.Name); v { cfg.ChaosMonkey = true diff --git a/node/eth/backend.go b/node/eth/backend.go index 07e3043043b..4fb8d18bcba 100644 --- a/node/eth/backend.go +++ b/node/eth/backend.go @@ -856,7 +856,7 @@ func New(ctx context.Context, stack *node.Node, config *ethconfig.Config, logger config.BatchSize, chainConfig, backend.engine, - &vm.Config{}, + &vm.Config{UseGevm: config.UseGevm}, backend.notifications, config.StateStream, false, /*badBlockHalt*/ @@ -868,7 +868,7 @@ func New(ctx context.Context, stack *node.Node, config *ethconfig.Config, logger config.ExperimentalBAL, ), backend.notifications.Events, - &vm.Config{}, + &vm.Config{UseGevm: config.UseGevm}, tmpdir, txnProvider, backend.miningSealingQuit, diff --git a/node/ethconfig/config.go b/node/ethconfig/config.go index 0ed826ab9ec..8a46c49dd43 100644 --- a/node/ethconfig/config.go +++ b/node/ethconfig/config.go @@ -238,6 +238,7 @@ type Config struct { StateStream bool ExperimentalBAL bool + UseGevm bool // URL to connect to Heimdall node HeimdallURL string From 11c00ad4d32b4c053e3969522b7b8cfcbf30e9fb Mon Sep 17 00:00:00 2001 From: Giulio Rebuffo Date: Fri, 8 May 2026 17:02:54 +0200 Subject: [PATCH 2/2] execution/vm/gevm: implement gevmstate.Database directly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GEVM dropped the parallel ReaderOps callback surface and unified on the state.Database interface. Update the Erigon adapter: - New gevmDatabase struct wraps erigonstate.StateReader (with the *Raw fast-path when the reader exposes it) and implements all six gevmstate.Database methods directly. - NewEvm replaces NewEvmWithReaderOps in the construction site. - readerOps() helper removed. CodeByHash and BlockHash on the wrapper return zero/nil — neither is exercised on the Erigon path: Code(address) is the loadCode read shape, and BlockEnv.GetHash is the BLOCKHASH source. go.mod re-pinned to the matching GEVM commit (5376a47e22e4eb29b2a3231131a1cc1a4e1b678e). Co-Authored-By: Claude Sonnet 4.6 --- execution/vm/gevm/adapter.go | 124 +++++++++++++++++++++-------------- go.mod | 2 +- go.sum | 4 +- 3 files changed, 77 insertions(+), 53 deletions(-) diff --git a/execution/vm/gevm/adapter.go b/execution/vm/gevm/adapter.go index fe9b03b7c28..43362ab1ccf 100644 --- a/execution/vm/gevm/adapter.go +++ b/execution/vm/gevm/adapter.go @@ -65,7 +65,7 @@ func NewBlockExecutorWithReader(chainConfig *chain.Config, header *types.Header, if fid == gevmspec.London && blockCtx.PrevRanDao != nil { fid = gevmspec.Merge } - evm := gevmhost.NewEvmWithReaderOps(reader, readerOps(reader), fid, blockEnv(blockCtx), cfgEnv(chainConfig)) + evm := gevmhost.NewEvm(newGevmDatabase(reader), fid, blockEnv(blockCtx), cfgEnv(chainConfig)) b := &BlockExecutor{evm: evm, rules: rules, header: header} b.runner.DebugGasTable = gevmvm.DebugGasTableForFork(evm.ForkID) b.gevmHooks.OnEnter = b.onEnter @@ -79,57 +79,81 @@ type rawReader interface { ReadAccountCodeRaw(common.Address) ([]byte, error) } -func readerOps(reader erigonstate.StateReader) gevmstate.ReaderOps { +// gevmDatabase implements gevmstate.Database by forwarding to Erigon's +// StateReader (and its `*Raw` fast-path if the reader exposes it). +type gevmDatabase struct { + reader erigonstate.StateReader + raw rawReader + hasRaw bool +} + +func newGevmDatabase(reader erigonstate.StateReader) *gevmDatabase { raw, hasRaw := reader.(rawReader) - return gevmstate.ReaderOps{ - Basic: func(address gevmtypes.Address) (gevmstate.AccountInfo, bool, error) { - var acc *accounts.Account - var err error - if hasRaw { - _, acc, err = raw.ReadAccountDataRaw(common.Address(address)) - } else { - acc, err = reader.ReadAccountData(toErigonAddress(address)) - } - if err != nil || acc == nil { - return gevmstate.AccountInfo{}, false, err - } - info := gevmstate.AccountInfo{ - Balance: acc.Balance, - Nonce: acc.Nonce, - Root: gevmtypes.B256(acc.Root), - Incarnation: acc.Incarnation, - CodeHash: gevmtypes.B256(acc.CodeHash.Value()), - } - if info.Incarnation == 0 && info.CodeHash != gevmtypes.B256Zero && info.CodeHash != gevmtypes.KeccakEmpty { - info.Incarnation = 1 - } - return info, true, nil - }, - Storage: func(address gevmtypes.Address, key uint256.Int) (uint256.Int, error) { - if hasRaw { - value, _, err := raw.ReadAccountStorageRaw(common.Address(address), common.Hash(key.Bytes32())) - return value, err - } - value, _, err := reader.ReadAccountStorage(toErigonAddress(address), storageKey(key)) - return value, err - }, - HasStorage: func(address gevmtypes.Address) (bool, error) { - if hasRaw { - return raw.HasStorageRaw(common.Address(address)) - } - return reader.HasStorage(toErigonAddress(address)) - }, - Code: func(address gevmtypes.Address) (gevmtypes.Bytes, error) { - var code []byte - var err error - if hasRaw { - code, err = raw.ReadAccountCodeRaw(common.Address(address)) - } else { - code, err = reader.ReadAccountCode(toErigonAddress(address)) - } - return gevmtypes.Bytes(code), err - }, + return &gevmDatabase{reader: reader, raw: raw, hasRaw: hasRaw} +} + +func (d *gevmDatabase) Basic(address gevmtypes.Address) (gevmstate.AccountInfo, bool, error) { + var acc *accounts.Account + var err error + if d.hasRaw { + _, acc, err = d.raw.ReadAccountDataRaw(common.Address(address)) + } else { + acc, err = d.reader.ReadAccountData(toErigonAddress(address)) } + if err != nil || acc == nil { + return gevmstate.AccountInfo{}, false, err + } + info := gevmstate.AccountInfo{ + Balance: acc.Balance, + Nonce: acc.Nonce, + Root: gevmtypes.B256(acc.Root), + Incarnation: acc.Incarnation, + CodeHash: gevmtypes.B256(acc.CodeHash.Value()), + } + if info.Incarnation == 0 && info.CodeHash != gevmtypes.B256Zero && info.CodeHash != gevmtypes.KeccakEmpty { + info.Incarnation = 1 + } + return info, true, nil +} + +func (d *gevmDatabase) Storage(address gevmtypes.Address, key uint256.Int) (uint256.Int, error) { + if d.hasRaw { + value, _, err := d.raw.ReadAccountStorageRaw(common.Address(address), common.Hash(key.Bytes32())) + return value, err + } + value, _, err := d.reader.ReadAccountStorage(toErigonAddress(address), storageKey(key)) + return value, err +} + +func (d *gevmDatabase) HasStorage(address gevmtypes.Address) (bool, error) { + if d.hasRaw { + return d.raw.HasStorageRaw(common.Address(address)) + } + return d.reader.HasStorage(toErigonAddress(address)) +} + +func (d *gevmDatabase) Code(address gevmtypes.Address) (gevmtypes.Bytes, error) { + var code []byte + var err error + if d.hasRaw { + code, err = d.raw.ReadAccountCodeRaw(common.Address(address)) + } else { + code, err = d.reader.ReadAccountCode(toErigonAddress(address)) + } + return gevmtypes.Bytes(code), err +} + +// CodeByHash is unused on the Erigon path: GEVM's host loads code via +// Code(address); BLOCKHASH-style code-hash-keyed lookups don't happen +// during stage_exec. Returning nil here is safe. +func (d *gevmDatabase) CodeByHash(codeHash gevmtypes.B256) (gevmtypes.Bytes, error) { + return nil, nil +} + +// BlockHash is unused on the Erigon path: BlockEnv.GetHash is set in +// blockEnv() and EvmHost.BlockHash prefers it over the Database path. +func (d *gevmDatabase) BlockHash(number uint64) (gevmtypes.B256, error) { + return gevmtypes.B256Zero, nil } func (b *BlockExecutor) Release() { diff --git a/go.mod b/go.mod index 12e8ef39ca4..74aa01a0059 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.25.7 replace github.com/holiman/bloomfilter/v2 => github.com/AskAlexSharov/bloomfilter/v2 v2.0.9 -replace github.com/Giulio2002/gevm => github.com/Giulio2002/gevm v0.0.0-20260508142314-92fa74d27f4c +replace github.com/Giulio2002/gevm => github.com/Giulio2002/gevm v0.0.0-20260508150222-5376a47e22e4 require ( github.com/Giulio2002/gevm v0.0.0-00010101000000-000000000000 diff --git a/go.sum b/go.sum index 45ea431cca0..67f385115b0 100644 --- a/go.sum +++ b/go.sum @@ -57,8 +57,8 @@ github.com/FastFilter/xorfilter v0.5.1 h1:UjtPttI1SnKWUmeQKu8Y+4ZvMJv7KP6/NZTsGB github.com/FastFilter/xorfilter v0.5.1/go.mod h1:h+9l02/leuyyhepO30BKr25MkZdy7LHcfPRBDRuflXw= github.com/Giulio2002/fastkeccak v0.0.0-20260211162430-e46f5da3471e h1:RrlPQT2lAmlGWldQ7PIhmxnusr7V5cFXynPWhktgP30= github.com/Giulio2002/fastkeccak v0.0.0-20260211162430-e46f5da3471e/go.mod h1:c8xKFYwwaqZu58HExXsizNY59YXkX8jWnpG8Llk9wZU= -github.com/Giulio2002/gevm v0.0.0-20260508142314-92fa74d27f4c h1:pAg2GYjRqODVzg0cKHGFJL7IbEeXuy2I8R/V7h1yoXI= -github.com/Giulio2002/gevm v0.0.0-20260508142314-92fa74d27f4c/go.mod h1:Mz4nQgnOmhniJ+JZmB7UhPJOxT0eEqtbZxpbt6u3vP0= +github.com/Giulio2002/gevm v0.0.0-20260508150222-5376a47e22e4 h1:v1aOGtlTnMMJIk/G21HlL4DLH0CT+OaICaw4ZdI3W/Q= +github.com/Giulio2002/gevm v0.0.0-20260508150222-5376a47e22e4/go.mod h1:Mz4nQgnOmhniJ+JZmB7UhPJOxT0eEqtbZxpbt6u3vP0= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=