From 204a411d773d983cd52e4c7444141ee4a6664272 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Mon, 15 Jun 2026 11:27:57 +0800 Subject: [PATCH 01/29] scaffold evm-only giga executor path --- giga/evmonly/README.md | 86 ++++++++++++++++++++++++++ giga/evmonly/precompiles/context.go | 67 +++++++++++++++++++++ giga/evmonly/seiv3/config.go | 29 +++++++++ giga/evmonly/seiv3/executor.go | 38 ++++++++++++ giga/evmonly/seiv3/executor_test.go | 31 ++++++++++ giga/evmonly/seiv3/runtime.go | 7 +++ giga/evmonly/types.go | 93 +++++++++++++++++++++++++++++ 7 files changed, 351 insertions(+) create mode 100644 giga/evmonly/README.md create mode 100644 giga/evmonly/precompiles/context.go create mode 100644 giga/evmonly/seiv3/config.go create mode 100644 giga/evmonly/seiv3/executor.go create mode 100644 giga/evmonly/seiv3/executor_test.go create mode 100644 giga/evmonly/seiv3/runtime.go create mode 100644 giga/evmonly/types.go diff --git a/giga/evmonly/README.md b/giga/evmonly/README.md new file mode 100644 index 0000000000..231a6d9d65 --- /dev/null +++ b/giga/evmonly/README.md @@ -0,0 +1,86 @@ +# EVM-only execution scaffold + +This package sketches the EVM-only execution boundary for the final-form giga +path. It is intentionally separate from the current Cosmos-backed giga wiring in +`app/app.go`. + +The target execution model is based on the `sei-v3` executor: + +- raw transaction bytes are Ethereum RLP transactions, not Cosmos SDK txs +- state layout is EVM-native: balance, storage, code, and nonce are keyed by + EVM addresses and hashes +- block execution can overlap with parsing or persistence work for nearby blocks +- hot execution should not allocate `sdk.Context`, `sdk.Tx`, + `MsgEVMTransaction`, Cosmos messages, or Cosmos coins + +Custom precompiles are intentionally not implemented in this scaffold. The open +work is to port them behind an EVM-native context that is visible to the +executor's conflict tracking without reintroducing Cosmos keeper dependencies. + +## Executor interface + +The boundary is: + +```go +ExecuteBlock(context.Context, BlockRequest) (*BlockResult, error) +``` + +The executor should be commit-neutral. It executes an ordered EVM block and +returns the state writes and receipts produced by that block. The caller owns +durable persistence, state commitment, block indexing, and receipt publication. + +## Input block format + +`BlockRequest` is the expected input: + +- `Context` contains block-constant EVM fields: + - block number + - timestamp + - block gas limit + - chain ID + - base fee + - blob base fee, when enabled + - coinbase + - parent hash + - current block hash + - prevRandao +- `Txs` is the canonical, already-ordered transaction list for the block. +- Each tx is raw Ethereum transaction RLP bytes. +- There is no Cosmos SDK tx envelope, `MsgEVMTransaction`, ante wrapper, memo, + fee object, account address mapping object, or Cosmos gas meter in the input. + +The runtime or consensus layer is responsible for choosing the block order and +providing the block context. The executor is responsible for parsing tx RLP, +recovering senders, validating EVM nonce/fee/intrinsic-gas rules, executing EVM +state transitions, and producing deterministic outputs. + +## Output format + +`BlockResult` contains two primary outputs: + +- `ChangeSet`: the EVM-native state writes produced by the block. +- `Receipts`: Ethereum receipts for the executed transactions. + +`ChangeSet` is expressed as post-block values, not deltas: + +- `Balances`: final balance for each changed EVM address +- `Nonces`: final nonce for each changed EVM address +- `Code`: final bytecode updates or deletions +- `Storage`: final storage slot updates or deletions + +The changeset should be deterministic and serializable by the runtime layer. +The executor should not require `sdk.Context` or Cosmos stores to build it. + +`Receipts` are emitted in transaction order and should contain the Ethereum +receipt fields needed by RPC and indexing: status, cumulative gas used, bloom, +logs, tx hash, contract address, and effective gas price metadata where needed. + +`Txs` carries per-transaction execution metadata used to build or enrich +receipts and RPC responses. `GasUsed` is the total EVM gas consumed by the block. + +## Open precompile work + +Native custom precompiles still need a separate design. If they introduce state +outside balance, nonce, code, and storage, that state must either become part of +the EVM-native changeset or be represented through an explicit extension that is +visible to the OCC conflict tracker. diff --git a/giga/evmonly/precompiles/context.go b/giga/evmonly/precompiles/context.go new file mode 100644 index 0000000000..8ab405ad00 --- /dev/null +++ b/giga/evmonly/precompiles/context.go @@ -0,0 +1,67 @@ +package precompiles + +import ( + "errors" + "math/big" + + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" +) + +var ErrCustomPrecompilesOpen = errors.New("evm-only custom precompiles are not implemented") + +// Registry resolves native custom precompiles for the EVM-only path. +type Registry interface { + Get(common.Address) (Contract, bool) +} + +// Contract is the sdk.Context-free custom precompile interface. +type Contract interface { + RequiredGas(input []byte) uint64 + Run(*Context, []byte) ([]byte, error) +} + +// Context is the only execution context custom precompiles should receive in +// the EVM-only path. It deliberately excludes sdk.Context and Cosmos keepers. +type Context struct { + Caller common.Address + Address common.Address + ApparentValue *big.Int + ReadOnly bool + DelegateCall bool + GasRemaining uint64 + Block BlockContext + State State + Logs LogSink +} + +// BlockContext is the block data custom precompiles may read. +type BlockContext struct { + Number uint64 + Time uint64 + ChainID *big.Int + BaseFee *big.Int + BlobBaseFee *big.Int + Coinbase common.Address + PrevRandao common.Hash +} + +// State is the precompile-facing state API. Implementations must make these +// reads and writes visible to the executor's conflict tracking. +type State interface { + GetBalance(common.Address) *big.Int + AddBalance(common.Address, *big.Int) + SubBalance(common.Address, *big.Int) error + GetNonce(common.Address) uint64 + SetNonce(common.Address, uint64) + GetCode(common.Address) []byte + GetState(common.Address, common.Hash) common.Hash + SetState(common.Address, common.Hash, common.Hash) + GetCustom([]byte) ([]byte, bool) + SetCustom([]byte, []byte) +} + +// LogSink lets custom precompiles emit Ethereum logs without Cosmos events. +type LogSink interface { + AddLog(*ethtypes.Log) +} diff --git a/giga/evmonly/seiv3/config.go b/giga/evmonly/seiv3/config.go new file mode 100644 index 0000000000..034b8aba9c --- /dev/null +++ b/giga/evmonly/seiv3/config.go @@ -0,0 +1,29 @@ +package seiv3 + +import "math" + +// Config captures the sei-v3 executor knobs needed by the EVM-only path. +type Config struct { + OCCWorkers int + FlushBatchSize int + DisableNonceCheck bool + DisableGasPriceCheck bool +} + +func DefaultConfig() Config { + return Config{ + OCCWorkers: int(math.Min(12, float64(runtimeCPU()))), + FlushBatchSize: 100, + } +} + +func (c Config) WithDefaults() Config { + defaults := DefaultConfig() + if c.OCCWorkers == 0 { + c.OCCWorkers = defaults.OCCWorkers + } + if c.FlushBatchSize == 0 { + c.FlushBatchSize = defaults.FlushBatchSize + } + return c +} diff --git a/giga/evmonly/seiv3/executor.go b/giga/evmonly/seiv3/executor.go new file mode 100644 index 0000000000..cf0331f52a --- /dev/null +++ b/giga/evmonly/seiv3/executor.go @@ -0,0 +1,38 @@ +package seiv3 + +import ( + "context" + "fmt" + + "github.com/sei-protocol/sei-chain/giga/evmonly" +) + +// Executor is the placeholder for the sei-v3-derived EVM-only executor. +type Executor struct { + cfg Config +} + +func NewExecutor(cfg Config) *Executor { + return &Executor{cfg: cfg.WithDefaults()} +} + +func (e *Executor) Config() Config { + return e.cfg +} + +func (e *Executor) ExecuteBlock(ctx context.Context, req evmonly.BlockRequest) (*evmonly.BlockResult, error) { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + if len(req.Txs) == 0 { + return &evmonly.BlockResult{}, nil + } + + return nil, fmt.Errorf( + "%w: port sei-v3 parser, EVMC host context, OCC scheduler, EVM-native stores, and receipt pipeline", + evmonly.ErrNotImplemented, + ) +} diff --git a/giga/evmonly/seiv3/executor_test.go b/giga/evmonly/seiv3/executor_test.go new file mode 100644 index 0000000000..580321cb3c --- /dev/null +++ b/giga/evmonly/seiv3/executor_test.go @@ -0,0 +1,31 @@ +package seiv3 + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/sei-protocol/sei-chain/giga/evmonly" +) + +func TestExecutorEmptyBlock(t *testing.T) { + executor := NewExecutor(Config{OCCWorkers: 1}) + + result, err := executor.ExecuteBlock(context.Background(), evmonly.BlockRequest{}) + + require.NoError(t, err) + require.NotNil(t, result) +} + +func TestExecutorTxsRemainScaffoldOnly(t *testing.T) { + executor := NewExecutor(Config{OCCWorkers: 1}) + + _, err := executor.ExecuteBlock(context.Background(), evmonly.BlockRequest{ + Txs: [][]byte{{0x01}}, + }) + + require.Error(t, err) + require.True(t, errors.Is(err, evmonly.ErrNotImplemented)) +} diff --git a/giga/evmonly/seiv3/runtime.go b/giga/evmonly/seiv3/runtime.go new file mode 100644 index 0000000000..cd5d04da9b --- /dev/null +++ b/giga/evmonly/seiv3/runtime.go @@ -0,0 +1,7 @@ +package seiv3 + +import "runtime" + +func runtimeCPU() int { + return runtime.NumCPU() +} diff --git a/giga/evmonly/types.go b/giga/evmonly/types.go new file mode 100644 index 0000000000..c7c835b3ad --- /dev/null +++ b/giga/evmonly/types.go @@ -0,0 +1,93 @@ +package evmonly + +import ( + "context" + "errors" + "math/big" + + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" +) + +var ErrNotImplemented = errors.New("evm-only executor is not implemented") + +// Executor is the Cosmos-free block execution boundary for the EVM-only path. +type Executor interface { + ExecuteBlock(context.Context, BlockRequest) (*BlockResult, error) +} + +// BlockRequest contains all consensus/runtime inputs needed to execute a block. +// Txs must be raw Ethereum transaction RLP bytes. +type BlockRequest struct { + Context BlockContext + Txs [][]byte +} + +// BlockContext contains block-constant EVM execution data. +type BlockContext struct { + Number uint64 + Time uint64 + GasLimit uint64 + ChainID *big.Int + BaseFee *big.Int + BlobBaseFee *big.Int + Coinbase common.Address + ParentHash common.Hash + BlockHash common.Hash + PrevRandao common.Hash +} + +// BlockResult is the executor output consumed by the new runtime boundary. +type BlockResult struct { + ChangeSet StateChangeSet + Txs []TxResult + Receipts ethtypes.Receipts + GasUsed uint64 +} + +// StateChangeSet is the deterministic EVM-native state output for a block. +// Values are post-block values, not deltas. +type StateChangeSet struct { + Balances []BalanceChange + Nonces []NonceChange + Code []CodeChange + Storage []StorageChange +} + +type BalanceChange struct { + Address common.Address + Balance *big.Int +} + +type NonceChange struct { + Address common.Address + Nonce uint64 +} + +type CodeChange struct { + Address common.Address + Code []byte + Delete bool +} + +type StorageChange struct { + Address common.Address + Key common.Hash + Value common.Hash + Delete bool +} + +// TxResult is the minimum per-transaction output needed for receipts, RPC, and +// runtime result reporting. +type TxResult struct { + Hash common.Hash + Sender common.Address + To *common.Address + ContractAddress common.Address + Status uint64 + GasUsed uint64 + CumulativeGasUsed uint64 + EffectiveGasPrice *big.Int + Logs []*ethtypes.Log + Err error +} From 7314cc27e8a5b1ad5aacf5b297a0d36d686030a3 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Mon, 15 Jun 2026 13:38:30 +0800 Subject: [PATCH 02/29] port native evm-only giga executor --- giga/evmonly/README.md | 33 +- giga/evmonly/precompiles/context.go | 1 + giga/evmonly/seiv3/config.go | 16 +- giga/evmonly/seiv3/executor.go | 262 ++++++++++- giga/evmonly/seiv3/executor_test.go | 100 ++++- giga/evmonly/seiv3/parser.go | 44 ++ giga/evmonly/seiv3/state_db.go | 649 ++++++++++++++++++++++++++++ giga/evmonly/state.go | 171 ++++++++ 8 files changed, 1251 insertions(+), 25 deletions(-) create mode 100644 giga/evmonly/seiv3/parser.go create mode 100644 giga/evmonly/seiv3/state_db.go create mode 100644 giga/evmonly/state.go diff --git a/giga/evmonly/README.md b/giga/evmonly/README.md index 231a6d9d65..75aa00d30b 100644 --- a/giga/evmonly/README.md +++ b/giga/evmonly/README.md @@ -1,6 +1,6 @@ -# EVM-only execution scaffold +# EVM-only giga execution -This package sketches the EVM-only execution boundary for the final-form giga +This package contains the EVM-only execution boundary for the final-form giga path. It is intentionally separate from the current Cosmos-backed giga wiring in `app/app.go`. @@ -13,9 +13,11 @@ The target execution model is based on the `sei-v3` executor: - hot execution should not allocate `sdk.Context`, `sdk.Tx`, `MsgEVMTransaction`, Cosmos messages, or Cosmos coins -Custom precompiles are intentionally not implemented in this scaffold. The open -work is to port them behind an EVM-native context that is visible to the -executor's conflict tracking without reintroducing Cosmos keeper dependencies. +The current port executes raw RLP transactions with go-ethereum against an +EVM-native state backend, then returns a changeset plus Ethereum receipts. +Custom precompiles are still placeholders. The open work is to port them behind +an EVM-native context that is visible to the executor's conflict tracking +without reintroducing Cosmos keeper dependencies. ## Executor interface @@ -28,6 +30,9 @@ ExecuteBlock(context.Context, BlockRequest) (*BlockResult, error) The executor should be commit-neutral. It executes an ordered EVM block and returns the state writes and receipts produced by that block. The caller owns durable persistence, state commitment, block indexing, and receipt publication. +The `seiv3` implementation accepts a `StateReader` backend through +`WithState(...)`; callers can persist the returned `ChangeSet` with a matching +`StateWriter`. ## Input block format @@ -84,3 +89,21 @@ Native custom precompiles still need a separate design. If they introduce state outside balance, nonce, code, and storage, that state must either become part of the EVM-native changeset or be represented through an explicit extension that is visible to the OCC conflict tracker. + +The intended direction is to treat each custom precompile's migrated module +state as contract storage owned by that precompile address. With no range reads +and no side state, precompile reads and writes can then flow through ordinary +`(address, slot)` storage tracking. + +Until that design is implemented, the `seiv3` executor accepts a custom +precompile registry only as a fail-closed placeholder. Calls to registered +custom precompile addresses return `ErrCustomPrecompilesOpen`. + +## Current limitations + +- The current port is sequential. The EVM-native state boundary and changeset + shape are intended to be replaceable with the sei-v3 OCC scheduler/store. +- State input is key-addressable only. The executor lazily reads storage slots + by `(address, slot)` and does not require or expose range iteration. +- The map-backed `MemoryState` is for tests and early integration; production + should provide a durable native state backend. diff --git a/giga/evmonly/precompiles/context.go b/giga/evmonly/precompiles/context.go index 8ab405ad00..828937d6b1 100644 --- a/giga/evmonly/precompiles/context.go +++ b/giga/evmonly/precompiles/context.go @@ -13,6 +13,7 @@ var ErrCustomPrecompilesOpen = errors.New("evm-only custom precompiles are not i // Registry resolves native custom precompiles for the EVM-only path. type Registry interface { Get(common.Address) (Contract, bool) + Addresses() []common.Address } // Contract is the sdk.Context-free custom precompile interface. diff --git a/giga/evmonly/seiv3/config.go b/giga/evmonly/seiv3/config.go index 034b8aba9c..e19cafbd2b 100644 --- a/giga/evmonly/seiv3/config.go +++ b/giga/evmonly/seiv3/config.go @@ -1,6 +1,13 @@ package seiv3 -import "math" +import ( + "math" + "math/big" + + "github.com/ethereum/go-ethereum/params" + + "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles" +) // Config captures the sei-v3 executor knobs needed by the EVM-only path. type Config struct { @@ -8,12 +15,16 @@ type Config struct { FlushBatchSize int DisableNonceCheck bool DisableGasPriceCheck bool + MinGasPrice *big.Int + ChainConfig *params.ChainConfig + CustomPrecompiles precompiles.Registry } func DefaultConfig() Config { return Config{ OCCWorkers: int(math.Min(12, float64(runtimeCPU()))), FlushBatchSize: 100, + MinGasPrice: big.NewInt(1_000_000_000), } } @@ -25,5 +36,8 @@ func (c Config) WithDefaults() Config { if c.FlushBatchSize == 0 { c.FlushBatchSize = defaults.FlushBatchSize } + if c.MinGasPrice == nil { + c.MinGasPrice = new(big.Int).Set(defaults.MinGasPrice) + } return c } diff --git a/giga/evmonly/seiv3/executor.go b/giga/evmonly/seiv3/executor.go index cf0331f52a..52f2715e63 100644 --- a/giga/evmonly/seiv3/executor.go +++ b/giga/evmonly/seiv3/executor.go @@ -3,17 +3,45 @@ package seiv3 import ( "context" "fmt" + "math" + "math/big" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/tracing" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/params" "github.com/sei-protocol/sei-chain/giga/evmonly" + "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles" ) -// Executor is the placeholder for the sei-v3-derived EVM-only executor. +// Executor runs raw EVM transactions against an EVM-native state backend. type Executor struct { - cfg Config + cfg Config + state evmonly.StateReader } -func NewExecutor(cfg Config) *Executor { - return &Executor{cfg: cfg.WithDefaults()} +type Option func(*Executor) + +func WithState(state evmonly.StateReader) Option { + return func(e *Executor) { + if state != nil { + e.state = state + } + } +} + +func NewExecutor(cfg Config, opts ...Option) *Executor { + e := &Executor{ + cfg: cfg.WithDefaults(), + state: evmonly.NewMemoryState(), + } + for _, opt := range opts { + opt(e) + } + return e } func (e *Executor) Config() Config { @@ -21,18 +49,224 @@ func (e *Executor) Config() Config { } func (e *Executor) ExecuteBlock(ctx context.Context, req evmonly.BlockRequest) (*evmonly.BlockResult, error) { - select { - case <-ctx.Done(): - return nil, ctx.Err() - default: - } - if len(req.Txs) == 0 { return &evmonly.BlockResult{}, nil } - return nil, fmt.Errorf( - "%w: port sei-v3 parser, EVMC host context, OCC scheduler, EVM-native stores, and receipt pipeline", - evmonly.ErrNotImplemented, - ) + chainConfig := e.chainConfig(req.Context) + signer := ethtypes.MakeSigner(chainConfig, new(big.Int).SetUint64(req.Context.Number), req.Context.Time) + parsed, err := parseBlockTxs(ctx, req.Txs, signer) + if err != nil { + return nil, err + } + + stateDB := newNativeStateDB(e.state) + blockCtx := buildBlockContext(req.Context) + evm := vm.NewEVM(blockCtx, stateDB, chainConfig, vm.Config{}, customPrecompileMap(e.cfg.CustomPrecompiles)) + stateDB.SetEVM(evm) + + gasLimit := req.Context.GasLimit + if gasLimit == 0 { + gasLimit = math.MaxUint64 + } + gasPool := new(core.GasPool).AddGas(gasLimit) + baseFee := cloneBig(req.Context.BaseFee) + + result := &evmonly.BlockResult{ + Txs: make([]evmonly.TxResult, 0, len(parsed)), + Receipts: make(ethtypes.Receipts, 0, len(parsed)), + } + for txIndex, p := range parsed { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + txResult, receipt, err := e.executeTx(evm, stateDB, gasPool, req.Context, p, txIndex, baseFee) + if err != nil { + return nil, fmt.Errorf("execute tx %d %s: %w", txIndex, p.tx.Hash(), err) + } + txResult.CumulativeGasUsed = result.GasUsed + txResult.GasUsed + receipt.CumulativeGasUsed = txResult.CumulativeGasUsed + result.Txs = append(result.Txs, txResult) + result.Receipts = append(result.Receipts, receipt) + result.GasUsed += txResult.GasUsed + } + stateDB.Finalise(true) + result.ChangeSet = stateDB.ChangeSet() + return result, nil } + +func (e *Executor) executeTx( + evm *vm.EVM, + stateDB *nativeStateDB, + gasPool *core.GasPool, + block evmonly.BlockContext, + p parsedTx, + txIndex int, + baseFee *big.Int, +) (evmonly.TxResult, *ethtypes.Receipt, error) { + tx := p.tx + if e.cfg.CustomPrecompiles != nil && tx.To() != nil { + if _, ok := e.cfg.CustomPrecompiles.Get(*tx.To()); ok { + return evmonly.TxResult{Hash: tx.Hash(), Sender: p.sender, To: tx.To(), Err: precompiles.ErrCustomPrecompilesOpen}, + nil, + precompiles.ErrCustomPrecompilesOpen + } + } + if !e.cfg.DisableGasPriceCheck && e.cfg.MinGasPrice != nil { + if effectiveGasPrice(tx, baseFee).Cmp(e.cfg.MinGasPrice) < 0 { + return evmonly.TxResult{Hash: tx.Hash(), Sender: p.sender, To: tx.To(), Err: errInsufficientGasPrice}, + nil, + errInsufficientGasPrice + } + } + + msg, err := core.TransactionToMessage(tx, ethtypes.MakeSigner(e.chainConfig(block), new(big.Int).SetUint64(block.Number), block.Time), baseFee) + if err != nil { + return evmonly.TxResult{Hash: tx.Hash(), Sender: p.sender, To: tx.To(), Err: err}, nil, err + } + msg.SkipNonceChecks = e.cfg.DisableNonceCheck + + stateDB.SetTxContext(tx.Hash(), txIndex) + logStart := len(stateDB.logs) + evm.SetTxContext(core.NewEVMTxContext(msg)) + execResult, err := core.ApplyMessage(evm, msg, gasPool) + if err != nil { + return evmonly.TxResult{Hash: tx.Hash(), Sender: p.sender, To: tx.To(), Err: err}, nil, err + } + + txLogs := append([]*ethtypes.Log(nil), stateDB.logs[logStart:]...) + for _, log := range txLogs { + log.BlockNumber = block.Number + log.BlockHash = block.BlockHash + log.TxHash = tx.Hash() + log.TxIndex = uint(txIndex) + } + + status := ethtypes.ReceiptStatusSuccessful + if execResult.Failed() { + status = ethtypes.ReceiptStatusFailed + } + receipt := ðtypes.Receipt{ + Type: tx.Type(), + Status: status, + Logs: txLogs, + TxHash: tx.Hash(), + GasUsed: execResult.UsedGas, + EffectiveGasPrice: effectiveGasPrice(tx, baseFee), + BlockHash: block.BlockHash, + BlockNumber: new(big.Int).SetUint64(block.Number), + TransactionIndex: uint(txIndex), + } + if tx.To() == nil { + receipt.ContractAddress = crypto.CreateAddress(p.sender, tx.Nonce()) + } + receipt.Bloom = ethtypes.CreateBloom(receipt) + + txResult := evmonly.TxResult{ + Hash: tx.Hash(), + Sender: p.sender, + To: tx.To(), + ContractAddress: receipt.ContractAddress, + Status: status, + GasUsed: execResult.UsedGas, + EffectiveGasPrice: new(big.Int).Set(receipt.EffectiveGasPrice), + Logs: txLogs, + Err: execResult.Err, + } + return txResult, receipt, nil +} + +func buildBlockContext(ctx evmonly.BlockContext) vm.BlockContext { + prevRandao := ctx.PrevRandao + baseFee := cloneBig(ctx.BaseFee) + blobBaseFee := cloneBig(ctx.BlobBaseFee) + gasLimit := ctx.GasLimit + if gasLimit == 0 { + gasLimit = math.MaxUint64 + } + return vm.BlockContext{ + CanTransfer: core.CanTransfer, + Transfer: core.Transfer, + GetHash: func(n uint64) common.Hash { + switch { + case n == ctx.Number: + return ctx.BlockHash + case ctx.Number > 0 && n == ctx.Number-1: + return ctx.ParentHash + default: + return common.Hash{} + } + }, + Coinbase: ctx.Coinbase, + GasLimit: gasLimit, + BlockNumber: new(big.Int).SetUint64(ctx.Number), + Time: ctx.Time, + Difficulty: new(big.Int), + BaseFee: baseFee, + BlobBaseFee: blobBaseFee, + Random: &prevRandao, + } +} + +type unresolvedCustomPrecompile struct{} + +func (unresolvedCustomPrecompile) RequiredGas([]byte) uint64 { + return 0 +} + +func (unresolvedCustomPrecompile) Run(*vm.EVM, common.Address, common.Address, []byte, *big.Int, bool, bool, *tracing.Hooks) ([]byte, error) { + return nil, precompiles.ErrCustomPrecompilesOpen +} + +func customPrecompileMap(registry precompiles.Registry) map[common.Address]vm.PrecompiledContract { + if registry == nil { + return nil + } + addresses := registry.Addresses() + if len(addresses) == 0 { + return nil + } + contracts := make(map[common.Address]vm.PrecompiledContract, len(addresses)) + for _, addr := range addresses { + contracts[addr] = unresolvedCustomPrecompile{} + } + return contracts +} + +func (e *Executor) chainConfig(ctx evmonly.BlockContext) *params.ChainConfig { + var cfg params.ChainConfig + if e.cfg.ChainConfig != nil { + cfg = *e.cfg.ChainConfig + } else { + cfg = *params.AllDevChainProtocolChanges + } + if ctx.ChainID != nil { + cfg.ChainID = new(big.Int).Set(ctx.ChainID) + } else if cfg.ChainID != nil { + cfg.ChainID = new(big.Int).Set(cfg.ChainID) + } else { + cfg.ChainID = big.NewInt(1) + } + return &cfg +} + +func effectiveGasPrice(tx *ethtypes.Transaction, baseFee *big.Int) *big.Int { + if baseFee == nil { + return tx.GasPrice() + } + if tx.Type() == ethtypes.DynamicFeeTxType || tx.Type() == ethtypes.BlobTxType || tx.Type() == ethtypes.SetCodeTxType { + return new(big.Int).Add(baseFee, tx.EffectiveGasTipValue(baseFee)) + } + return tx.GasPrice() +} + +func cloneBig(v *big.Int) *big.Int { + if v == nil { + return new(big.Int) + } + return new(big.Int).Set(v) +} + +var errInsufficientGasPrice = fmt.Errorf("insufficient gas price") diff --git a/giga/evmonly/seiv3/executor_test.go b/giga/evmonly/seiv3/executor_test.go index 580321cb3c..6664103fae 100644 --- a/giga/evmonly/seiv3/executor_test.go +++ b/giga/evmonly/seiv3/executor_test.go @@ -2,12 +2,18 @@ package seiv3 import ( "context" + "crypto/ecdsa" "errors" + "math/big" "testing" + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" "github.com/stretchr/testify/require" "github.com/sei-protocol/sei-chain/giga/evmonly" + "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles" ) func TestExecutorEmptyBlock(t *testing.T) { @@ -19,13 +25,97 @@ func TestExecutorEmptyBlock(t *testing.T) { require.NotNil(t, result) } -func TestExecutorTxsRemainScaffoldOnly(t *testing.T) { - executor := NewExecutor(Config{OCCWorkers: 1}) +func TestExecutorTransferTx(t *testing.T) { + chainID := big.NewInt(713715) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + recipient := common.HexToAddress("0x00000000000000000000000000000000000000a1") + + state := evmonly.NewMemoryState() + state.SetBalance(sender, big.NewInt(200_000_000_000_000)) + + rawTx := signLegacyTx(t, key, chainID, 0, &recipient, big.NewInt(7), nil) + executor := NewExecutor(Config{OCCWorkers: 1}, WithState(state)) + + result, err := executor.ExecuteBlock(context.Background(), evmonly.BlockRequest{ + Context: blockContext(chainID), + Txs: [][]byte{rawTx}, + }) + + require.NoError(t, err) + require.Len(t, result.Txs, 1) + require.Len(t, result.Receipts, 1) + require.Equal(t, ethtypes.ReceiptStatusSuccessful, result.Txs[0].Status) + require.Equal(t, uint64(21_000), result.GasUsed) + require.NotEmpty(t, result.ChangeSet.Balances) + + state.ApplyChangeSet(result.ChangeSet) + require.Equal(t, big.NewInt(7), state.GetBalance(recipient)) + require.Equal(t, uint64(1), state.GetNonce(sender)) +} + +func TestExecutorCustomPrecompilePlaceholder(t *testing.T) { + chainID := big.NewInt(713715) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + customAddr := common.HexToAddress("0x0000000000000000000000000000000000001001") + + state := evmonly.NewMemoryState() + state.SetBalance(sender, big.NewInt(200_000_000_000_000)) + + rawTx := signLegacyTx(t, key, chainID, 0, &customAddr, big.NewInt(0), []byte{0x01}) + executor := NewExecutor(Config{ + OCCWorkers: 1, + CustomPrecompiles: staticPrecompileRegistry{addr: customAddr}, + }, WithState(state)) - _, err := executor.ExecuteBlock(context.Background(), evmonly.BlockRequest{ - Txs: [][]byte{{0x01}}, + _, err = executor.ExecuteBlock(context.Background(), evmonly.BlockRequest{ + Context: blockContext(chainID), + Txs: [][]byte{rawTx}, }) require.Error(t, err) - require.True(t, errors.Is(err, evmonly.ErrNotImplemented)) + require.True(t, errors.Is(err, precompiles.ErrCustomPrecompilesOpen)) +} + +func signLegacyTx(t *testing.T, key *ecdsa.PrivateKey, chainID *big.Int, nonce uint64, to *common.Address, value *big.Int, data []byte) []byte { + t.Helper() + tx := ethtypes.NewTx(ðtypes.LegacyTx{ + Nonce: nonce, + GasPrice: big.NewInt(1_000_000_000), + Gas: 100_000, + To: to, + Value: value, + Data: data, + }) + signed, err := ethtypes.SignTx(tx, ethtypes.LatestSignerForChainID(chainID), key) + require.NoError(t, err) + raw, err := signed.MarshalBinary() + require.NoError(t, err) + return raw +} + +func blockContext(chainID *big.Int) evmonly.BlockContext { + return evmonly.BlockContext{ + Number: 1, + Time: 1, + GasLimit: 30_000_000, + ChainID: chainID, + BaseFee: big.NewInt(0), + Coinbase: common.HexToAddress("0x00000000000000000000000000000000000000cb"), + } +} + +type staticPrecompileRegistry struct { + addr common.Address +} + +func (r staticPrecompileRegistry) Get(addr common.Address) (precompiles.Contract, bool) { + return nil, addr == r.addr +} + +func (r staticPrecompileRegistry) Addresses() []common.Address { + return []common.Address{r.addr} } diff --git a/giga/evmonly/seiv3/parser.go b/giga/evmonly/seiv3/parser.go new file mode 100644 index 0000000000..76a4da311a --- /dev/null +++ b/giga/evmonly/seiv3/parser.go @@ -0,0 +1,44 @@ +package seiv3 + +import ( + "context" + "fmt" + + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/rlp" +) + +type parsedTx struct { + tx *ethtypes.Transaction + sender common.Address +} + +func parseBlockTxs(ctx context.Context, txs [][]byte, signer ethtypes.Signer) ([]parsedTx, error) { + parsed := make([]parsedTx, len(txs)) + for i, raw := range txs { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + tx, sender, err := parseTx(raw, signer) + if err != nil { + return nil, fmt.Errorf("parse tx %d: %w", i, err) + } + parsed[i] = parsedTx{tx: tx, sender: sender} + } + return parsed, nil +} + +func parseTx(raw []byte, signer ethtypes.Signer) (*ethtypes.Transaction, common.Address, error) { + var tx ethtypes.Transaction + if err := rlp.DecodeBytes(raw, &tx); err != nil { + return nil, common.Address{}, err + } + sender, err := ethtypes.Sender(signer, &tx) + if err != nil { + return nil, common.Address{}, err + } + return &tx, sender, nil +} diff --git a/giga/evmonly/seiv3/state_db.go b/giga/evmonly/seiv3/state_db.go new file mode 100644 index 0000000000..a641ea7f10 --- /dev/null +++ b/giga/evmonly/seiv3/state_db.go @@ -0,0 +1,649 @@ +package seiv3 + +import ( + "bytes" + "errors" + "math/big" + "sort" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/stateless" + "github.com/ethereum/go-ethereum/core/tracing" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/params" + ethutils "github.com/ethereum/go-ethereum/trie/utils" + "github.com/holiman/uint256" + + "github.com/sei-protocol/sei-chain/giga/evmonly" +) + +var errInsufficientBalance = errors.New("insufficient balance") + +type nativeStateDB struct { + source evmonly.StateReader + + accounts map[common.Address]*nativeAccount + base map[common.Address]*nativeAccount + + refund uint64 + logs []*ethtypes.Log + preimages map[common.Hash][]byte + + accessList accessList + transientStates map[common.Address]map[common.Hash]common.Hash + snapshots []nativeSnapshot + + txHash common.Hash + txIndex int + err error + evm *vm.EVM +} + +type nativeAccount struct { + Balance *uint256.Int + Nonce uint64 + Code []byte + Storage map[common.Hash]common.Hash + SelfDestructed bool + Created bool +} + +type nativeSnapshot struct { + accounts map[common.Address]*nativeAccount + refund uint64 + logs []*ethtypes.Log + accessList accessList + transientStates map[common.Address]map[common.Hash]common.Hash + preimages map[common.Hash][]byte + err error +} + +type accessList struct { + addresses map[common.Address]struct{} + slots map[common.Address]map[common.Hash]struct{} +} + +func newNativeStateDB(source evmonly.StateReader) *nativeStateDB { + if source == nil { + source = evmonly.NewMemoryState() + } + return &nativeStateDB{ + source: source, + accounts: map[common.Address]*nativeAccount{}, + base: map[common.Address]*nativeAccount{}, + preimages: map[common.Hash][]byte{}, + accessList: newAccessList(), + transientStates: map[common.Address]map[common.Hash]common.Hash{}, + } +} + +func (s *nativeStateDB) ChangeSet() evmonly.StateChangeSet { + addresses := make([]common.Address, 0, len(s.accounts)) + for addr := range s.accounts { + addresses = append(addresses, addr) + } + sort.Slice(addresses, func(i, j int) bool { + return bytes.Compare(addresses[i][:], addresses[j][:]) < 0 + }) + + var changes evmonly.StateChangeSet + for _, addr := range addresses { + acct := s.accounts[addr] + base := s.baseAccount(addr) + + if !acct.Balance.Eq(base.Balance) { + changes.Balances = append(changes.Balances, evmonly.BalanceChange{ + Address: addr, + Balance: acct.Balance.ToBig(), + }) + } + if acct.Nonce != base.Nonce { + changes.Nonces = append(changes.Nonces, evmonly.NonceChange{ + Address: addr, + Nonce: acct.Nonce, + }) + } + if !bytes.Equal(acct.Code, base.Code) { + changes.Code = append(changes.Code, evmonly.CodeChange{ + Address: addr, + Code: cloneBytes(acct.Code), + Delete: len(acct.Code) == 0, + }) + } + storageKeys := storageKeyUnion(base.Storage, acct.Storage) + for _, key := range storageKeys { + oldValue := base.Storage[key] + newValue := acct.Storage[key] + if oldValue == newValue { + continue + } + changes.Storage = append(changes.Storage, evmonly.StorageChange{ + Address: addr, + Key: key, + Value: newValue, + Delete: newValue == (common.Hash{}), + }) + } + } + return changes +} + +func (s *nativeStateDB) CreateAccount(addr common.Address) { + acct := s.account(addr) + balance := acct.Balance.Clone() + *acct = nativeAccount{ + Balance: balance, + Storage: map[common.Hash]common.Hash{}, + Created: true, + } +} + +func (s *nativeStateDB) CreateContract(addr common.Address) { + s.account(addr).Created = true +} + +func (s *nativeStateDB) SubBalance(addr common.Address, amount *uint256.Int, _ tracing.BalanceChangeReason) uint256.Int { + prev := *s.GetBalance(addr) + if amount == nil || amount.IsZero() { + return prev + } + acct := s.account(addr) + if acct.Balance.Cmp(amount) < 0 { + s.err = errInsufficientBalance + acct.Balance.Clear() + return prev + } + acct.Balance.Sub(acct.Balance, amount) + return prev +} + +func (s *nativeStateDB) AddBalance(addr common.Address, amount *uint256.Int, _ tracing.BalanceChangeReason) uint256.Int { + prev := *s.GetBalance(addr) + if amount == nil || amount.IsZero() { + return prev + } + acct := s.account(addr) + acct.Balance.Add(acct.Balance, amount) + return prev +} + +func (s *nativeStateDB) GetBalance(addr common.Address) *uint256.Int { + return s.account(addr).Balance.Clone() +} + +func (s *nativeStateDB) SetBalance(addr common.Address, balance *uint256.Int, _ tracing.BalanceChangeReason) { + acct := s.account(addr) + if balance == nil { + acct.Balance = uint256.NewInt(0) + return + } + acct.Balance = balance.Clone() +} + +func (s *nativeStateDB) GetNonce(addr common.Address) uint64 { + return s.account(addr).Nonce +} + +func (s *nativeStateDB) SetNonce(addr common.Address, nonce uint64, _ tracing.NonceChangeReason) { + s.account(addr).Nonce = nonce +} + +func (s *nativeStateDB) GetCodeHash(addr common.Address) common.Hash { + code := s.GetCode(addr) + if len(code) == 0 { + return common.Hash{} + } + return crypto.Keccak256Hash(code) +} + +func (s *nativeStateDB) GetCode(addr common.Address) []byte { + return cloneBytes(s.account(addr).Code) +} + +func (s *nativeStateDB) SetCode(addr common.Address, code []byte) []byte { + acct := s.account(addr) + prev := cloneBytes(acct.Code) + acct.Code = cloneBytes(code) + return prev +} + +func (s *nativeStateDB) GetCodeSize(addr common.Address) int { + return len(s.account(addr).Code) +} + +func (s *nativeStateDB) AddRefund(gas uint64) { + s.refund += gas +} + +func (s *nativeStateDB) SubRefund(gas uint64) { + if gas > s.refund { + panic("refund counter underflow") + } + s.refund -= gas +} + +func (s *nativeStateDB) GetRefund() uint64 { + return s.refund +} + +func (s *nativeStateDB) GetCommittedState(addr common.Address, key common.Hash) common.Hash { + s.ensureStorage(addr, key) + return s.baseAccount(addr).Storage[key] +} + +func (s *nativeStateDB) GetState(addr common.Address, key common.Hash) common.Hash { + s.ensureStorage(addr, key) + return s.account(addr).Storage[key] +} + +func (s *nativeStateDB) SetState(addr common.Address, key common.Hash, value common.Hash) common.Hash { + s.ensureStorage(addr, key) + acct := s.account(addr) + prev := acct.Storage[key] + if value == (common.Hash{}) { + delete(acct.Storage, key) + } else { + acct.Storage[key] = value + } + return prev +} + +func (s *nativeStateDB) SetStorage(addr common.Address, states map[common.Hash]common.Hash) { + acct := s.account(addr) + acct.Storage = map[common.Hash]common.Hash{} + for key, value := range states { + if value != (common.Hash{}) { + acct.Storage[key] = value + } + } +} + +func (s *nativeStateDB) GetStorageRoot(common.Address) common.Hash { + return common.Hash{} +} + +func (s *nativeStateDB) GetTransientState(addr common.Address, key common.Hash) common.Hash { + if states, ok := s.transientStates[addr]; ok { + return states[key] + } + return common.Hash{} +} + +func (s *nativeStateDB) SetTransientState(addr common.Address, key, value common.Hash) { + states, ok := s.transientStates[addr] + if !ok { + states = map[common.Hash]common.Hash{} + s.transientStates[addr] = states + } + if value == (common.Hash{}) { + delete(states, key) + return + } + states[key] = value +} + +func (s *nativeStateDB) SelfDestruct(addr common.Address) uint256.Int { + acct := s.account(addr) + prev := *acct.Balance.Clone() + acct.Balance.Clear() + acct.SelfDestructed = true + return prev +} + +func (s *nativeStateDB) SelfDestruct6780(addr common.Address) (uint256.Int, bool) { + if !s.account(addr).Created { + return *uint256.NewInt(0), false + } + return s.SelfDestruct(addr), true +} + +func (s *nativeStateDB) HasSelfDestructed(addr common.Address) bool { + return s.account(addr).SelfDestructed +} + +func (s *nativeStateDB) Exist(addr common.Address) bool { + acct := s.account(addr) + return acct.SelfDestructed || acct.Nonce != 0 || !acct.Balance.IsZero() || len(acct.Code) != 0 +} + +func (s *nativeStateDB) Empty(addr common.Address) bool { + acct := s.account(addr) + return acct.Nonce == 0 && acct.Balance.IsZero() && len(acct.Code) == 0 +} + +func (s *nativeStateDB) AddressInAccessList(addr common.Address) bool { + _, ok := s.accessList.addresses[addr] + return ok +} + +func (s *nativeStateDB) SlotInAccessList(addr common.Address, slot common.Hash) (bool, bool) { + _, addressOk := s.accessList.addresses[addr] + if !addressOk { + return false, false + } + slots, ok := s.accessList.slots[addr] + if !ok { + return true, false + } + _, slotOk := slots[slot] + return true, slotOk +} + +func (s *nativeStateDB) AddAddressToAccessList(addr common.Address) { + s.accessList.addresses[addr] = struct{}{} +} + +func (s *nativeStateDB) AddSlotToAccessList(addr common.Address, slot common.Hash) { + s.AddAddressToAccessList(addr) + slots, ok := s.accessList.slots[addr] + if !ok { + slots = map[common.Hash]struct{}{} + s.accessList.slots[addr] = slots + } + slots[slot] = struct{}{} +} + +func (s *nativeStateDB) Prepare(_ params.Rules, sender, coinbase common.Address, dest *common.Address, precompiles []common.Address, txAccesses ethtypes.AccessList) { + s.accessList = newAccessList() + s.AddAddressToAccessList(sender) + s.AddAddressToAccessList(coinbase) + if dest != nil { + s.AddAddressToAccessList(*dest) + } + for _, addr := range precompiles { + s.AddAddressToAccessList(addr) + } + for _, tuple := range txAccesses { + s.AddAddressToAccessList(tuple.Address) + for _, key := range tuple.StorageKeys { + s.AddSlotToAccessList(tuple.Address, key) + } + } +} + +func (s *nativeStateDB) PointCache() *ethutils.PointCache { + return nil +} + +func (s *nativeStateDB) Snapshot() int { + id := len(s.snapshots) + s.snapshots = append(s.snapshots, nativeSnapshot{ + accounts: cloneAccounts(s.accounts), + refund: s.refund, + logs: append([]*ethtypes.Log(nil), s.logs...), + accessList: cloneAccessList(s.accessList), + transientStates: cloneTransientStates(s.transientStates), + preimages: clonePreimages(s.preimages), + err: s.err, + }) + return id +} + +func (s *nativeStateDB) RevertToSnapshot(id int) { + if id < 0 || id >= len(s.snapshots) { + panic("invalid state snapshot") + } + snapshot := s.snapshots[id] + s.accounts = cloneAccounts(snapshot.accounts) + s.refund = snapshot.refund + s.logs = append([]*ethtypes.Log(nil), snapshot.logs...) + s.accessList = cloneAccessList(snapshot.accessList) + s.transientStates = cloneTransientStates(snapshot.transientStates) + s.preimages = clonePreimages(snapshot.preimages) + s.err = snapshot.err + s.snapshots = s.snapshots[:id] +} + +func (s *nativeStateDB) AddLog(log *ethtypes.Log) { + log.TxHash = s.txHash + log.TxIndex = uint(s.txIndex) + log.Index = uint(len(s.logs)) + s.logs = append(s.logs, log) +} + +func (s *nativeStateDB) AddPreimage(hash common.Hash, preimage []byte) { + s.preimages[hash] = cloneBytes(preimage) +} + +func (s *nativeStateDB) Witness() *stateless.Witness { + return nil +} + +func (s *nativeStateDB) AccessEvents() *vm.AccessEvents { + return nil +} + +func (s *nativeStateDB) Finalise(bool) { + for _, acct := range s.accounts { + if acct.SelfDestructed { + acct.Code = nil + acct.Storage = map[common.Hash]common.Hash{} + } + } +} + +func (s *nativeStateDB) Error() error { + return s.err +} + +func (s *nativeStateDB) Commit(uint64, bool, bool) (common.Hash, error) { + return common.Hash{}, s.err +} + +func (s *nativeStateDB) SetTxContext(hash common.Hash, index int) { + s.txHash = hash + s.txIndex = index +} + +func (s *nativeStateDB) Copy() vm.StateDB { + cp := &nativeStateDB{ + source: s.source, + accounts: cloneAccounts(s.accounts), + base: cloneAccounts(s.base), + refund: s.refund, + logs: append([]*ethtypes.Log(nil), s.logs...), + preimages: clonePreimages(s.preimages), + accessList: cloneAccessList(s.accessList), + transientStates: cloneTransientStates(s.transientStates), + snapshots: cloneSnapshots(s.snapshots), + txHash: s.txHash, + txIndex: s.txIndex, + err: s.err, + evm: s.evm, + } + return cp +} + +func (s *nativeStateDB) IntermediateRoot(bool) common.Hash { + return common.Hash{} +} + +func (s *nativeStateDB) GetLogs(common.Hash, uint64, common.Hash) []*ethtypes.Log { + return s.Logs() +} + +func (s *nativeStateDB) TxIndex() int { + return s.txIndex +} + +func (s *nativeStateDB) Preimages() map[common.Hash][]byte { + return clonePreimages(s.preimages) +} + +func (s *nativeStateDB) Logs() []*ethtypes.Log { + return append([]*ethtypes.Log(nil), s.logs...) +} + +func (s *nativeStateDB) SetEVM(evm *vm.EVM) { + s.evm = evm +} + +func (s *nativeStateDB) account(addr common.Address) *nativeAccount { + if acct, ok := s.accounts[addr]; ok { + return acct + } + acct := s.loadAccount(addr) + s.accounts[addr] = acct.clone() + s.base[addr] = acct.clone() + return s.accounts[addr] +} + +func (s *nativeStateDB) baseAccount(addr common.Address) *nativeAccount { + if acct, ok := s.base[addr]; ok { + return acct + } + acct := s.loadAccount(addr) + s.base[addr] = acct.clone() + return s.base[addr] +} + +func (s *nativeStateDB) ensureStorage(addr common.Address, key common.Hash) { + base := s.baseAccount(addr) + if _, ok := base.Storage[key]; !ok { + if value := s.source.GetState(addr, key); value != (common.Hash{}) { + base.Storage[key] = value + } + } + acct := s.account(addr) + if _, ok := acct.Storage[key]; !ok { + if value := base.Storage[key]; value != (common.Hash{}) { + acct.Storage[key] = value + } + } +} + +func (s *nativeStateDB) loadAccount(addr common.Address) *nativeAccount { + acct := &nativeAccount{ + Balance: uint256FromBig(s.source.GetBalance(addr)), + Nonce: s.source.GetNonce(addr), + Code: cloneBytes(s.source.GetCode(addr)), + Storage: map[common.Hash]common.Hash{}, + } + return acct +} + +func (a *nativeAccount) clone() *nativeAccount { + if a == nil { + return &nativeAccount{Balance: uint256.NewInt(0), Storage: map[common.Hash]common.Hash{}} + } + cp := &nativeAccount{ + Balance: uint256.NewInt(0), + Nonce: a.Nonce, + Code: cloneBytes(a.Code), + Storage: map[common.Hash]common.Hash{}, + SelfDestructed: a.SelfDestructed, + Created: a.Created, + } + if a.Balance != nil { + cp.Balance = a.Balance.Clone() + } + for key, value := range a.Storage { + cp.Storage[key] = value + } + return cp +} + +func newAccessList() accessList { + return accessList{ + addresses: map[common.Address]struct{}{}, + slots: map[common.Address]map[common.Hash]struct{}{}, + } +} + +func cloneAccessList(al accessList) accessList { + cp := newAccessList() + for addr := range al.addresses { + cp.addresses[addr] = struct{}{} + } + for addr, slots := range al.slots { + cp.slots[addr] = map[common.Hash]struct{}{} + for slot := range slots { + cp.slots[addr][slot] = struct{}{} + } + } + return cp +} + +func cloneAccounts(accounts map[common.Address]*nativeAccount) map[common.Address]*nativeAccount { + cp := make(map[common.Address]*nativeAccount, len(accounts)) + for addr, acct := range accounts { + cp[addr] = acct.clone() + } + return cp +} + +func cloneTransientStates(states map[common.Address]map[common.Hash]common.Hash) map[common.Address]map[common.Hash]common.Hash { + cp := make(map[common.Address]map[common.Hash]common.Hash, len(states)) + for addr, slots := range states { + cp[addr] = map[common.Hash]common.Hash{} + for key, value := range slots { + cp[addr][key] = value + } + } + return cp +} + +func clonePreimages(preimages map[common.Hash][]byte) map[common.Hash][]byte { + cp := make(map[common.Hash][]byte, len(preimages)) + for hash, preimage := range preimages { + cp[hash] = cloneBytes(preimage) + } + return cp +} + +func cloneSnapshots(snapshots []nativeSnapshot) []nativeSnapshot { + cp := make([]nativeSnapshot, len(snapshots)) + for i, snapshot := range snapshots { + cp[i] = nativeSnapshot{ + accounts: cloneAccounts(snapshot.accounts), + refund: snapshot.refund, + logs: append([]*ethtypes.Log(nil), snapshot.logs...), + accessList: cloneAccessList(snapshot.accessList), + transientStates: cloneTransientStates(snapshot.transientStates), + preimages: clonePreimages(snapshot.preimages), + err: snapshot.err, + } + } + return cp +} + +func storageKeyUnion(a, b map[common.Hash]common.Hash) []common.Hash { + seen := map[common.Hash]struct{}{} + for key := range a { + seen[key] = struct{}{} + } + for key := range b { + seen[key] = struct{}{} + } + keys := make([]common.Hash, 0, len(seen)) + for key := range seen { + keys = append(keys, key) + } + sort.Slice(keys, func(i, j int) bool { + return bytes.Compare(keys[i][:], keys[j][:]) < 0 + }) + return keys +} + +func uint256FromBig(v *big.Int) *uint256.Int { + if v == nil { + return uint256.NewInt(0) + } + u, overflow := uint256.FromBig(v) + if overflow { + panic("state balance exceeds uint256") + } + if u == nil { + return uint256.NewInt(0) + } + return u +} + +func cloneBytes(v []byte) []byte { + if len(v) == 0 { + return nil + } + return append([]byte(nil), v...) +} diff --git a/giga/evmonly/state.go b/giga/evmonly/state.go new file mode 100644 index 0000000000..8e92570166 --- /dev/null +++ b/giga/evmonly/state.go @@ -0,0 +1,171 @@ +package evmonly + +import ( + "math/big" + "sync" + + "github.com/ethereum/go-ethereum/common" +) + +// StateReader supplies EVM-native state to an executor. +type StateReader interface { + GetBalance(common.Address) *big.Int + GetNonce(common.Address) uint64 + GetCode(common.Address) []byte + GetState(common.Address, common.Hash) common.Hash +} + +// StateWriter persists an executor-produced changeset. +type StateWriter interface { + ApplyChangeSet(StateChangeSet) +} + +// StateBackend is the minimal state boundary needed by the EVM-only executor. +type StateBackend interface { + StateReader + StateWriter +} + +// MemoryState is a small EVM-native state backend for tests and early wiring. +type MemoryState struct { + mu sync.RWMutex + accounts map[common.Address]*StateAccount +} + +// StateAccount is an EVM-native account snapshot. +type StateAccount struct { + Balance *big.Int + Nonce uint64 + Code []byte + Storage map[common.Hash]common.Hash +} + +func NewMemoryState() *MemoryState { + return &MemoryState{accounts: map[common.Address]*StateAccount{}} +} + +func (s *MemoryState) GetBalance(addr common.Address) *big.Int { + s.mu.RLock() + defer s.mu.RUnlock() + if acct, ok := s.accounts[addr]; ok && acct.Balance != nil { + return new(big.Int).Set(acct.Balance) + } + return new(big.Int) +} + +func (s *MemoryState) SetBalance(addr common.Address, balance *big.Int) { + s.mu.Lock() + defer s.mu.Unlock() + acct := s.getOrCreateAccountLocked(addr) + acct.Balance = cloneBig(balance) +} + +func (s *MemoryState) GetNonce(addr common.Address) uint64 { + s.mu.RLock() + defer s.mu.RUnlock() + if acct, ok := s.accounts[addr]; ok { + return acct.Nonce + } + return 0 +} + +func (s *MemoryState) SetNonce(addr common.Address, nonce uint64) { + s.mu.Lock() + defer s.mu.Unlock() + acct := s.getOrCreateAccountLocked(addr) + acct.Nonce = nonce +} + +func (s *MemoryState) GetCode(addr common.Address) []byte { + s.mu.RLock() + defer s.mu.RUnlock() + if acct, ok := s.accounts[addr]; ok { + return cloneBytes(acct.Code) + } + return nil +} + +func (s *MemoryState) SetCode(addr common.Address, code []byte) { + s.mu.Lock() + defer s.mu.Unlock() + acct := s.getOrCreateAccountLocked(addr) + acct.Code = cloneBytes(code) +} + +func (s *MemoryState) GetState(addr common.Address, key common.Hash) common.Hash { + s.mu.RLock() + defer s.mu.RUnlock() + if acct, ok := s.accounts[addr]; ok && acct.Storage != nil { + return acct.Storage[key] + } + return common.Hash{} +} + +func (s *MemoryState) SetState(addr common.Address, key common.Hash, value common.Hash) { + s.mu.Lock() + defer s.mu.Unlock() + acct := s.getOrCreateAccountLocked(addr) + if acct.Storage == nil { + acct.Storage = map[common.Hash]common.Hash{} + } + if value == (common.Hash{}) { + delete(acct.Storage, key) + return + } + acct.Storage[key] = value +} + +func (s *MemoryState) ApplyChangeSet(cs StateChangeSet) { + s.mu.Lock() + defer s.mu.Unlock() + for _, change := range cs.Balances { + acct := s.getOrCreateAccountLocked(change.Address) + acct.Balance = cloneBig(change.Balance) + } + for _, change := range cs.Nonces { + acct := s.getOrCreateAccountLocked(change.Address) + acct.Nonce = change.Nonce + } + for _, change := range cs.Code { + acct := s.getOrCreateAccountLocked(change.Address) + if change.Delete { + acct.Code = nil + } else { + acct.Code = cloneBytes(change.Code) + } + } + for _, change := range cs.Storage { + acct := s.getOrCreateAccountLocked(change.Address) + if acct.Storage == nil { + acct.Storage = map[common.Hash]common.Hash{} + } + if change.Delete { + delete(acct.Storage, change.Key) + } else { + acct.Storage[change.Key] = change.Value + } + } +} + +func (s *MemoryState) getOrCreateAccountLocked(addr common.Address) *StateAccount { + acct, ok := s.accounts[addr] + if !ok { + acct = &StateAccount{Balance: new(big.Int), Storage: map[common.Hash]common.Hash{}} + s.accounts[addr] = acct + } + return acct +} + +func cloneBig(v *big.Int) *big.Int { + if v == nil { + return new(big.Int) + } + return new(big.Int).Set(v) +} + +func cloneBytes(v []byte) []byte { + if len(v) == 0 { + return nil + } + return append([]byte(nil), v...) +} From 93e75ff46b0bcd44ed421ca53354c4fce52dbd14 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Mon, 15 Jun 2026 15:11:56 +0800 Subject: [PATCH 03/29] clean up evm-only executor scaffold leftovers --- giga/evmonly/README.md | 32 ++++++++++++++++++++++++----- giga/evmonly/seiv3/config.go | 13 +----------- giga/evmonly/seiv3/executor.go | 13 +++++------- giga/evmonly/seiv3/executor_test.go | 5 ++--- giga/evmonly/seiv3/runtime.go | 7 ------- giga/evmonly/seiv3/state_db.go | 1 - giga/evmonly/types.go | 3 --- 7 files changed, 35 insertions(+), 39 deletions(-) delete mode 100644 giga/evmonly/seiv3/runtime.go diff --git a/giga/evmonly/README.md b/giga/evmonly/README.md index 75aa00d30b..11e0eb3cc3 100644 --- a/giga/evmonly/README.md +++ b/giga/evmonly/README.md @@ -13,11 +13,28 @@ The target execution model is based on the `sei-v3` executor: - hot execution should not allocate `sdk.Context`, `sdk.Tx`, `MsgEVMTransaction`, Cosmos messages, or Cosmos coins -The current port executes raw RLP transactions with go-ethereum against an -EVM-native state backend, then returns a changeset plus Ethereum receipts. -Custom precompiles are still placeholders. The open work is to port them behind -an EVM-native context that is visible to the executor's conflict tracking -without reintroducing Cosmos keeper dependencies. +The current implementation executes raw RLP transactions with go-ethereum +against an EVM-native state backend, then returns a changeset plus Ethereum +receipts. Custom precompiles are still placeholders. The open work is to port +them behind an EVM-native context that is visible to the executor's conflict +tracking without reintroducing Cosmos keeper dependencies. + +## Current implementation + +The `seiv3` package currently provides: + +- sequential execution of the ordered block transaction list +- RLP decoding and sender recovery through go-ethereum signers +- go-ethereum `core.ApplyMessage` execution against an SDK-free `vm.StateDB` +- key-addressable state reads for balance, nonce, code, and storage +- deterministic post-block `StateChangeSet` construction +- Ethereum receipt construction with logs, bloom, gas, tx hash, block metadata, + contract address, and effective gas price +- a map-backed `MemoryState` for tests and early integration +- fail-closed custom precompile placeholders + +The executor accepts config for nonce checks, gas-price checks, minimum gas +price, chain config, and the custom precompile registry. ## Executor interface @@ -59,6 +76,10 @@ providing the block context. The executor is responsible for parsing tx RLP, recovering senders, validating EVM nonce/fee/intrinsic-gas rules, executing EVM state transitions, and producing deterministic outputs. +`BlockHash` is used for receipts and log metadata. The current `BLOCKHASH` +opcode callback only exposes `ParentHash`; older historical hashes require a +runtime-provided hash source in a later integration step. + ## Output format `BlockResult` contains two primary outputs: @@ -107,3 +128,4 @@ custom precompile addresses return `ErrCustomPrecompilesOpen`. by `(address, slot)` and does not require or expose range iteration. - The map-backed `MemoryState` is for tests and early integration; production should provide a durable native state backend. +- Historical `BLOCKHASH` lookups beyond the parent block are not wired yet. diff --git a/giga/evmonly/seiv3/config.go b/giga/evmonly/seiv3/config.go index e19cafbd2b..2e7e4761b7 100644 --- a/giga/evmonly/seiv3/config.go +++ b/giga/evmonly/seiv3/config.go @@ -1,7 +1,6 @@ package seiv3 import ( - "math" "math/big" "github.com/ethereum/go-ethereum/params" @@ -11,8 +10,6 @@ import ( // Config captures the sei-v3 executor knobs needed by the EVM-only path. type Config struct { - OCCWorkers int - FlushBatchSize int DisableNonceCheck bool DisableGasPriceCheck bool MinGasPrice *big.Int @@ -22,20 +19,12 @@ type Config struct { func DefaultConfig() Config { return Config{ - OCCWorkers: int(math.Min(12, float64(runtimeCPU()))), - FlushBatchSize: 100, - MinGasPrice: big.NewInt(1_000_000_000), + MinGasPrice: big.NewInt(1_000_000_000), } } func (c Config) WithDefaults() Config { defaults := DefaultConfig() - if c.OCCWorkers == 0 { - c.OCCWorkers = defaults.OCCWorkers - } - if c.FlushBatchSize == 0 { - c.FlushBatchSize = defaults.FlushBatchSize - } if c.MinGasPrice == nil { c.MinGasPrice = new(big.Int).Set(defaults.MinGasPrice) } diff --git a/giga/evmonly/seiv3/executor.go b/giga/evmonly/seiv3/executor.go index 52f2715e63..9024c2a47d 100644 --- a/giga/evmonly/seiv3/executor.go +++ b/giga/evmonly/seiv3/executor.go @@ -82,7 +82,7 @@ func (e *Executor) ExecuteBlock(ctx context.Context, req evmonly.BlockRequest) ( return nil, ctx.Err() default: } - txResult, receipt, err := e.executeTx(evm, stateDB, gasPool, req.Context, p, txIndex, baseFee) + txResult, receipt, err := e.executeTx(evm, stateDB, gasPool, req.Context, p, txIndex, baseFee, signer) if err != nil { return nil, fmt.Errorf("execute tx %d %s: %w", txIndex, p.tx.Hash(), err) } @@ -105,6 +105,7 @@ func (e *Executor) executeTx( p parsedTx, txIndex int, baseFee *big.Int, + signer ethtypes.Signer, ) (evmonly.TxResult, *ethtypes.Receipt, error) { tx := p.tx if e.cfg.CustomPrecompiles != nil && tx.To() != nil { @@ -122,7 +123,7 @@ func (e *Executor) executeTx( } } - msg, err := core.TransactionToMessage(tx, ethtypes.MakeSigner(e.chainConfig(block), new(big.Int).SetUint64(block.Number), block.Time), baseFee) + msg, err := core.TransactionToMessage(tx, signer, baseFee) if err != nil { return evmonly.TxResult{Hash: tx.Hash(), Sender: p.sender, To: tx.To(), Err: err}, nil, err } @@ -190,14 +191,10 @@ func buildBlockContext(ctx evmonly.BlockContext) vm.BlockContext { CanTransfer: core.CanTransfer, Transfer: core.Transfer, GetHash: func(n uint64) common.Hash { - switch { - case n == ctx.Number: - return ctx.BlockHash - case ctx.Number > 0 && n == ctx.Number-1: + if ctx.Number > 0 && n == ctx.Number-1 { return ctx.ParentHash - default: - return common.Hash{} } + return common.Hash{} }, Coinbase: ctx.Coinbase, GasLimit: gasLimit, diff --git a/giga/evmonly/seiv3/executor_test.go b/giga/evmonly/seiv3/executor_test.go index 6664103fae..7ca19c15a0 100644 --- a/giga/evmonly/seiv3/executor_test.go +++ b/giga/evmonly/seiv3/executor_test.go @@ -17,7 +17,7 @@ import ( ) func TestExecutorEmptyBlock(t *testing.T) { - executor := NewExecutor(Config{OCCWorkers: 1}) + executor := NewExecutor(Config{}) result, err := executor.ExecuteBlock(context.Background(), evmonly.BlockRequest{}) @@ -36,7 +36,7 @@ func TestExecutorTransferTx(t *testing.T) { state.SetBalance(sender, big.NewInt(200_000_000_000_000)) rawTx := signLegacyTx(t, key, chainID, 0, &recipient, big.NewInt(7), nil) - executor := NewExecutor(Config{OCCWorkers: 1}, WithState(state)) + executor := NewExecutor(Config{}, WithState(state)) result, err := executor.ExecuteBlock(context.Background(), evmonly.BlockRequest{ Context: blockContext(chainID), @@ -67,7 +67,6 @@ func TestExecutorCustomPrecompilePlaceholder(t *testing.T) { rawTx := signLegacyTx(t, key, chainID, 0, &customAddr, big.NewInt(0), []byte{0x01}) executor := NewExecutor(Config{ - OCCWorkers: 1, CustomPrecompiles: staticPrecompileRegistry{addr: customAddr}, }, WithState(state)) diff --git a/giga/evmonly/seiv3/runtime.go b/giga/evmonly/seiv3/runtime.go deleted file mode 100644 index cd5d04da9b..0000000000 --- a/giga/evmonly/seiv3/runtime.go +++ /dev/null @@ -1,7 +0,0 @@ -package seiv3 - -import "runtime" - -func runtimeCPU() int { - return runtime.NumCPU() -} diff --git a/giga/evmonly/seiv3/state_db.go b/giga/evmonly/seiv3/state_db.go index a641ea7f10..2008d6ddb5 100644 --- a/giga/evmonly/seiv3/state_db.go +++ b/giga/evmonly/seiv3/state_db.go @@ -152,7 +152,6 @@ func (s *nativeStateDB) SubBalance(addr common.Address, amount *uint256.Int, _ t acct := s.account(addr) if acct.Balance.Cmp(amount) < 0 { s.err = errInsufficientBalance - acct.Balance.Clear() return prev } acct.Balance.Sub(acct.Balance, amount) diff --git a/giga/evmonly/types.go b/giga/evmonly/types.go index c7c835b3ad..92ad6f34aa 100644 --- a/giga/evmonly/types.go +++ b/giga/evmonly/types.go @@ -2,15 +2,12 @@ package evmonly import ( "context" - "errors" "math/big" "github.com/ethereum/go-ethereum/common" ethtypes "github.com/ethereum/go-ethereum/core/types" ) -var ErrNotImplemented = errors.New("evm-only executor is not implemented") - // Executor is the Cosmos-free block execution boundary for the EVM-only path. type Executor interface { ExecuteBlock(context.Context, BlockRequest) (*BlockResult, error) From 7338faf46820212096c4f13948714a877635d662 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Mon, 15 Jun 2026 15:47:21 +0800 Subject: [PATCH 04/29] flatten evm-only executor package --- giga/evmonly/README.md | 9 +++-- giga/evmonly/{seiv3 => }/config.go | 2 +- giga/evmonly/{seiv3 => }/executor.go | 42 +++++++++-------------- giga/evmonly/{seiv3 => }/executor_test.go | 17 +++++---- giga/evmonly/{seiv3 => }/parser.go | 2 +- giga/evmonly/{seiv3 => }/state_db.go | 29 ++++++---------- giga/evmonly/types.go | 4 +-- 7 files changed, 43 insertions(+), 62 deletions(-) rename giga/evmonly/{seiv3 => }/config.go (97%) rename giga/evmonly/{seiv3 => }/executor.go (84%) rename giga/evmonly/{seiv3 => }/executor_test.go (86%) rename giga/evmonly/{seiv3 => }/parser.go (98%) rename giga/evmonly/{seiv3 => }/state_db.go (96%) diff --git a/giga/evmonly/README.md b/giga/evmonly/README.md index 11e0eb3cc3..d0ab7e8e92 100644 --- a/giga/evmonly/README.md +++ b/giga/evmonly/README.md @@ -21,7 +21,7 @@ tracking without reintroducing Cosmos keeper dependencies. ## Current implementation -The `seiv3` package currently provides: +The `evmonly` package currently provides: - sequential execution of the ordered block transaction list - RLP decoding and sender recovery through go-ethereum signers @@ -47,9 +47,8 @@ ExecuteBlock(context.Context, BlockRequest) (*BlockResult, error) The executor should be commit-neutral. It executes an ordered EVM block and returns the state writes and receipts produced by that block. The caller owns durable persistence, state commitment, block indexing, and receipt publication. -The `seiv3` implementation accepts a `StateReader` backend through -`WithState(...)`; callers can persist the returned `ChangeSet` with a matching -`StateWriter`. +The concrete `Executor` accepts a `StateReader` backend through `WithState(...)`; +callers can persist the returned `ChangeSet` with a matching `StateWriter`. ## Input block format @@ -116,7 +115,7 @@ state as contract storage owned by that precompile address. With no range reads and no side state, precompile reads and writes can then flow through ordinary `(address, slot)` storage tracking. -Until that design is implemented, the `seiv3` executor accepts a custom +Until that design is implemented, the `evmonly` executor accepts a custom precompile registry only as a fail-closed placeholder. Calls to registered custom precompile addresses return `ErrCustomPrecompilesOpen`. diff --git a/giga/evmonly/seiv3/config.go b/giga/evmonly/config.go similarity index 97% rename from giga/evmonly/seiv3/config.go rename to giga/evmonly/config.go index 2e7e4761b7..8371b4c845 100644 --- a/giga/evmonly/seiv3/config.go +++ b/giga/evmonly/config.go @@ -1,4 +1,4 @@ -package seiv3 +package evmonly import ( "math/big" diff --git a/giga/evmonly/seiv3/executor.go b/giga/evmonly/executor.go similarity index 84% rename from giga/evmonly/seiv3/executor.go rename to giga/evmonly/executor.go index 9024c2a47d..370d8315d9 100644 --- a/giga/evmonly/seiv3/executor.go +++ b/giga/evmonly/executor.go @@ -1,4 +1,4 @@ -package seiv3 +package evmonly import ( "context" @@ -13,19 +13,18 @@ import ( "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/params" - "github.com/sei-protocol/sei-chain/giga/evmonly" "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles" ) // Executor runs raw EVM transactions against an EVM-native state backend. type Executor struct { cfg Config - state evmonly.StateReader + state StateReader } type Option func(*Executor) -func WithState(state evmonly.StateReader) Option { +func WithState(state StateReader) Option { return func(e *Executor) { if state != nil { e.state = state @@ -36,7 +35,7 @@ func WithState(state evmonly.StateReader) Option { func NewExecutor(cfg Config, opts ...Option) *Executor { e := &Executor{ cfg: cfg.WithDefaults(), - state: evmonly.NewMemoryState(), + state: NewMemoryState(), } for _, opt := range opts { opt(e) @@ -48,9 +47,9 @@ func (e *Executor) Config() Config { return e.cfg } -func (e *Executor) ExecuteBlock(ctx context.Context, req evmonly.BlockRequest) (*evmonly.BlockResult, error) { +func (e *Executor) ExecuteBlock(ctx context.Context, req BlockRequest) (*BlockResult, error) { if len(req.Txs) == 0 { - return &evmonly.BlockResult{}, nil + return &BlockResult{}, nil } chainConfig := e.chainConfig(req.Context) @@ -72,8 +71,8 @@ func (e *Executor) ExecuteBlock(ctx context.Context, req evmonly.BlockRequest) ( gasPool := new(core.GasPool).AddGas(gasLimit) baseFee := cloneBig(req.Context.BaseFee) - result := &evmonly.BlockResult{ - Txs: make([]evmonly.TxResult, 0, len(parsed)), + result := &BlockResult{ + Txs: make([]TxResult, 0, len(parsed)), Receipts: make(ethtypes.Receipts, 0, len(parsed)), } for txIndex, p := range parsed { @@ -101,23 +100,23 @@ func (e *Executor) executeTx( evm *vm.EVM, stateDB *nativeStateDB, gasPool *core.GasPool, - block evmonly.BlockContext, + block BlockContext, p parsedTx, txIndex int, baseFee *big.Int, signer ethtypes.Signer, -) (evmonly.TxResult, *ethtypes.Receipt, error) { +) (TxResult, *ethtypes.Receipt, error) { tx := p.tx if e.cfg.CustomPrecompiles != nil && tx.To() != nil { if _, ok := e.cfg.CustomPrecompiles.Get(*tx.To()); ok { - return evmonly.TxResult{Hash: tx.Hash(), Sender: p.sender, To: tx.To(), Err: precompiles.ErrCustomPrecompilesOpen}, + return TxResult{Hash: tx.Hash(), Sender: p.sender, To: tx.To(), Err: precompiles.ErrCustomPrecompilesOpen}, nil, precompiles.ErrCustomPrecompilesOpen } } if !e.cfg.DisableGasPriceCheck && e.cfg.MinGasPrice != nil { if effectiveGasPrice(tx, baseFee).Cmp(e.cfg.MinGasPrice) < 0 { - return evmonly.TxResult{Hash: tx.Hash(), Sender: p.sender, To: tx.To(), Err: errInsufficientGasPrice}, + return TxResult{Hash: tx.Hash(), Sender: p.sender, To: tx.To(), Err: errInsufficientGasPrice}, nil, errInsufficientGasPrice } @@ -125,7 +124,7 @@ func (e *Executor) executeTx( msg, err := core.TransactionToMessage(tx, signer, baseFee) if err != nil { - return evmonly.TxResult{Hash: tx.Hash(), Sender: p.sender, To: tx.To(), Err: err}, nil, err + return TxResult{Hash: tx.Hash(), Sender: p.sender, To: tx.To(), Err: err}, nil, err } msg.SkipNonceChecks = e.cfg.DisableNonceCheck @@ -134,7 +133,7 @@ func (e *Executor) executeTx( evm.SetTxContext(core.NewEVMTxContext(msg)) execResult, err := core.ApplyMessage(evm, msg, gasPool) if err != nil { - return evmonly.TxResult{Hash: tx.Hash(), Sender: p.sender, To: tx.To(), Err: err}, nil, err + return TxResult{Hash: tx.Hash(), Sender: p.sender, To: tx.To(), Err: err}, nil, err } txLogs := append([]*ethtypes.Log(nil), stateDB.logs[logStart:]...) @@ -165,7 +164,7 @@ func (e *Executor) executeTx( } receipt.Bloom = ethtypes.CreateBloom(receipt) - txResult := evmonly.TxResult{ + txResult := TxResult{ Hash: tx.Hash(), Sender: p.sender, To: tx.To(), @@ -179,7 +178,7 @@ func (e *Executor) executeTx( return txResult, receipt, nil } -func buildBlockContext(ctx evmonly.BlockContext) vm.BlockContext { +func buildBlockContext(ctx BlockContext) vm.BlockContext { prevRandao := ctx.PrevRandao baseFee := cloneBig(ctx.BaseFee) blobBaseFee := cloneBig(ctx.BlobBaseFee) @@ -232,7 +231,7 @@ func customPrecompileMap(registry precompiles.Registry) map[common.Address]vm.Pr return contracts } -func (e *Executor) chainConfig(ctx evmonly.BlockContext) *params.ChainConfig { +func (e *Executor) chainConfig(ctx BlockContext) *params.ChainConfig { var cfg params.ChainConfig if e.cfg.ChainConfig != nil { cfg = *e.cfg.ChainConfig @@ -259,11 +258,4 @@ func effectiveGasPrice(tx *ethtypes.Transaction, baseFee *big.Int) *big.Int { return tx.GasPrice() } -func cloneBig(v *big.Int) *big.Int { - if v == nil { - return new(big.Int) - } - return new(big.Int).Set(v) -} - var errInsufficientGasPrice = fmt.Errorf("insufficient gas price") diff --git a/giga/evmonly/seiv3/executor_test.go b/giga/evmonly/executor_test.go similarity index 86% rename from giga/evmonly/seiv3/executor_test.go rename to giga/evmonly/executor_test.go index 7ca19c15a0..40fa4898db 100644 --- a/giga/evmonly/seiv3/executor_test.go +++ b/giga/evmonly/executor_test.go @@ -1,4 +1,4 @@ -package seiv3 +package evmonly import ( "context" @@ -12,14 +12,13 @@ import ( "github.com/ethereum/go-ethereum/crypto" "github.com/stretchr/testify/require" - "github.com/sei-protocol/sei-chain/giga/evmonly" "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles" ) func TestExecutorEmptyBlock(t *testing.T) { executor := NewExecutor(Config{}) - result, err := executor.ExecuteBlock(context.Background(), evmonly.BlockRequest{}) + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{}) require.NoError(t, err) require.NotNil(t, result) @@ -32,13 +31,13 @@ func TestExecutorTransferTx(t *testing.T) { sender := crypto.PubkeyToAddress(key.PublicKey) recipient := common.HexToAddress("0x00000000000000000000000000000000000000a1") - state := evmonly.NewMemoryState() + state := NewMemoryState() state.SetBalance(sender, big.NewInt(200_000_000_000_000)) rawTx := signLegacyTx(t, key, chainID, 0, &recipient, big.NewInt(7), nil) executor := NewExecutor(Config{}, WithState(state)) - result, err := executor.ExecuteBlock(context.Background(), evmonly.BlockRequest{ + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ Context: blockContext(chainID), Txs: [][]byte{rawTx}, }) @@ -62,7 +61,7 @@ func TestExecutorCustomPrecompilePlaceholder(t *testing.T) { sender := crypto.PubkeyToAddress(key.PublicKey) customAddr := common.HexToAddress("0x0000000000000000000000000000000000001001") - state := evmonly.NewMemoryState() + state := NewMemoryState() state.SetBalance(sender, big.NewInt(200_000_000_000_000)) rawTx := signLegacyTx(t, key, chainID, 0, &customAddr, big.NewInt(0), []byte{0x01}) @@ -70,7 +69,7 @@ func TestExecutorCustomPrecompilePlaceholder(t *testing.T) { CustomPrecompiles: staticPrecompileRegistry{addr: customAddr}, }, WithState(state)) - _, err = executor.ExecuteBlock(context.Background(), evmonly.BlockRequest{ + _, err = executor.ExecuteBlock(context.Background(), BlockRequest{ Context: blockContext(chainID), Txs: [][]byte{rawTx}, }) @@ -96,8 +95,8 @@ func signLegacyTx(t *testing.T, key *ecdsa.PrivateKey, chainID *big.Int, nonce u return raw } -func blockContext(chainID *big.Int) evmonly.BlockContext { - return evmonly.BlockContext{ +func blockContext(chainID *big.Int) BlockContext { + return BlockContext{ Number: 1, Time: 1, GasLimit: 30_000_000, diff --git a/giga/evmonly/seiv3/parser.go b/giga/evmonly/parser.go similarity index 98% rename from giga/evmonly/seiv3/parser.go rename to giga/evmonly/parser.go index 76a4da311a..f2c0776e3d 100644 --- a/giga/evmonly/seiv3/parser.go +++ b/giga/evmonly/parser.go @@ -1,4 +1,4 @@ -package seiv3 +package evmonly import ( "context" diff --git a/giga/evmonly/seiv3/state_db.go b/giga/evmonly/state_db.go similarity index 96% rename from giga/evmonly/seiv3/state_db.go rename to giga/evmonly/state_db.go index 2008d6ddb5..e888d5a257 100644 --- a/giga/evmonly/seiv3/state_db.go +++ b/giga/evmonly/state_db.go @@ -1,4 +1,4 @@ -package seiv3 +package evmonly import ( "bytes" @@ -15,14 +15,12 @@ import ( "github.com/ethereum/go-ethereum/params" ethutils "github.com/ethereum/go-ethereum/trie/utils" "github.com/holiman/uint256" - - "github.com/sei-protocol/sei-chain/giga/evmonly" ) var errInsufficientBalance = errors.New("insufficient balance") type nativeStateDB struct { - source evmonly.StateReader + source StateReader accounts map[common.Address]*nativeAccount base map[common.Address]*nativeAccount @@ -65,9 +63,9 @@ type accessList struct { slots map[common.Address]map[common.Hash]struct{} } -func newNativeStateDB(source evmonly.StateReader) *nativeStateDB { +func newNativeStateDB(source StateReader) *nativeStateDB { if source == nil { - source = evmonly.NewMemoryState() + source = NewMemoryState() } return &nativeStateDB{ source: source, @@ -79,7 +77,7 @@ func newNativeStateDB(source evmonly.StateReader) *nativeStateDB { } } -func (s *nativeStateDB) ChangeSet() evmonly.StateChangeSet { +func (s *nativeStateDB) ChangeSet() StateChangeSet { addresses := make([]common.Address, 0, len(s.accounts)) for addr := range s.accounts { addresses = append(addresses, addr) @@ -88,25 +86,25 @@ func (s *nativeStateDB) ChangeSet() evmonly.StateChangeSet { return bytes.Compare(addresses[i][:], addresses[j][:]) < 0 }) - var changes evmonly.StateChangeSet + var changes StateChangeSet for _, addr := range addresses { acct := s.accounts[addr] base := s.baseAccount(addr) if !acct.Balance.Eq(base.Balance) { - changes.Balances = append(changes.Balances, evmonly.BalanceChange{ + changes.Balances = append(changes.Balances, BalanceChange{ Address: addr, Balance: acct.Balance.ToBig(), }) } if acct.Nonce != base.Nonce { - changes.Nonces = append(changes.Nonces, evmonly.NonceChange{ + changes.Nonces = append(changes.Nonces, NonceChange{ Address: addr, Nonce: acct.Nonce, }) } if !bytes.Equal(acct.Code, base.Code) { - changes.Code = append(changes.Code, evmonly.CodeChange{ + changes.Code = append(changes.Code, CodeChange{ Address: addr, Code: cloneBytes(acct.Code), Delete: len(acct.Code) == 0, @@ -119,7 +117,7 @@ func (s *nativeStateDB) ChangeSet() evmonly.StateChangeSet { if oldValue == newValue { continue } - changes.Storage = append(changes.Storage, evmonly.StorageChange{ + changes.Storage = append(changes.Storage, StorageChange{ Address: addr, Key: key, Value: newValue, @@ -639,10 +637,3 @@ func uint256FromBig(v *big.Int) *uint256.Int { } return u } - -func cloneBytes(v []byte) []byte { - if len(v) == 0 { - return nil - } - return append([]byte(nil), v...) -} diff --git a/giga/evmonly/types.go b/giga/evmonly/types.go index 92ad6f34aa..708c10e91f 100644 --- a/giga/evmonly/types.go +++ b/giga/evmonly/types.go @@ -8,8 +8,8 @@ import ( ethtypes "github.com/ethereum/go-ethereum/core/types" ) -// Executor is the Cosmos-free block execution boundary for the EVM-only path. -type Executor interface { +// BlockExecutor is the Cosmos-free block execution boundary for the EVM-only path. +type BlockExecutor interface { ExecuteBlock(context.Context, BlockRequest) (*BlockResult, error) } From 5de0066c6ea5bf723a23398d2e21db0e90dac97f Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Mon, 15 Jun 2026 19:25:27 +0800 Subject: [PATCH 05/29] fix evmonly lint after rebase --- giga/evmonly/executor.go | 11 +++++++---- giga/evmonly/state_db.go | 18 +++++++++++++----- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/giga/evmonly/executor.go b/giga/evmonly/executor.go index 370d8315d9..f5d97644f3 100644 --- a/giga/evmonly/executor.go +++ b/giga/evmonly/executor.go @@ -75,13 +75,14 @@ func (e *Executor) ExecuteBlock(ctx context.Context, req BlockRequest) (*BlockRe Txs: make([]TxResult, 0, len(parsed)), Receipts: make(ethtypes.Receipts, 0, len(parsed)), } + var txIndexUint uint for txIndex, p := range parsed { select { case <-ctx.Done(): return nil, ctx.Err() default: } - txResult, receipt, err := e.executeTx(evm, stateDB, gasPool, req.Context, p, txIndex, baseFee, signer) + txResult, receipt, err := e.executeTx(evm, stateDB, gasPool, req.Context, p, txIndex, txIndexUint, baseFee, signer) if err != nil { return nil, fmt.Errorf("execute tx %d %s: %w", txIndex, p.tx.Hash(), err) } @@ -90,6 +91,7 @@ func (e *Executor) ExecuteBlock(ctx context.Context, req BlockRequest) (*BlockRe result.Txs = append(result.Txs, txResult) result.Receipts = append(result.Receipts, receipt) result.GasUsed += txResult.GasUsed + txIndexUint++ } stateDB.Finalise(true) result.ChangeSet = stateDB.ChangeSet() @@ -103,6 +105,7 @@ func (e *Executor) executeTx( block BlockContext, p parsedTx, txIndex int, + txIndexUint uint, baseFee *big.Int, signer ethtypes.Signer, ) (TxResult, *ethtypes.Receipt, error) { @@ -128,7 +131,7 @@ func (e *Executor) executeTx( } msg.SkipNonceChecks = e.cfg.DisableNonceCheck - stateDB.SetTxContext(tx.Hash(), txIndex) + stateDB.setTxContext(tx.Hash(), txIndex, txIndexUint) logStart := len(stateDB.logs) evm.SetTxContext(core.NewEVMTxContext(msg)) execResult, err := core.ApplyMessage(evm, msg, gasPool) @@ -141,7 +144,7 @@ func (e *Executor) executeTx( log.BlockNumber = block.Number log.BlockHash = block.BlockHash log.TxHash = tx.Hash() - log.TxIndex = uint(txIndex) + log.TxIndex = txIndexUint } status := ethtypes.ReceiptStatusSuccessful @@ -157,7 +160,7 @@ func (e *Executor) executeTx( EffectiveGasPrice: effectiveGasPrice(tx, baseFee), BlockHash: block.BlockHash, BlockNumber: new(big.Int).SetUint64(block.Number), - TransactionIndex: uint(txIndex), + TransactionIndex: txIndexUint, } if tx.To() == nil { receipt.ContractAddress = crypto.CreateAddress(p.sender, tx.Nonce()) diff --git a/giga/evmonly/state_db.go b/giga/evmonly/state_db.go index e888d5a257..8038488b4b 100644 --- a/giga/evmonly/state_db.go +++ b/giga/evmonly/state_db.go @@ -33,10 +33,11 @@ type nativeStateDB struct { transientStates map[common.Address]map[common.Hash]common.Hash snapshots []nativeSnapshot - txHash common.Hash - txIndex int - err error - evm *vm.EVM + txHash common.Hash + txIndex int + txIndexUint uint + err error + evm *vm.EVM } type nativeAccount struct { @@ -395,7 +396,7 @@ func (s *nativeStateDB) RevertToSnapshot(id int) { func (s *nativeStateDB) AddLog(log *ethtypes.Log) { log.TxHash = s.txHash - log.TxIndex = uint(s.txIndex) + log.TxIndex = s.txIndexUint log.Index = uint(len(s.logs)) s.logs = append(s.logs, log) } @@ -434,6 +435,12 @@ func (s *nativeStateDB) SetTxContext(hash common.Hash, index int) { s.txIndex = index } +func (s *nativeStateDB) setTxContext(hash common.Hash, index int, indexUint uint) { + s.txHash = hash + s.txIndex = index + s.txIndexUint = indexUint +} + func (s *nativeStateDB) Copy() vm.StateDB { cp := &nativeStateDB{ source: s.source, @@ -447,6 +454,7 @@ func (s *nativeStateDB) Copy() vm.StateDB { snapshots: cloneSnapshots(s.snapshots), txHash: s.txHash, txIndex: s.txIndex, + txIndexUint: s.txIndexUint, err: s.err, evm: s.evm, } From 4a5612578168187e4e726a220200888c5c9ef96c Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Tue, 16 Jun 2026 15:05:30 +0800 Subject: [PATCH 06/29] address evmonly cursor review comments --- giga/evmonly/executor.go | 1 + giga/evmonly/executor_test.go | 115 ++++++++++++++++++++++++++++++++++ giga/evmonly/parser.go | 3 +- giga/evmonly/state_db.go | 1 + 4 files changed, 118 insertions(+), 2 deletions(-) diff --git a/giga/evmonly/executor.go b/giga/evmonly/executor.go index f5d97644f3..ad427fcd31 100644 --- a/giga/evmonly/executor.go +++ b/giga/evmonly/executor.go @@ -138,6 +138,7 @@ func (e *Executor) executeTx( if err != nil { return TxResult{Hash: tx.Hash(), Sender: p.sender, To: tx.To(), Err: err}, nil, err } + stateDB.Finalise(true) txLogs := append([]*ethtypes.Log(nil), stateDB.logs[logStart:]...) for _, log := range txLogs { diff --git a/giga/evmonly/executor_test.go b/giga/evmonly/executor_test.go index 40fa4898db..f8fe9589d3 100644 --- a/giga/evmonly/executor_test.go +++ b/giga/evmonly/executor_test.go @@ -10,6 +10,7 @@ import ( "github.com/ethereum/go-ethereum/common" ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/params" "github.com/stretchr/testify/require" "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles" @@ -54,6 +55,78 @@ func TestExecutorTransferTx(t *testing.T) { require.Equal(t, uint64(1), state.GetNonce(sender)) } +func TestExecutorDynamicFeeTx(t *testing.T) { + chainID := big.NewInt(713715) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + recipient := common.HexToAddress("0x00000000000000000000000000000000000000a2") + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(200_000_000_000_000)) + + rawTx := signDynamicFeeTx(t, key, chainID, 0, &recipient, big.NewInt(11), nil) + executor := NewExecutor(Config{}, WithState(state)) + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: blockContext(chainID), + Txs: [][]byte{rawTx}, + }) + + require.NoError(t, err) + require.Len(t, result.Txs, 1) + require.Equal(t, uint8(ethtypes.DynamicFeeTxType), result.Receipts[0].Type) + + state.ApplyChangeSet(result.ChangeSet) + require.Equal(t, big.NewInt(11), state.GetBalance(recipient)) +} + +func TestExecutorFinalisesAfterEachTx(t *testing.T) { + chainID := big.NewInt(713715) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + contract := common.HexToAddress("0x00000000000000000000000000000000000000c1") + beneficiary := common.HexToAddress("0x00000000000000000000000000000000000000b1") + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(500_000_000_000_000)) + state.SetCode(contract, selfDestructCode(beneficiary)) + + firstCall := signLegacyTx(t, key, chainID, 0, &contract, big.NewInt(0), nil) + secondCall := signLegacyTx(t, key, chainID, 1, &contract, big.NewInt(5), nil) + executor := NewExecutor(Config{ + ChainConfig: legacySelfDestructChainConfig(chainID), + }, WithState(state)) + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: blockContext(chainID), + Txs: [][]byte{firstCall, secondCall}, + }) + + require.NoError(t, err) + require.Len(t, result.Receipts, 2) + + state.ApplyChangeSet(result.ChangeSet) + require.Empty(t, state.GetCode(contract)) + require.Equal(t, big.NewInt(5), state.GetBalance(contract)) + require.Equal(t, big.NewInt(0), state.GetBalance(beneficiary)) +} + +func TestPrepareClearsTransientStorage(t *testing.T) { + stateDB := newNativeStateDB(NewMemoryState()) + addr := common.HexToAddress("0x00000000000000000000000000000000000000a3") + key := common.HexToHash("0x01") + value := common.HexToHash("0x02") + + stateDB.SetTransientState(addr, key, value) + require.Equal(t, value, stateDB.GetTransientState(addr, key)) + + stateDB.Prepare(params.Rules{}, addr, common.Address{}, nil, nil, nil) + + require.Equal(t, common.Hash{}, stateDB.GetTransientState(addr, key)) +} + func TestExecutorCustomPrecompilePlaceholder(t *testing.T) { chainID := big.NewInt(713715) key, err := crypto.GenerateKey() @@ -95,6 +168,25 @@ func signLegacyTx(t *testing.T, key *ecdsa.PrivateKey, chainID *big.Int, nonce u return raw } +func signDynamicFeeTx(t *testing.T, key *ecdsa.PrivateKey, chainID *big.Int, nonce uint64, to *common.Address, value *big.Int, data []byte) []byte { + t.Helper() + tx := ethtypes.NewTx(ðtypes.DynamicFeeTx{ + ChainID: chainID, + Nonce: nonce, + GasTipCap: big.NewInt(1_000_000_000), + GasFeeCap: big.NewInt(1_000_000_000), + Gas: 100_000, + To: to, + Value: value, + Data: data, + }) + signed, err := ethtypes.SignTx(tx, ethtypes.LatestSignerForChainID(chainID), key) + require.NoError(t, err) + raw, err := signed.MarshalBinary() + require.NoError(t, err) + return raw +} + func blockContext(chainID *big.Int) BlockContext { return BlockContext{ Number: 1, @@ -106,6 +198,29 @@ func blockContext(chainID *big.Int) BlockContext { } } +func legacySelfDestructChainConfig(chainID *big.Int) *params.ChainConfig { + return ¶ms.ChainConfig{ + ChainID: chainID, + HomesteadBlock: big.NewInt(0), + DAOForkBlock: nil, + DAOForkSupport: false, + EIP150Block: big.NewInt(0), + EIP155Block: big.NewInt(0), + EIP158Block: big.NewInt(0), + ByzantiumBlock: big.NewInt(0), + ConstantinopleBlock: big.NewInt(0), + PetersburgBlock: big.NewInt(0), + IstanbulBlock: big.NewInt(0), + BerlinBlock: big.NewInt(0), + LondonBlock: big.NewInt(0), + } +} + +func selfDestructCode(beneficiary common.Address) []byte { + code := append([]byte{0x73}, beneficiary.Bytes()...) + return append(code, 0xff) +} + type staticPrecompileRegistry struct { addr common.Address } diff --git a/giga/evmonly/parser.go b/giga/evmonly/parser.go index f2c0776e3d..d5a47fffb6 100644 --- a/giga/evmonly/parser.go +++ b/giga/evmonly/parser.go @@ -6,7 +6,6 @@ import ( "github.com/ethereum/go-ethereum/common" ethtypes "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/rlp" ) type parsedTx struct { @@ -33,7 +32,7 @@ func parseBlockTxs(ctx context.Context, txs [][]byte, signer ethtypes.Signer) ([ func parseTx(raw []byte, signer ethtypes.Signer) (*ethtypes.Transaction, common.Address, error) { var tx ethtypes.Transaction - if err := rlp.DecodeBytes(raw, &tx); err != nil { + if err := tx.UnmarshalBinary(raw); err != nil { return nil, common.Address{}, err } sender, err := ethtypes.Sender(signer, &tx) diff --git a/giga/evmonly/state_db.go b/giga/evmonly/state_db.go index 8038488b4b..885f24109b 100644 --- a/giga/evmonly/state_db.go +++ b/giga/evmonly/state_db.go @@ -345,6 +345,7 @@ func (s *nativeStateDB) AddSlotToAccessList(addr common.Address, slot common.Has func (s *nativeStateDB) Prepare(_ params.Rules, sender, coinbase common.Address, dest *common.Address, precompiles []common.Address, txAccesses ethtypes.AccessList) { s.accessList = newAccessList() + s.transientStates = map[common.Address]map[common.Hash]common.Hash{} s.AddAddressToAccessList(sender) s.AddAddressToAccessList(coinbase) if dest != nil { From f0d8315aa928bcab961c29df90a5491cbbc5ea9a Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Tue, 16 Jun 2026 15:22:22 +0800 Subject: [PATCH 07/29] fix evmonly state snapshot finalization --- giga/evmonly/executor_test.go | 26 ++++++++++++++++++++++++++ giga/evmonly/state_db.go | 5 +++++ 2 files changed, 31 insertions(+) diff --git a/giga/evmonly/executor_test.go b/giga/evmonly/executor_test.go index f8fe9589d3..1f2730b9df 100644 --- a/giga/evmonly/executor_test.go +++ b/giga/evmonly/executor_test.go @@ -127,6 +127,32 @@ func TestPrepareClearsTransientStorage(t *testing.T) { require.Equal(t, common.Hash{}, stateDB.GetTransientState(addr, key)) } +func TestSnapshotRevertRestoresBaseState(t *testing.T) { + addr := common.HexToAddress("0x00000000000000000000000000000000000000a4") + key := common.HexToHash("0x01") + value := common.HexToHash("0x02") + + state := NewMemoryState() + state.SetState(addr, key, value) + stateDB := newNativeStateDB(state) + stateDB.GetBalance(addr) + + snapshot := stateDB.Snapshot() + require.Equal(t, value, stateDB.GetState(addr, key)) + stateDB.RevertToSnapshot(snapshot) + + require.Empty(t, stateDB.ChangeSet().Storage) +} + +func TestFinaliseClearsRefund(t *testing.T) { + stateDB := newNativeStateDB(NewMemoryState()) + stateDB.AddRefund(12) + + stateDB.Finalise(true) + + require.Zero(t, stateDB.GetRefund()) +} + func TestExecutorCustomPrecompilePlaceholder(t *testing.T) { chainID := big.NewInt(713715) key, err := crypto.GenerateKey() diff --git a/giga/evmonly/state_db.go b/giga/evmonly/state_db.go index 885f24109b..210a5ab7f7 100644 --- a/giga/evmonly/state_db.go +++ b/giga/evmonly/state_db.go @@ -51,6 +51,7 @@ type nativeAccount struct { type nativeSnapshot struct { accounts map[common.Address]*nativeAccount + base map[common.Address]*nativeAccount refund uint64 logs []*ethtypes.Log accessList accessList @@ -370,6 +371,7 @@ func (s *nativeStateDB) Snapshot() int { id := len(s.snapshots) s.snapshots = append(s.snapshots, nativeSnapshot{ accounts: cloneAccounts(s.accounts), + base: cloneAccounts(s.base), refund: s.refund, logs: append([]*ethtypes.Log(nil), s.logs...), accessList: cloneAccessList(s.accessList), @@ -386,6 +388,7 @@ func (s *nativeStateDB) RevertToSnapshot(id int) { } snapshot := s.snapshots[id] s.accounts = cloneAccounts(snapshot.accounts) + s.base = cloneAccounts(snapshot.base) s.refund = snapshot.refund s.logs = append([]*ethtypes.Log(nil), snapshot.logs...) s.accessList = cloneAccessList(snapshot.accessList) @@ -421,6 +424,7 @@ func (s *nativeStateDB) Finalise(bool) { acct.Storage = map[common.Hash]common.Hash{} } } + s.refund = 0 } func (s *nativeStateDB) Error() error { @@ -604,6 +608,7 @@ func cloneSnapshots(snapshots []nativeSnapshot) []nativeSnapshot { for i, snapshot := range snapshots { cp[i] = nativeSnapshot{ accounts: cloneAccounts(snapshot.accounts), + base: cloneAccounts(snapshot.base), refund: snapshot.refund, logs: append([]*ethtypes.Log(nil), snapshot.logs...), accessList: cloneAccessList(snapshot.accessList), From 298afddfa1cdb2dbc349e3588d8cc3dddd26065e Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Wed, 17 Jun 2026 10:30:51 +0800 Subject: [PATCH 08/29] let custom precompile placeholders fail in receipts --- giga/evmonly/README.md | 4 ++++ giga/evmonly/executor.go | 9 ++------- giga/evmonly/executor_test.go | 9 ++++++--- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/giga/evmonly/README.md b/giga/evmonly/README.md index d0ab7e8e92..928a203ce3 100644 --- a/giga/evmonly/README.md +++ b/giga/evmonly/README.md @@ -50,6 +50,10 @@ durable persistence, state commitment, block indexing, and receipt publication. The concrete `Executor` accepts a `StateReader` backend through `WithState(...)`; callers can persist the returned `ChangeSet` with a matching `StateWriter`. +A non-nil `error` means block validation failed and the caller must not commit a +partial output. EVM call failures inside an otherwise valid transaction are +represented in `Receipts` and `Txs` with failed status. + ## Input block format `BlockRequest` is the expected input: diff --git a/giga/evmonly/executor.go b/giga/evmonly/executor.go index ad427fcd31..ad49d485f7 100644 --- a/giga/evmonly/executor.go +++ b/giga/evmonly/executor.go @@ -110,14 +110,9 @@ func (e *Executor) executeTx( signer ethtypes.Signer, ) (TxResult, *ethtypes.Receipt, error) { tx := p.tx - if e.cfg.CustomPrecompiles != nil && tx.To() != nil { - if _, ok := e.cfg.CustomPrecompiles.Get(*tx.To()); ok { - return TxResult{Hash: tx.Hash(), Sender: p.sender, To: tx.To(), Err: precompiles.ErrCustomPrecompilesOpen}, - nil, - precompiles.ErrCustomPrecompilesOpen - } - } if !e.cfg.DisableGasPriceCheck && e.cfg.MinGasPrice != nil { + // MinGasPrice is block-validity policy; unlike EVM call failures, it + // does not produce a receipt for an otherwise invalid block. if effectiveGasPrice(tx, baseFee).Cmp(e.cfg.MinGasPrice) < 0 { return TxResult{Hash: tx.Hash(), Sender: p.sender, To: tx.To(), Err: errInsufficientGasPrice}, nil, diff --git a/giga/evmonly/executor_test.go b/giga/evmonly/executor_test.go index 1f2730b9df..3d7f4d000e 100644 --- a/giga/evmonly/executor_test.go +++ b/giga/evmonly/executor_test.go @@ -168,13 +168,16 @@ func TestExecutorCustomPrecompilePlaceholder(t *testing.T) { CustomPrecompiles: staticPrecompileRegistry{addr: customAddr}, }, WithState(state)) - _, err = executor.ExecuteBlock(context.Background(), BlockRequest{ + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ Context: blockContext(chainID), Txs: [][]byte{rawTx}, }) - require.Error(t, err) - require.True(t, errors.Is(err, precompiles.ErrCustomPrecompilesOpen)) + require.NoError(t, err) + require.Len(t, result.Txs, 1) + require.Len(t, result.Receipts, 1) + require.Equal(t, ethtypes.ReceiptStatusFailed, result.Txs[0].Status) + require.True(t, errors.Is(result.Txs[0].Err, precompiles.ErrCustomPrecompilesOpen)) } func signLegacyTx(t *testing.T, key *ecdsa.PrivateKey, chainID *big.Int, nonce uint64, to *common.Address, value *big.Int, data []byte) []byte { From 0a6110a935302ad714cd898a1c3a44cb80b2b0e6 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Wed, 24 Jun 2026 11:31:41 +0800 Subject: [PATCH 09/29] expand evmonly executor test coverage --- giga/evmonly/executor_test.go | 263 +++++++++++++++++++++++++++++++++- 1 file changed, 262 insertions(+), 1 deletion(-) diff --git a/giga/evmonly/executor_test.go b/giga/evmonly/executor_test.go index 3d7f4d000e..31c84bc00a 100644 --- a/giga/evmonly/executor_test.go +++ b/giga/evmonly/executor_test.go @@ -8,7 +8,9 @@ import ( "testing" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/params" "github.com/stretchr/testify/require" @@ -81,6 +83,217 @@ func TestExecutorDynamicFeeTx(t *testing.T) { require.Equal(t, big.NewInt(11), state.GetBalance(recipient)) } +func TestExecutorReceiptAndLogMetadata(t *testing.T) { + chainID := big.NewInt(713715) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + recipient := testAddress(0xa5) + logContract := testAddress(0xc2) + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000_000_000_000)) + state.SetCode(logContract, log0Code()) + + transfer := signLegacyTx(t, key, chainID, 0, &recipient, big.NewInt(3), nil) + emitLog := signLegacyTx(t, key, chainID, 1, &logContract, big.NewInt(0), nil) + transferTx := decodeTx(t, transfer) + emitLogTx := decodeTx(t, emitLog) + ctx := blockContext(chainID) + ctx.Number = 42 + ctx.BlockHash = testHash(0x42) + executor := NewExecutor(Config{}, WithState(state)) + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: ctx, + Txs: [][]byte{transfer, emitLog}, + }) + + require.NoError(t, err) + require.Len(t, result.Txs, 2) + require.Len(t, result.Receipts, 2) + + require.Equal(t, transferTx.Hash(), result.Receipts[0].TxHash) + require.Equal(t, uint(0), result.Receipts[0].TransactionIndex) + require.Equal(t, ctx.BlockHash, result.Receipts[0].BlockHash) + require.Equal(t, new(big.Int).SetUint64(ctx.Number), result.Receipts[0].BlockNumber) + require.Equal(t, result.Txs[0].GasUsed, result.Receipts[0].CumulativeGasUsed) + + require.Equal(t, emitLogTx.Hash(), result.Receipts[1].TxHash) + require.Equal(t, uint(1), result.Receipts[1].TransactionIndex) + require.Equal(t, result.GasUsed, result.Receipts[1].CumulativeGasUsed) + require.Len(t, result.Receipts[1].Logs, 1) + log := result.Receipts[1].Logs[0] + require.Equal(t, logContract, log.Address) + require.Equal(t, ctx.Number, log.BlockNumber) + require.Equal(t, ctx.BlockHash, log.BlockHash) + require.Equal(t, emitLogTx.Hash(), log.TxHash) + require.Equal(t, uint(1), log.TxIndex) + require.Equal(t, uint(0), log.Index) + + state.ApplyChangeSet(result.ChangeSet) + require.Equal(t, big.NewInt(3), state.GetBalance(recipient)) + require.Equal(t, uint64(2), state.GetNonce(sender)) +} + +func TestExecutorEVMFailureProducesReceiptAndContinues(t *testing.T) { + chainID := big.NewInt(713715) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + oogContract := testAddress(0xc3) + recipient := testAddress(0xa6) + keySlot := testHash(0x01) + value := testHash(0x02) + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000_000_000_000)) + state.SetCode(oogContract, storeCode(keySlot, value)) + + oogCall := signLegacyTxWithGas(t, key, chainID, 0, &oogContract, big.NewInt(0), nil, 22_000) + laterTransfer := signLegacyTx(t, key, chainID, 1, &recipient, big.NewInt(5), nil) + executor := NewExecutor(Config{}, WithState(state)) + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: blockContext(chainID), + Txs: [][]byte{oogCall, laterTransfer}, + }) + + require.NoError(t, err) + require.Len(t, result.Txs, 2) + require.Equal(t, ethtypes.ReceiptStatusFailed, result.Txs[0].Status) + require.True(t, errors.Is(result.Txs[0].Err, vm.ErrOutOfGas)) + require.Equal(t, uint64(22_000), result.Txs[0].GasUsed) + require.Equal(t, ethtypes.ReceiptStatusSuccessful, result.Txs[1].Status) + require.Equal(t, result.GasUsed, result.Receipts[1].CumulativeGasUsed) + + state.ApplyChangeSet(result.ChangeSet) + require.Equal(t, common.Hash{}, state.GetState(oogContract, keySlot)) + require.Equal(t, big.NewInt(5), state.GetBalance(recipient)) + require.Equal(t, uint64(2), state.GetNonce(sender)) +} + +func TestExecutorValidationFailuresAbortBlock(t *testing.T) { + chainID := big.NewInt(713715) + recipient := testAddress(0xa7) + + t.Run("invalid nonce", func(t *testing.T) { + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000_000_000_000)) + rawTx := signLegacyTx(t, key, chainID, 1, &recipient, big.NewInt(1), nil) + executor := NewExecutor(Config{}, WithState(state)) + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: blockContext(chainID), + Txs: [][]byte{rawTx}, + }) + + require.Error(t, err) + require.True(t, errors.Is(err, core.ErrNonceTooHigh)) + require.Nil(t, result) + require.Equal(t, uint64(0), state.GetNonce(sender)) + require.Equal(t, big.NewInt(0), state.GetBalance(recipient)) + }) + + t.Run("insufficient balance", func(t *testing.T) { + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1)) + rawTx := signLegacyTx(t, key, chainID, 0, &recipient, big.NewInt(1), nil) + executor := NewExecutor(Config{}, WithState(state)) + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: blockContext(chainID), + Txs: [][]byte{rawTx}, + }) + + require.Error(t, err) + require.True(t, errors.Is(err, core.ErrInsufficientFunds)) + require.Nil(t, result) + require.Equal(t, uint64(0), state.GetNonce(sender)) + require.Equal(t, big.NewInt(0), state.GetBalance(recipient)) + }) +} + +func TestExecutorCreatesContractThenUpdatesStorage(t *testing.T) { + chainID := big.NewInt(713715) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + storageKey := testHash(0x11) + storageValue := testHash(0x22) + runtime := storeCode(storageKey, storageValue) + contractAddr := crypto.CreateAddress(sender, 0) + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(2_000_000_000_000_000)) + + createContract := signLegacyTxWithGas(t, key, chainID, 0, nil, big.NewInt(0), initCode(runtime), 300_000) + callContract := signLegacyTx(t, key, chainID, 1, &contractAddr, big.NewInt(0), nil) + executor := NewExecutor(Config{}, WithState(state)) + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: blockContext(chainID), + Txs: [][]byte{createContract, callContract}, + }) + + require.NoError(t, err) + require.Len(t, result.Receipts, 2) + require.Equal(t, ethtypes.ReceiptStatusSuccessful, result.Txs[0].Status) + require.Equal(t, contractAddr, result.Txs[0].ContractAddress) + require.Equal(t, contractAddr, result.Receipts[0].ContractAddress) + require.Equal(t, ethtypes.ReceiptStatusSuccessful, result.Txs[1].Status) + + state.ApplyChangeSet(result.ChangeSet) + require.Equal(t, runtime, state.GetCode(contractAddr)) + require.Equal(t, storageValue, state.GetState(contractAddr, storageKey)) + require.Equal(t, uint64(2), state.GetNonce(sender)) +} + +func TestExecutorCreateSelfDestructThenTransferSameAddress(t *testing.T) { + chainID := big.NewInt(713715) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + beneficiary := testAddress(0xb2) + runtime := selfDestructCode(beneficiary) + contractAddr := crypto.CreateAddress(sender, 0) + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(2_000_000_000_000_000)) + + createContract := signLegacyTxWithGas(t, key, chainID, 0, nil, big.NewInt(0), initCode(runtime), 300_000) + destroyContract := signLegacyTx(t, key, chainID, 1, &contractAddr, big.NewInt(0), nil) + transferToDestroyed := signLegacyTx(t, key, chainID, 2, &contractAddr, big.NewInt(9), nil) + executor := NewExecutor(Config{ + ChainConfig: legacySelfDestructChainConfig(chainID), + }, WithState(state)) + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: blockContext(chainID), + Txs: [][]byte{createContract, destroyContract, transferToDestroyed}, + }) + + require.NoError(t, err) + require.Len(t, result.Receipts, 3) + for _, txResult := range result.Txs { + require.Equal(t, ethtypes.ReceiptStatusSuccessful, txResult.Status) + } + + state.ApplyChangeSet(result.ChangeSet) + require.Empty(t, state.GetCode(contractAddr)) + require.Equal(t, big.NewInt(9), state.GetBalance(contractAddr)) + require.Equal(t, big.NewInt(0), state.GetBalance(beneficiary)) + require.Equal(t, uint64(3), state.GetNonce(sender)) +} + func TestExecutorFinalisesAfterEachTx(t *testing.T) { chainID := big.NewInt(713715) key, err := crypto.GenerateKey() @@ -181,11 +394,16 @@ func TestExecutorCustomPrecompilePlaceholder(t *testing.T) { } func signLegacyTx(t *testing.T, key *ecdsa.PrivateKey, chainID *big.Int, nonce uint64, to *common.Address, value *big.Int, data []byte) []byte { + t.Helper() + return signLegacyTxWithGas(t, key, chainID, nonce, to, value, data, 100_000) +} + +func signLegacyTxWithGas(t *testing.T, key *ecdsa.PrivateKey, chainID *big.Int, nonce uint64, to *common.Address, value *big.Int, data []byte, gas uint64) []byte { t.Helper() tx := ethtypes.NewTx(ðtypes.LegacyTx{ Nonce: nonce, GasPrice: big.NewInt(1_000_000_000), - Gas: 100_000, + Gas: gas, To: to, Value: value, Data: data, @@ -197,6 +415,13 @@ func signLegacyTx(t *testing.T, key *ecdsa.PrivateKey, chainID *big.Int, nonce u return raw } +func decodeTx(t *testing.T, raw []byte) *ethtypes.Transaction { + t.Helper() + var tx ethtypes.Transaction + require.NoError(t, tx.UnmarshalBinary(raw)) + return &tx +} + func signDynamicFeeTx(t *testing.T, key *ecdsa.PrivateKey, chainID *big.Int, nonce uint64, to *common.Address, value *big.Int, data []byte) []byte { t.Helper() tx := ethtypes.NewTx(ðtypes.DynamicFeeTx{ @@ -250,6 +475,42 @@ func selfDestructCode(beneficiary common.Address) []byte { return append(code, 0xff) } +func log0Code() []byte { + return []byte{0x60, 0x00, 0x60, 0x00, 0xa0, 0x00} +} + +func storeCode(key, value common.Hash) []byte { + code := append([]byte{0x7f}, value.Bytes()...) + code = append(code, 0x7f) + code = append(code, key.Bytes()...) + return append(code, 0x55, 0x00) +} + +func initCode(runtime []byte) []byte { + if len(runtime) > 255 { + panic("test runtime too large") + } + runtimeLen := byte(len(runtime)) //nolint:gosec // bounded by the check above. + code := []byte{ + 0x60, runtimeLen, + 0x60, 0x0c, + 0x60, 0x00, + 0x39, + 0x60, runtimeLen, + 0x60, 0x00, + 0xf3, + } + return append(code, runtime...) +} + +func testAddress(suffix byte) common.Address { + return common.BytesToAddress([]byte{suffix}) +} + +func testHash(suffix byte) common.Hash { + return common.BytesToHash([]byte{suffix}) +} + type staticPrecompileRegistry struct { addr common.Address } From 23fc6d37075a2deb6ab938b6e9a6a096c4966998 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Wed, 24 Jun 2026 11:47:45 +0800 Subject: [PATCH 10/29] add evmonly validation edge case tests --- giga/evmonly/executor_test.go | 237 +++++++++++++++++++++++++++++++++- 1 file changed, 232 insertions(+), 5 deletions(-) diff --git a/giga/evmonly/executor_test.go b/giga/evmonly/executor_test.go index 31c84bc00a..9e04a4fdc0 100644 --- a/giga/evmonly/executor_test.go +++ b/giga/evmonly/executor_test.go @@ -18,6 +18,8 @@ import ( "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles" ) +const testGasPriceWei = 1_000_000_000 + func TestExecutorEmptyBlock(t *testing.T) { executor := NewExecutor(Config{}) @@ -177,7 +179,7 @@ func TestExecutorValidationFailuresAbortBlock(t *testing.T) { chainID := big.NewInt(713715) recipient := testAddress(0xa7) - t.Run("invalid nonce", func(t *testing.T) { + t.Run("nonce too high", func(t *testing.T) { key, err := crypto.GenerateKey() require.NoError(t, err) sender := crypto.PubkeyToAddress(key.PublicKey) @@ -199,6 +201,29 @@ func TestExecutorValidationFailuresAbortBlock(t *testing.T) { require.Equal(t, big.NewInt(0), state.GetBalance(recipient)) }) + t.Run("nonce too low", func(t *testing.T) { + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000_000_000_000)) + state.SetNonce(sender, 1) + rawTx := signLegacyTx(t, key, chainID, 0, &recipient, big.NewInt(1), nil) + executor := NewExecutor(Config{}, WithState(state)) + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: blockContext(chainID), + Txs: [][]byte{rawTx}, + }) + + require.Error(t, err) + require.True(t, errors.Is(err, core.ErrNonceTooLow)) + require.Nil(t, result) + require.Equal(t, uint64(1), state.GetNonce(sender)) + require.Equal(t, big.NewInt(0), state.GetBalance(recipient)) + }) + t.Run("insufficient balance", func(t *testing.T) { key, err := crypto.GenerateKey() require.NoError(t, err) @@ -220,6 +245,169 @@ func TestExecutorValidationFailuresAbortBlock(t *testing.T) { require.Equal(t, uint64(0), state.GetNonce(sender)) require.Equal(t, big.NewInt(0), state.GetBalance(recipient)) }) + + t.Run("min gas price", func(t *testing.T) { + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000_000_000_000)) + rawTx := signLegacyTxWithGasPrice(t, key, chainID, 0, &recipient, big.NewInt(1), nil, 100_000, big.NewInt(1)) + executor := NewExecutor(Config{ + MinGasPrice: big.NewInt(2), + }, WithState(state)) + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: blockContext(chainID), + Txs: [][]byte{rawTx}, + }) + + require.Error(t, err) + require.True(t, errors.Is(err, errInsufficientGasPrice)) + require.Nil(t, result) + require.Equal(t, uint64(0), state.GetNonce(sender)) + require.Equal(t, big.NewInt(0), state.GetBalance(recipient)) + }) + + t.Run("fee cap below base fee", func(t *testing.T) { + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000_000_000_000)) + rawTx := signDynamicFeeTxWithFees( + t, + key, + chainID, + 0, + &recipient, + big.NewInt(1), + nil, + big.NewInt(testGasPriceWei), + big.NewInt(testGasPriceWei), + 100_000, + ) + executor := NewExecutor(Config{ + DisableGasPriceCheck: true, + }, WithState(state)) + ctx := blockContext(chainID) + ctx.BaseFee = big.NewInt(2 * testGasPriceWei) + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: ctx, + Txs: [][]byte{rawTx}, + }) + + require.Error(t, err) + require.True(t, errors.Is(err, core.ErrFeeCapTooLow)) + require.Nil(t, result) + require.Equal(t, uint64(0), state.GetNonce(sender)) + require.Equal(t, big.NewInt(0), state.GetBalance(recipient)) + }) + + t.Run("intrinsic gas too low", func(t *testing.T) { + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000_000_000_000)) + rawTx := signLegacyTxWithGas(t, key, chainID, 0, &recipient, big.NewInt(1), nil, 20_000) + executor := NewExecutor(Config{}, WithState(state)) + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: blockContext(chainID), + Txs: [][]byte{rawTx}, + }) + + require.Error(t, err) + require.True(t, errors.Is(err, core.ErrIntrinsicGas)) + require.Nil(t, result) + require.Equal(t, uint64(0), state.GetNonce(sender)) + require.Equal(t, big.NewInt(0), state.GetBalance(recipient)) + }) + + t.Run("block gas exhausted", func(t *testing.T) { + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000_000_000_000)) + firstTransfer := signLegacyTxWithGas(t, key, chainID, 0, &recipient, big.NewInt(1), nil, 21_000) + secondTransfer := signLegacyTxWithGas(t, key, chainID, 1, &recipient, big.NewInt(1), nil, 21_000) + executor := NewExecutor(Config{}, WithState(state)) + ctx := blockContext(chainID) + ctx.GasLimit = 30_000 + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: ctx, + Txs: [][]byte{firstTransfer, secondTransfer}, + }) + + require.Error(t, err) + require.True(t, errors.Is(err, core.ErrGasLimitReached)) + require.Nil(t, result) + require.Equal(t, uint64(0), state.GetNonce(sender)) + require.Equal(t, big.NewInt(0), state.GetBalance(recipient)) + }) +} + +func TestExecutorRejectsBadSignatureBeforeExecution(t *testing.T) { + chainID := big.NewInt(713715) + recipient := testAddress(0xa8) + + t.Run("wrong chain id", func(t *testing.T) { + wrongChainID := big.NewInt(1) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000_000_000_000)) + rawTx := signLegacyTx(t, key, wrongChainID, 0, &recipient, big.NewInt(1), nil) + executor := NewExecutor(Config{}, WithState(state)) + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: blockContext(chainID), + Txs: [][]byte{rawTx}, + }) + + require.Error(t, err) + require.True(t, errors.Is(err, ethtypes.ErrInvalidChainId)) + require.Nil(t, result) + require.Equal(t, uint64(0), state.GetNonce(sender)) + require.Equal(t, big.NewInt(0), state.GetBalance(recipient)) + }) + + t.Run("invalid signature values", func(t *testing.T) { + state := NewMemoryState() + rawTx := legacyTxWithSignatureValues( + t, + 0, + &recipient, + big.NewInt(1), + nil, + 100_000, + big.NewInt(testGasPriceWei), + new(big.Int).Add(big.NewInt(35), new(big.Int).Mul(big.NewInt(2), chainID)), + new(big.Int), + new(big.Int), + ) + executor := NewExecutor(Config{}, WithState(state)) + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: blockContext(chainID), + Txs: [][]byte{rawTx}, + }) + + require.Error(t, err) + require.True(t, errors.Is(err, ethtypes.ErrInvalidSig)) + require.Nil(t, result) + require.Equal(t, big.NewInt(0), state.GetBalance(recipient)) + }) } func TestExecutorCreatesContractThenUpdatesStorage(t *testing.T) { @@ -399,10 +587,15 @@ func signLegacyTx(t *testing.T, key *ecdsa.PrivateKey, chainID *big.Int, nonce u } func signLegacyTxWithGas(t *testing.T, key *ecdsa.PrivateKey, chainID *big.Int, nonce uint64, to *common.Address, value *big.Int, data []byte, gas uint64) []byte { + t.Helper() + return signLegacyTxWithGasPrice(t, key, chainID, nonce, to, value, data, gas, big.NewInt(testGasPriceWei)) +} + +func signLegacyTxWithGasPrice(t *testing.T, key *ecdsa.PrivateKey, chainID *big.Int, nonce uint64, to *common.Address, value *big.Int, data []byte, gas uint64, gasPrice *big.Int) []byte { t.Helper() tx := ethtypes.NewTx(ðtypes.LegacyTx{ Nonce: nonce, - GasPrice: big.NewInt(1_000_000_000), + GasPrice: new(big.Int).Set(gasPrice), Gas: gas, To: to, Value: value, @@ -415,6 +608,24 @@ func signLegacyTxWithGas(t *testing.T, key *ecdsa.PrivateKey, chainID *big.Int, return raw } +func legacyTxWithSignatureValues(t *testing.T, nonce uint64, to *common.Address, value *big.Int, data []byte, gas uint64, gasPrice *big.Int, v *big.Int, r *big.Int, s *big.Int) []byte { + t.Helper() + tx := ethtypes.NewTx(ðtypes.LegacyTx{ + Nonce: nonce, + GasPrice: new(big.Int).Set(gasPrice), + Gas: gas, + To: to, + Value: value, + Data: data, + V: new(big.Int).Set(v), + R: new(big.Int).Set(r), + S: new(big.Int).Set(s), + }) + raw, err := tx.MarshalBinary() + require.NoError(t, err) + return raw +} + func decodeTx(t *testing.T, raw []byte) *ethtypes.Transaction { t.Helper() var tx ethtypes.Transaction @@ -423,13 +634,29 @@ func decodeTx(t *testing.T, raw []byte) *ethtypes.Transaction { } func signDynamicFeeTx(t *testing.T, key *ecdsa.PrivateKey, chainID *big.Int, nonce uint64, to *common.Address, value *big.Int, data []byte) []byte { + t.Helper() + return signDynamicFeeTxWithFees( + t, + key, + chainID, + nonce, + to, + value, + data, + big.NewInt(testGasPriceWei), + big.NewInt(testGasPriceWei), + 100_000, + ) +} + +func signDynamicFeeTxWithFees(t *testing.T, key *ecdsa.PrivateKey, chainID *big.Int, nonce uint64, to *common.Address, value *big.Int, data []byte, gasTipCap *big.Int, gasFeeCap *big.Int, gas uint64) []byte { t.Helper() tx := ethtypes.NewTx(ðtypes.DynamicFeeTx{ ChainID: chainID, Nonce: nonce, - GasTipCap: big.NewInt(1_000_000_000), - GasFeeCap: big.NewInt(1_000_000_000), - Gas: 100_000, + GasTipCap: new(big.Int).Set(gasTipCap), + GasFeeCap: new(big.Int).Set(gasFeeCap), + Gas: gas, To: to, Value: value, Data: data, From 9c28df76b131dcd74670e5ce11ebd87d3e4844ab Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Wed, 24 Jun 2026 12:41:27 +0800 Subject: [PATCH 11/29] fix evmonly state finalization edge cases --- giga/evmonly/executor_test.go | 70 +++++++++++++++++++++++++++++++++++ giga/evmonly/state_db.go | 17 ++++++--- 2 files changed, 82 insertions(+), 5 deletions(-) diff --git a/giga/evmonly/executor_test.go b/giga/evmonly/executor_test.go index 9e04a4fdc0..440e9928f5 100644 --- a/giga/evmonly/executor_test.go +++ b/giga/evmonly/executor_test.go @@ -478,10 +478,44 @@ func TestExecutorCreateSelfDestructThenTransferSameAddress(t *testing.T) { state.ApplyChangeSet(result.ChangeSet) require.Empty(t, state.GetCode(contractAddr)) require.Equal(t, big.NewInt(9), state.GetBalance(contractAddr)) + require.Equal(t, uint64(0), state.GetNonce(contractAddr)) require.Equal(t, big.NewInt(0), state.GetBalance(beneficiary)) require.Equal(t, uint64(3), state.GetNonce(sender)) } +func TestExecutorEIP6780CreateFlagExpiresAfterTx(t *testing.T) { + chainID := big.NewInt(713715) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + beneficiary := testAddress(0xb3) + runtime := selfDestructCode(beneficiary) + contractAddr := crypto.CreateAddress(sender, 0) + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(2_000_000_000_000_000)) + + createContract := signLegacyTxWithGas(t, key, chainID, 0, nil, big.NewInt(0), initCode(runtime), 300_000) + selfDestructAfterCreateTx := signLegacyTx(t, key, chainID, 1, &contractAddr, big.NewInt(0), nil) + executor := NewExecutor(Config{}, WithState(state)) + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: blockContext(chainID), + Txs: [][]byte{createContract, selfDestructAfterCreateTx}, + }) + + require.NoError(t, err) + require.Len(t, result.Receipts, 2) + for _, txResult := range result.Txs { + require.Equal(t, ethtypes.ReceiptStatusSuccessful, txResult.Status) + } + + state.ApplyChangeSet(result.ChangeSet) + require.Equal(t, runtime, state.GetCode(contractAddr)) + require.Equal(t, uint64(1), state.GetNonce(contractAddr)) + require.Equal(t, big.NewInt(0), state.GetBalance(beneficiary)) +} + func TestExecutorFinalisesAfterEachTx(t *testing.T) { chainID := big.NewInt(713715) key, err := crypto.GenerateKey() @@ -545,6 +579,42 @@ func TestSnapshotRevertRestoresBaseState(t *testing.T) { require.Empty(t, stateDB.ChangeSet().Storage) } +func TestStateDBFirstStorageReadPreservesBase(t *testing.T) { + addr := testAddress(0xa9) + key := testHash(0x01) + value := testHash(0x02) + nextValue := testHash(0x03) + + t.Run("get state", func(t *testing.T) { + state := NewMemoryState() + state.SetState(addr, key, value) + stateDB := newNativeStateDB(state) + + require.Equal(t, value, stateDB.GetState(addr, key)) + require.Empty(t, stateDB.ChangeSet().Storage) + }) + + t.Run("get committed state", func(t *testing.T) { + state := NewMemoryState() + state.SetState(addr, key, value) + stateDB := newNativeStateDB(state) + + require.Equal(t, value, stateDB.GetCommittedState(addr, key)) + require.Empty(t, stateDB.ChangeSet().Storage) + }) + + t.Run("set state returns persisted previous value", func(t *testing.T) { + state := NewMemoryState() + state.SetState(addr, key, value) + stateDB := newNativeStateDB(state) + + require.Equal(t, value, stateDB.SetState(addr, key, nextValue)) + changes := stateDB.ChangeSet() + require.Len(t, changes.Storage, 1) + require.Equal(t, nextValue, changes.Storage[0].Value) + }) +} + func TestFinaliseClearsRefund(t *testing.T) { stateDB := newNativeStateDB(NewMemoryState()) stateDB.AddRefund(12) diff --git a/giga/evmonly/state_db.go b/giga/evmonly/state_db.go index 210a5ab7f7..0c2214a3fd 100644 --- a/giga/evmonly/state_db.go +++ b/giga/evmonly/state_db.go @@ -292,8 +292,9 @@ func (s *nativeStateDB) SelfDestruct(addr common.Address) uint256.Int { } func (s *nativeStateDB) SelfDestruct6780(addr common.Address) (uint256.Int, bool) { - if !s.account(addr).Created { - return *uint256.NewInt(0), false + acct := s.account(addr) + if !acct.Created { + return *acct.Balance.Clone(), false } return s.SelfDestruct(addr), true } @@ -422,7 +423,10 @@ func (s *nativeStateDB) Finalise(bool) { if acct.SelfDestructed { acct.Code = nil acct.Storage = map[common.Hash]common.Hash{} + acct.Nonce = 0 + acct.SelfDestructed = false } + acct.Created = false } s.refund = 0 } @@ -494,9 +498,12 @@ func (s *nativeStateDB) account(addr common.Address) *nativeAccount { if acct, ok := s.accounts[addr]; ok { return acct } - acct := s.loadAccount(addr) - s.accounts[addr] = acct.clone() - s.base[addr] = acct.clone() + base, ok := s.base[addr] + if !ok { + base = s.loadAccount(addr) + s.base[addr] = base.clone() + } + s.accounts[addr] = base.clone() return s.accounts[addr] } From 10c3108f790ebcb48f9a09916f36d2c77274a673 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Mon, 29 Jun 2026 20:32:15 +0800 Subject: [PATCH 12/29] add evmonly executor OCC --- giga/evmonly/README.md | 1 + giga/evmonly/config.go | 1 + giga/evmonly/executor.go | 18 ++ giga/evmonly/executor_test.go | 79 +++++++++ giga/evmonly/occ.go | 308 ++++++++++++++++++++++++++++++++++ giga/evmonly/state_db.go | 200 ++++++++++++++++++++-- 6 files changed, 591 insertions(+), 16 deletions(-) create mode 100644 giga/evmonly/occ.go diff --git a/giga/evmonly/README.md b/giga/evmonly/README.md index 928a203ce3..9c2f9a9a5d 100644 --- a/giga/evmonly/README.md +++ b/giga/evmonly/README.md @@ -28,6 +28,7 @@ The `evmonly` package currently provides: - go-ethereum `core.ApplyMessage` execution against an SDK-free `vm.StateDB` - key-addressable state reads for balance, nonce, code, and storage - deterministic post-block `StateChangeSet` construction +- optional executor-internal OCC for non-conflicting transaction sets - Ethereum receipt construction with logs, bloom, gas, tx hash, block metadata, contract address, and effective gas price - a map-backed `MemoryState` for tests and early integration diff --git a/giga/evmonly/config.go b/giga/evmonly/config.go index 8371b4c845..e376dfa2d3 100644 --- a/giga/evmonly/config.go +++ b/giga/evmonly/config.go @@ -15,6 +15,7 @@ type Config struct { MinGasPrice *big.Int ChainConfig *params.ChainConfig CustomPrecompiles precompiles.Registry + OCCWorkers int } func DefaultConfig() Config { diff --git a/giga/evmonly/executor.go b/giga/evmonly/executor.go index ad49d485f7..1d3599b8f8 100644 --- a/giga/evmonly/executor.go +++ b/giga/evmonly/executor.go @@ -51,7 +51,23 @@ func (e *Executor) ExecuteBlock(ctx context.Context, req BlockRequest) (*BlockRe if len(req.Txs) == 0 { return &BlockResult{}, nil } + if e.useOCC(req) { + return e.executeBlockOCC(ctx, req) + } + return e.executeBlockSequential(ctx, req) +} + +func (e *Executor) useOCC(req BlockRequest) bool { + if e.cfg.OCCWorkers <= 1 || len(req.Txs) <= 1 { + return false + } + if e.cfg.CustomPrecompiles == nil { + return true + } + return len(e.cfg.CustomPrecompiles.Addresses()) == 0 +} +func (e *Executor) executeBlockSequential(ctx context.Context, req BlockRequest) (*BlockResult, error) { chainConfig := e.chainConfig(req.Context) signer := ethtypes.MakeSigner(chainConfig, new(big.Int).SetUint64(req.Context.Number), req.Context.Time) parsed, err := parseBlockTxs(ctx, req.Txs, signer) @@ -93,6 +109,7 @@ func (e *Executor) ExecuteBlock(ctx context.Context, req BlockRequest) (*BlockRe result.GasUsed += txResult.GasUsed txIndexUint++ } + stateDB.clearSnapshots() stateDB.Finalise(true) result.ChangeSet = stateDB.ChangeSet() return result, nil @@ -133,6 +150,7 @@ func (e *Executor) executeTx( if err != nil { return TxResult{Hash: tx.Hash(), Sender: p.sender, To: tx.To(), Err: err}, nil, err } + stateDB.clearSnapshots() stateDB.Finalise(true) txLogs := append([]*ethtypes.Log(nil), stateDB.logs[logStart:]...) diff --git a/giga/evmonly/executor_test.go b/giga/evmonly/executor_test.go index 440e9928f5..04ef979cdf 100644 --- a/giga/evmonly/executor_test.go +++ b/giga/evmonly/executor_test.go @@ -85,6 +85,85 @@ func TestExecutorDynamicFeeTx(t *testing.T) { require.Equal(t, big.NewInt(11), state.GetBalance(recipient)) } +func TestExecutorOCCNonConflictingTransfersMatchSequential(t *testing.T) { + chainID := big.NewInt(713715) + txCount := 12 + rawTxs := make([][]byte, 0, txCount) + senders := make([]common.Address, 0, txCount) + recipients := make([]common.Address, 0, txCount) + seqState := NewMemoryState() + occState := NewMemoryState() + + for i := 0; i < txCount; i++ { + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + recipient := common.BigToAddress(big.NewInt(int64(10_000 + i))) + senders = append(senders, sender) + recipients = append(recipients, recipient) + seqState.SetBalance(sender, big.NewInt(1_000_000)) + occState.SetBalance(sender, big.NewInt(1_000_000)) + rawTxs = append(rawTxs, signLegacyTxWithGasPrice(t, key, chainID, 0, &recipient, big.NewInt(7), nil, 100_000, big.NewInt(0))) + } + + cfg := Config{MinGasPrice: big.NewInt(0)} + seqExecutor := NewExecutor(cfg, WithState(seqState)) + occExecutor := NewExecutor(Config{MinGasPrice: big.NewInt(0), OCCWorkers: 4}, WithState(occState)) + req := BlockRequest{Context: blockContext(chainID), Txs: rawTxs} + + seqResult, err := seqExecutor.ExecuteBlock(context.Background(), req) + require.NoError(t, err) + occResult, err := occExecutor.ExecuteBlock(context.Background(), req) + require.NoError(t, err) + + require.Equal(t, seqResult.GasUsed, occResult.GasUsed) + require.Len(t, occResult.Txs, txCount) + require.Len(t, occResult.Receipts, txCount) + for i := range txCount { + require.Equal(t, seqResult.Txs[i].Hash, occResult.Txs[i].Hash) + require.Equal(t, seqResult.Txs[i].Status, occResult.Txs[i].Status) + require.Equal(t, seqResult.Receipts[i].CumulativeGasUsed, occResult.Receipts[i].CumulativeGasUsed) + } + + seqState.ApplyChangeSet(seqResult.ChangeSet) + occState.ApplyChangeSet(occResult.ChangeSet) + for i := range txCount { + require.Equal(t, seqState.GetBalance(senders[i]), occState.GetBalance(senders[i])) + require.Equal(t, seqState.GetNonce(senders[i]), occState.GetNonce(senders[i])) + require.Equal(t, seqState.GetBalance(recipients[i]), occState.GetBalance(recipients[i])) + } +} + +func TestExecutorOCCConflictingTransfersMatchSequential(t *testing.T) { + chainID := big.NewInt(713715) + txCount := 8 + recipient := testAddress(0xdd) + rawTxs := make([][]byte, 0, txCount) + seqState := NewMemoryState() + occState := NewMemoryState() + + for i := 0; i < txCount; i++ { + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + seqState.SetBalance(sender, big.NewInt(1_000_000)) + occState.SetBalance(sender, big.NewInt(1_000_000)) + rawTxs = append(rawTxs, signLegacyTxWithGasPrice(t, key, chainID, 0, &recipient, big.NewInt(3), nil, 100_000, big.NewInt(0))) + } + + req := BlockRequest{Context: blockContext(chainID), Txs: rawTxs} + seqResult, err := NewExecutor(Config{MinGasPrice: big.NewInt(0)}, WithState(seqState)).ExecuteBlock(context.Background(), req) + require.NoError(t, err) + occResult, err := NewExecutor(Config{MinGasPrice: big.NewInt(0), OCCWorkers: 4}, WithState(occState)).ExecuteBlock(context.Background(), req) + require.NoError(t, err) + + seqState.ApplyChangeSet(seqResult.ChangeSet) + occState.ApplyChangeSet(occResult.ChangeSet) + require.Equal(t, seqResult.GasUsed, occResult.GasUsed) + require.Equal(t, seqState.GetBalance(recipient), occState.GetBalance(recipient)) + require.Equal(t, big.NewInt(int64(txCount*3)), occState.GetBalance(recipient)) +} + func TestExecutorReceiptAndLogMetadata(t *testing.T) { chainID := big.NewInt(713715) key, err := crypto.GenerateKey() diff --git a/giga/evmonly/occ.go b/giga/evmonly/occ.go new file mode 100644 index 0000000000..6edf0d9b87 --- /dev/null +++ b/giga/evmonly/occ.go @@ -0,0 +1,308 @@ +package evmonly + +import ( + "bytes" + "context" + "fmt" + "math" + "math/big" + "sort" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/params" + "golang.org/x/sync/errgroup" +) + +type occTxExecution struct { + txResult TxResult + receipt *ethtypes.Receipt + changeSet StateChangeSet + readSet map[stateAccessKey]struct{} + writeSet map[stateAccessKey]struct{} + gasUsed uint64 +} + +func (e *Executor) executeBlockOCC(ctx context.Context, req BlockRequest) (*BlockResult, error) { + chainConfig := e.chainConfig(req.Context) + signer := ethtypes.MakeSigner(chainConfig, new(big.Int).SetUint64(req.Context.Number), req.Context.Time) + blockCtx := buildBlockContext(req.Context) + baseFee := cloneBig(req.Context.BaseFee) + gasLimit := req.Context.GasLimit + if gasLimit == 0 { + gasLimit = math.MaxUint64 + } + + workers := e.cfg.OCCWorkers + if workers > len(req.Txs) { + workers = len(req.Txs) + } + results := make([]occTxExecution, len(req.Txs)) + jobs := make(chan int) + group, groupCtx := errgroup.WithContext(ctx) + + group.Go(func() error { + defer close(jobs) + for i := range req.Txs { + select { + case jobs <- i: + case <-groupCtx.Done(): + return groupCtx.Err() + } + } + return nil + }) + for range workers { + group.Go(func() error { + for { + select { + case <-groupCtx.Done(): + return groupCtx.Err() + case idx, ok := <-jobs: + if !ok { + return nil + } + result, err := e.executeTxSpeculative(groupCtx, req, idx, signer, chainConfig, blockCtx, baseFee, gasLimit) + if err != nil { + return err + } + results[idx] = result + } + } + }) + } + if err := group.Wait(); err != nil { + return nil, err + } + if !validateOCCResults(results, gasLimit) { + return e.executeBlockSequential(ctx, req) + } + return mergeOCCResults(results), nil +} + +func (e *Executor) executeTxSpeculative( + ctx context.Context, + req BlockRequest, + txIndex int, + signer ethtypes.Signer, + chainConfig *params.ChainConfig, + blockCtx vm.BlockContext, + baseFee *big.Int, + gasLimit uint64, +) (occTxExecution, error) { + tx, sender, err := parseTx(req.Txs[txIndex], signer) + if err != nil { + return occTxExecution{}, fmt.Errorf("parse tx %d: %w", txIndex, err) + } + stateDB := newNativeStateDB(e.state) + stateDB.enableAccessTracking() + evm := vm.NewEVM(blockCtx, stateDB, chainConfig, vm.Config{}, nil) + stateDB.SetEVM(evm) + gasPool := new(core.GasPool).AddGas(gasLimit) + txResult, receipt, err := e.executeTx( + evm, + stateDB, + gasPool, + req.Context, + parsedTx{tx: tx, sender: sender}, + txIndex, + uint(txIndex), + baseFee, + signer, + ) + if err != nil { + return occTxExecution{}, fmt.Errorf("execute tx %d %s: %w", txIndex, tx.Hash(), err) + } + readSet, writeSet := stateDB.accessSets() + return occTxExecution{ + txResult: txResult, + receipt: receipt, + changeSet: stateDB.ChangeSet(), + readSet: readSet, + writeSet: writeSet, + gasUsed: txResult.GasUsed, + }, nil +} + +func validateOCCResults(results []occTxExecution, gasLimit uint64) bool { + writes := newStateAccessIndex() + var totalGas uint64 + for _, result := range results { + if result.gasUsed > math.MaxUint64-totalGas { + return false + } + totalGas += result.gasUsed + if totalGas > gasLimit { + return false + } + if writes.conflictsWithAny(result.readSet) || writes.conflictsWithAny(result.writeSet) { + return false + } + writes.addAll(result.writeSet) + } + return true +} + +func mergeOCCResults(results []occTxExecution) *BlockResult { + blockResult := &BlockResult{ + Txs: make([]TxResult, len(results)), + Receipts: make(ethtypes.Receipts, len(results)), + } + var logIndex uint + for i, result := range results { + blockResult.GasUsed += result.gasUsed + result.txResult.CumulativeGasUsed = blockResult.GasUsed + result.receipt.CumulativeGasUsed = blockResult.GasUsed + for _, log := range result.receipt.Logs { + log.Index = logIndex + logIndex++ + } + blockResult.Txs[i] = result.txResult + blockResult.Receipts[i] = result.receipt + } + blockResult.ChangeSet = mergeChangeSets(results) + return blockResult +} + +type stateAccessIndex struct { + exact map[stateAccessKey]struct{} + account map[common.Address]struct{} + touched map[common.Address]struct{} +} + +func newStateAccessIndex() *stateAccessIndex { + return &stateAccessIndex{ + exact: map[stateAccessKey]struct{}{}, + account: map[common.Address]struct{}{}, + touched: map[common.Address]struct{}{}, + } +} + +func (i *stateAccessIndex) conflictsWithAny(set map[stateAccessKey]struct{}) bool { + for key := range set { + if i.conflictsWith(key) { + return true + } + } + return false +} + +func (i *stateAccessIndex) conflictsWith(key stateAccessKey) bool { + if _, ok := i.exact[key]; ok { + return true + } + if _, ok := i.account[key.address]; ok { + return true + } + if key.kind == stateAccessAccount { + _, ok := i.touched[key.address] + return ok + } + return false +} + +func (i *stateAccessIndex) addAll(set map[stateAccessKey]struct{}) { + for key := range set { + i.exact[key] = struct{}{} + i.touched[key.address] = struct{}{} + if key.kind == stateAccessAccount { + i.account[key.address] = struct{}{} + } + } +} + +type storageChangeKey struct { + address common.Address + key common.Hash +} + +func mergeChangeSets(results []occTxExecution) StateChangeSet { + balances := map[common.Address]*big.Int{} + nonces := map[common.Address]uint64{} + code := map[common.Address]CodeChange{} + storage := map[storageChangeKey]StorageChange{} + + for _, result := range results { + for _, change := range result.changeSet.Balances { + balances[change.Address] = cloneBig(change.Balance) + } + for _, change := range result.changeSet.Nonces { + nonces[change.Address] = change.Nonce + } + for _, change := range result.changeSet.Code { + code[change.Address] = CodeChange{ + Address: change.Address, + Code: cloneBytes(change.Code), + Delete: change.Delete, + } + } + for _, change := range result.changeSet.Storage { + storage[storageChangeKey{address: change.Address, key: change.Key}] = change + } + } + + var merged StateChangeSet + balanceAddrs := sortedAddressesFromBigMap(balances) + for _, addr := range balanceAddrs { + merged.Balances = append(merged.Balances, BalanceChange{Address: addr, Balance: cloneBig(balances[addr])}) + } + nonceAddrs := sortedAddressesFromUint64Map(nonces) + for _, addr := range nonceAddrs { + merged.Nonces = append(merged.Nonces, NonceChange{Address: addr, Nonce: nonces[addr]}) + } + codeAddrs := sortedAddressesFromCodeMap(code) + for _, addr := range codeAddrs { + change := code[addr] + change.Code = cloneBytes(change.Code) + merged.Code = append(merged.Code, change) + } + storageKeys := make([]storageChangeKey, 0, len(storage)) + for key := range storage { + storageKeys = append(storageKeys, key) + } + sort.Slice(storageKeys, func(i, j int) bool { + if cmp := bytes.Compare(storageKeys[i].address[:], storageKeys[j].address[:]); cmp != 0 { + return cmp < 0 + } + return bytes.Compare(storageKeys[i].key[:], storageKeys[j].key[:]) < 0 + }) + for _, key := range storageKeys { + merged.Storage = append(merged.Storage, storage[key]) + } + return merged +} + +func sortedAddressesFromBigMap(values map[common.Address]*big.Int) []common.Address { + addrs := make([]common.Address, 0, len(values)) + for addr := range values { + addrs = append(addrs, addr) + } + sort.Slice(addrs, func(i, j int) bool { + return bytes.Compare(addrs[i][:], addrs[j][:]) < 0 + }) + return addrs +} + +func sortedAddressesFromUint64Map(values map[common.Address]uint64) []common.Address { + addrs := make([]common.Address, 0, len(values)) + for addr := range values { + addrs = append(addrs, addr) + } + sort.Slice(addrs, func(i, j int) bool { + return bytes.Compare(addrs[i][:], addrs[j][:]) < 0 + }) + return addrs +} + +func sortedAddressesFromCodeMap(values map[common.Address]CodeChange) []common.Address { + addrs := make([]common.Address, 0, len(values)) + for addr := range values { + addrs = append(addrs, addr) + } + sort.Slice(addrs, func(i, j int) bool { + return bytes.Compare(addrs[i][:], addrs[j][:]) < 0 + }) + return addrs +} diff --git a/giga/evmonly/state_db.go b/giga/evmonly/state_db.go index 0c2214a3fd..f3a929042a 100644 --- a/giga/evmonly/state_db.go +++ b/giga/evmonly/state_db.go @@ -31,7 +31,11 @@ type nativeStateDB struct { accessList accessList transientStates map[common.Address]map[common.Hash]common.Hash + finaliseAddrs map[common.Address]struct{} + journal []nativeJournalEntry snapshots []nativeSnapshot + readSet map[stateAccessKey]struct{} + writeSet map[stateAccessKey]struct{} txHash common.Hash txIndex int @@ -50,21 +54,50 @@ type nativeAccount struct { } type nativeSnapshot struct { - accounts map[common.Address]*nativeAccount - base map[common.Address]*nativeAccount + journalLen int refund uint64 - logs []*ethtypes.Log + logsLen int accessList accessList transientStates map[common.Address]map[common.Hash]common.Hash + finaliseAddrs map[common.Address]struct{} preimages map[common.Hash][]byte + journaledAddrs map[common.Address]struct{} err error } +type nativeJournalKind uint8 + +const ( + nativeJournalAccount nativeJournalKind = iota +) + +type nativeJournalEntry struct { + kind nativeJournalKind + address common.Address + account *nativeAccount +} + type accessList struct { addresses map[common.Address]struct{} slots map[common.Address]map[common.Hash]struct{} } +type stateAccessKind uint8 + +const ( + stateAccessAccount stateAccessKind = iota + stateAccessBalance + stateAccessNonce + stateAccessCode + stateAccessStorage +) + +type stateAccessKey struct { + kind stateAccessKind + address common.Address + slot common.Hash +} + func newNativeStateDB(source StateReader) *nativeStateDB { if source == nil { source = NewMemoryState() @@ -76,6 +109,7 @@ func newNativeStateDB(source StateReader) *nativeStateDB { preimages: map[common.Hash][]byte{}, accessList: newAccessList(), transientStates: map[common.Address]map[common.Hash]common.Hash{}, + finaliseAddrs: map[common.Address]struct{}{}, } } @@ -132,16 +166,23 @@ func (s *nativeStateDB) ChangeSet() StateChangeSet { func (s *nativeStateDB) CreateAccount(addr common.Address) { acct := s.account(addr) + s.recordAccount(addr) + s.markWrite(stateAccessKey{kind: stateAccessAccount, address: addr}) balance := acct.Balance.Clone() *acct = nativeAccount{ Balance: balance, Storage: map[common.Hash]common.Hash{}, Created: true, } + s.markForFinalise(addr) } func (s *nativeStateDB) CreateContract(addr common.Address) { - s.account(addr).Created = true + acct := s.account(addr) + s.recordAccount(addr) + s.markWrite(stateAccessKey{kind: stateAccessAccount, address: addr}) + acct.Created = true + s.markForFinalise(addr) } func (s *nativeStateDB) SubBalance(addr common.Address, amount *uint256.Int, _ tracing.BalanceChangeReason) uint256.Int { @@ -154,6 +195,8 @@ func (s *nativeStateDB) SubBalance(addr common.Address, amount *uint256.Int, _ t s.err = errInsufficientBalance return prev } + s.recordAccount(addr) + s.markWrite(stateAccessKey{kind: stateAccessBalance, address: addr}) acct.Balance.Sub(acct.Balance, amount) return prev } @@ -164,16 +207,21 @@ func (s *nativeStateDB) AddBalance(addr common.Address, amount *uint256.Int, _ t return prev } acct := s.account(addr) + s.recordAccount(addr) + s.markWrite(stateAccessKey{kind: stateAccessBalance, address: addr}) acct.Balance.Add(acct.Balance, amount) return prev } func (s *nativeStateDB) GetBalance(addr common.Address) *uint256.Int { + s.markRead(stateAccessKey{kind: stateAccessBalance, address: addr}) return s.account(addr).Balance.Clone() } func (s *nativeStateDB) SetBalance(addr common.Address, balance *uint256.Int, _ tracing.BalanceChangeReason) { acct := s.account(addr) + s.recordAccount(addr) + s.markWrite(stateAccessKey{kind: stateAccessBalance, address: addr}) if balance == nil { acct.Balance = uint256.NewInt(0) return @@ -182,11 +230,15 @@ func (s *nativeStateDB) SetBalance(addr common.Address, balance *uint256.Int, _ } func (s *nativeStateDB) GetNonce(addr common.Address) uint64 { + s.markRead(stateAccessKey{kind: stateAccessNonce, address: addr}) return s.account(addr).Nonce } func (s *nativeStateDB) SetNonce(addr common.Address, nonce uint64, _ tracing.NonceChangeReason) { - s.account(addr).Nonce = nonce + acct := s.account(addr) + s.recordAccount(addr) + s.markWrite(stateAccessKey{kind: stateAccessNonce, address: addr}) + acct.Nonce = nonce } func (s *nativeStateDB) GetCodeHash(addr common.Address) common.Hash { @@ -198,17 +250,21 @@ func (s *nativeStateDB) GetCodeHash(addr common.Address) common.Hash { } func (s *nativeStateDB) GetCode(addr common.Address) []byte { + s.markRead(stateAccessKey{kind: stateAccessCode, address: addr}) return cloneBytes(s.account(addr).Code) } func (s *nativeStateDB) SetCode(addr common.Address, code []byte) []byte { acct := s.account(addr) prev := cloneBytes(acct.Code) + s.recordAccount(addr) + s.markWrite(stateAccessKey{kind: stateAccessCode, address: addr}) acct.Code = cloneBytes(code) return prev } func (s *nativeStateDB) GetCodeSize(addr common.Address) int { + s.markRead(stateAccessKey{kind: stateAccessCode, address: addr}) return len(s.account(addr).Code) } @@ -228,11 +284,13 @@ func (s *nativeStateDB) GetRefund() uint64 { } func (s *nativeStateDB) GetCommittedState(addr common.Address, key common.Hash) common.Hash { + s.markRead(stateAccessKey{kind: stateAccessStorage, address: addr, slot: key}) s.ensureStorage(addr, key) return s.baseAccount(addr).Storage[key] } func (s *nativeStateDB) GetState(addr common.Address, key common.Hash) common.Hash { + s.markRead(stateAccessKey{kind: stateAccessStorage, address: addr, slot: key}) s.ensureStorage(addr, key) return s.account(addr).Storage[key] } @@ -241,6 +299,8 @@ func (s *nativeStateDB) SetState(addr common.Address, key common.Hash, value com s.ensureStorage(addr, key) acct := s.account(addr) prev := acct.Storage[key] + s.recordAccount(addr) + s.markWrite(stateAccessKey{kind: stateAccessStorage, address: addr, slot: key}) if value == (common.Hash{}) { delete(acct.Storage, key) } else { @@ -251,6 +311,8 @@ func (s *nativeStateDB) SetState(addr common.Address, key common.Hash, value com func (s *nativeStateDB) SetStorage(addr common.Address, states map[common.Hash]common.Hash) { acct := s.account(addr) + s.recordAccount(addr) + s.markWrite(stateAccessKey{kind: stateAccessAccount, address: addr}) acct.Storage = map[common.Hash]common.Hash{} for key, value := range states { if value != (common.Hash{}) { @@ -286,8 +348,11 @@ func (s *nativeStateDB) SetTransientState(addr common.Address, key, value common func (s *nativeStateDB) SelfDestruct(addr common.Address) uint256.Int { acct := s.account(addr) prev := *acct.Balance.Clone() + s.recordAccount(addr) + s.markWrite(stateAccessKey{kind: stateAccessAccount, address: addr}) acct.Balance.Clear() acct.SelfDestructed = true + s.markForFinalise(addr) return prev } @@ -304,11 +369,13 @@ func (s *nativeStateDB) HasSelfDestructed(addr common.Address) bool { } func (s *nativeStateDB) Exist(addr common.Address) bool { + s.markRead(stateAccessKey{kind: stateAccessAccount, address: addr}) acct := s.account(addr) return acct.SelfDestructed || acct.Nonce != 0 || !acct.Balance.IsZero() || len(acct.Code) != 0 } func (s *nativeStateDB) Empty(addr common.Address) bool { + s.markRead(stateAccessKey{kind: stateAccessAccount, address: addr}) acct := s.account(addr) return acct.Nonce == 0 && acct.Balance.IsZero() && len(acct.Code) == 0 } @@ -371,13 +438,14 @@ func (s *nativeStateDB) PointCache() *ethutils.PointCache { func (s *nativeStateDB) Snapshot() int { id := len(s.snapshots) s.snapshots = append(s.snapshots, nativeSnapshot{ - accounts: cloneAccounts(s.accounts), - base: cloneAccounts(s.base), + journalLen: len(s.journal), refund: s.refund, - logs: append([]*ethtypes.Log(nil), s.logs...), + logsLen: len(s.logs), accessList: cloneAccessList(s.accessList), transientStates: cloneTransientStates(s.transientStates), + finaliseAddrs: cloneAddressSet(s.finaliseAddrs), preimages: clonePreimages(s.preimages), + journaledAddrs: map[common.Address]struct{}{}, err: s.err, }) return id @@ -388,12 +456,15 @@ func (s *nativeStateDB) RevertToSnapshot(id int) { panic("invalid state snapshot") } snapshot := s.snapshots[id] - s.accounts = cloneAccounts(snapshot.accounts) - s.base = cloneAccounts(snapshot.base) + for i := len(s.journal) - 1; i >= snapshot.journalLen; i-- { + s.journal[i].revert(s) + } + s.journal = s.journal[:snapshot.journalLen] s.refund = snapshot.refund - s.logs = append([]*ethtypes.Log(nil), snapshot.logs...) + s.logs = s.logs[:snapshot.logsLen] s.accessList = cloneAccessList(snapshot.accessList) s.transientStates = cloneTransientStates(snapshot.transientStates) + s.finaliseAddrs = cloneAddressSet(snapshot.finaliseAddrs) s.preimages = clonePreimages(snapshot.preimages) s.err = snapshot.err s.snapshots = s.snapshots[:id] @@ -419,15 +490,21 @@ func (s *nativeStateDB) AccessEvents() *vm.AccessEvents { } func (s *nativeStateDB) Finalise(bool) { - for _, acct := range s.accounts { + for addr := range s.finaliseAddrs { + acct := s.account(addr) if acct.SelfDestructed { + s.recordAccount(addr) acct.Code = nil acct.Storage = map[common.Hash]common.Hash{} acct.Nonce = 0 acct.SelfDestructed = false } - acct.Created = false + if acct.Created { + s.recordAccount(addr) + acct.Created = false + } } + clear(s.finaliseAddrs) s.refund = 0 } @@ -460,7 +537,11 @@ func (s *nativeStateDB) Copy() vm.StateDB { preimages: clonePreimages(s.preimages), accessList: cloneAccessList(s.accessList), transientStates: cloneTransientStates(s.transientStates), + finaliseAddrs: cloneAddressSet(s.finaliseAddrs), + journal: cloneJournal(s.journal), snapshots: cloneSnapshots(s.snapshots), + readSet: cloneAccessSet(s.readSet), + writeSet: cloneAccessSet(s.writeSet), txHash: s.txHash, txIndex: s.txIndex, txIndexUint: s.txIndexUint, @@ -494,6 +575,61 @@ func (s *nativeStateDB) SetEVM(evm *vm.EVM) { s.evm = evm } +func (s *nativeStateDB) enableAccessTracking() { + s.readSet = map[stateAccessKey]struct{}{} + s.writeSet = map[stateAccessKey]struct{}{} +} + +func (s *nativeStateDB) accessSets() (map[stateAccessKey]struct{}, map[stateAccessKey]struct{}) { + return cloneAccessSet(s.readSet), cloneAccessSet(s.writeSet) +} + +func (s *nativeStateDB) markRead(key stateAccessKey) { + if s.readSet != nil { + s.readSet[key] = struct{}{} + } +} + +func (s *nativeStateDB) markWrite(key stateAccessKey) { + if s.writeSet != nil { + s.writeSet[key] = struct{}{} + } +} + +func (s *nativeStateDB) recordAccount(addr common.Address) { + if len(s.snapshots) == 0 { + return + } + snapshot := &s.snapshots[len(s.snapshots)-1] + if _, ok := snapshot.journaledAddrs[addr]; ok { + return + } + snapshot.journaledAddrs[addr] = struct{}{} + s.journal = append(s.journal, nativeJournalEntry{ + kind: nativeJournalAccount, + address: addr, + account: s.account(addr).clone(), + }) +} + +func (e nativeJournalEntry) revert(s *nativeStateDB) { + switch e.kind { + case nativeJournalAccount: + s.accounts[e.address] = e.account.clone() + default: + panic("unknown native state journal entry") + } +} + +func (s *nativeStateDB) markForFinalise(addr common.Address) { + s.finaliseAddrs[addr] = struct{}{} +} + +func (s *nativeStateDB) clearSnapshots() { + s.journal = s.journal[:0] + s.snapshots = s.snapshots[:0] +} + func (s *nativeStateDB) account(addr common.Address) *nativeAccount { if acct, ok := s.accounts[addr]; ok { return acct @@ -602,6 +738,25 @@ func cloneTransientStates(states map[common.Address]map[common.Hash]common.Hash) return cp } +func cloneAddressSet(addrs map[common.Address]struct{}) map[common.Address]struct{} { + cp := make(map[common.Address]struct{}, len(addrs)) + for addr := range addrs { + cp[addr] = struct{}{} + } + return cp +} + +func cloneAccessSet(set map[stateAccessKey]struct{}) map[stateAccessKey]struct{} { + if set == nil { + return nil + } + cp := make(map[stateAccessKey]struct{}, len(set)) + for key := range set { + cp[key] = struct{}{} + } + return cp +} + func clonePreimages(preimages map[common.Hash][]byte) map[common.Hash][]byte { cp := make(map[common.Hash][]byte, len(preimages)) for hash, preimage := range preimages { @@ -610,17 +765,30 @@ func clonePreimages(preimages map[common.Hash][]byte) map[common.Hash][]byte { return cp } +func cloneJournal(journal []nativeJournalEntry) []nativeJournalEntry { + cp := make([]nativeJournalEntry, len(journal)) + for i, entry := range journal { + cp[i] = nativeJournalEntry{ + kind: entry.kind, + address: entry.address, + account: entry.account.clone(), + } + } + return cp +} + func cloneSnapshots(snapshots []nativeSnapshot) []nativeSnapshot { cp := make([]nativeSnapshot, len(snapshots)) for i, snapshot := range snapshots { cp[i] = nativeSnapshot{ - accounts: cloneAccounts(snapshot.accounts), - base: cloneAccounts(snapshot.base), + journalLen: snapshot.journalLen, refund: snapshot.refund, - logs: append([]*ethtypes.Log(nil), snapshot.logs...), + logsLen: snapshot.logsLen, accessList: cloneAccessList(snapshot.accessList), transientStates: cloneTransientStates(snapshot.transientStates), + finaliseAddrs: cloneAddressSet(snapshot.finaliseAddrs), preimages: clonePreimages(snapshot.preimages), + journaledAddrs: cloneAddressSet(snapshot.journaledAddrs), err: snapshot.err, } } From f1fa418bffdea98376e05a061cc19f5b959eed2e Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Tue, 30 Jun 2026 14:10:29 +0800 Subject: [PATCH 13/29] optimize evmonly OCC scheduling --- giga/evmonly/occ.go | 55 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 42 insertions(+), 13 deletions(-) diff --git a/giga/evmonly/occ.go b/giga/evmonly/occ.go index 6edf0d9b87..f37e6b71b7 100644 --- a/giga/evmonly/occ.go +++ b/giga/evmonly/occ.go @@ -25,6 +25,11 @@ type occTxExecution struct { gasUsed uint64 } +type occTxRange struct { + start int + end int +} + func (e *Executor) executeBlockOCC(ctx context.Context, req BlockRequest) (*BlockResult, error) { chainConfig := e.chainConfig(req.Context) signer := ethtypes.MakeSigner(chainConfig, new(big.Int).SetUint64(req.Context.Number), req.Context.Time) @@ -36,18 +41,24 @@ func (e *Executor) executeBlockOCC(ctx context.Context, req BlockRequest) (*Bloc } workers := e.cfg.OCCWorkers - if workers > len(req.Txs) { - workers = len(req.Txs) + txCount := len(req.Txs) + if workers > txCount { + workers = txCount } - results := make([]occTxExecution, len(req.Txs)) - jobs := make(chan int) + results := make([]occTxExecution, txCount) + chunkSize := occChunkSize(txCount, workers) + jobs := make(chan occTxRange) group, groupCtx := errgroup.WithContext(ctx) group.Go(func() error { defer close(jobs) - for i := range req.Txs { + for start := 0; start < txCount; start += chunkSize { + end := start + chunkSize + if end > txCount { + end = txCount + } select { - case jobs <- i: + case jobs <- occTxRange{start: start, end: end}: case <-groupCtx.Done(): return groupCtx.Err() } @@ -60,15 +71,17 @@ func (e *Executor) executeBlockOCC(ctx context.Context, req BlockRequest) (*Bloc select { case <-groupCtx.Done(): return groupCtx.Err() - case idx, ok := <-jobs: + case txRange, ok := <-jobs: if !ok { return nil } - result, err := e.executeTxSpeculative(groupCtx, req, idx, signer, chainConfig, blockCtx, baseFee, gasLimit) - if err != nil { - return err + for idx := txRange.start; idx < txRange.end; idx++ { + result, err := e.executeTxSpeculative(groupCtx, req, idx, signer, chainConfig, blockCtx, baseFee, gasLimit) + if err != nil { + return err + } + results[idx] = result } - results[idx] = result } } }) @@ -82,6 +95,21 @@ func (e *Executor) executeBlockOCC(ctx context.Context, req BlockRequest) (*Bloc return mergeOCCResults(results), nil } +func occChunkSize(txCount int, workers int) int { + if txCount <= 0 || workers <= 0 { + return 1 + } + targetChunks := workers * 8 + chunkSize := (txCount + targetChunks - 1) / targetChunks + if chunkSize < 16 { + return 16 + } + if chunkSize > 256 { + return 256 + } + return chunkSize +} + func (e *Executor) executeTxSpeculative( ctx context.Context, req BlockRequest, @@ -96,6 +124,7 @@ func (e *Executor) executeTxSpeculative( if err != nil { return occTxExecution{}, fmt.Errorf("parse tx %d: %w", txIndex, err) } + p := parsedTx{tx: tx, sender: sender} stateDB := newNativeStateDB(e.state) stateDB.enableAccessTracking() evm := vm.NewEVM(blockCtx, stateDB, chainConfig, vm.Config{}, nil) @@ -106,14 +135,14 @@ func (e *Executor) executeTxSpeculative( stateDB, gasPool, req.Context, - parsedTx{tx: tx, sender: sender}, + p, txIndex, uint(txIndex), baseFee, signer, ) if err != nil { - return occTxExecution{}, fmt.Errorf("execute tx %d %s: %w", txIndex, tx.Hash(), err) + return occTxExecution{}, fmt.Errorf("execute tx %d %s: %w", txIndex, p.tx.Hash(), err) } readSet, writeSet := stateDB.accessSets() return occTxExecution{ From dd519f70d6fa217eff661ea7963cbf91639f93a2 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Thu, 2 Jul 2026 13:51:06 +0800 Subject: [PATCH 14/29] add evmonly executor result sink --- giga/evmonly/executor.go | 41 ++++++++++++++++++++++++----- giga/evmonly/executor_test.go | 49 +++++++++++++++++++++++++++++++++++ giga/evmonly/types.go | 6 +++++ 3 files changed, 90 insertions(+), 6 deletions(-) diff --git a/giga/evmonly/executor.go b/giga/evmonly/executor.go index 1d3599b8f8..8e8183fb42 100644 --- a/giga/evmonly/executor.go +++ b/giga/evmonly/executor.go @@ -18,8 +18,9 @@ import ( // Executor runs raw EVM transactions against an EVM-native state backend. type Executor struct { - cfg Config - state StateReader + cfg Config + state StateReader + resultSink ResultSink } type Option func(*Executor) @@ -32,6 +33,12 @@ func WithState(state StateReader) Option { } } +func WithResultSink(sink ResultSink) Option { + return func(e *Executor) { + e.resultSink = sink + } +} + func NewExecutor(cfg Config, opts ...Option) *Executor { e := &Executor{ cfg: cfg.WithDefaults(), @@ -48,13 +55,35 @@ func (e *Executor) Config() Config { } func (e *Executor) ExecuteBlock(ctx context.Context, req BlockRequest) (*BlockResult, error) { + var result *BlockResult + var err error if len(req.Txs) == 0 { - return &BlockResult{}, nil + result = &BlockResult{} + } else if e.useOCC(req) { + result, err = e.executeBlockOCC(ctx, req) + } else { + result, err = e.executeBlockSequential(ctx, req) + } + if err != nil { + return nil, err + } + if err := e.sinkBlockResult(ctx, req.Context.Number, result); err != nil { + return nil, err + } + return result, nil +} + +func (e *Executor) sinkBlockResult(ctx context.Context, height uint64, result *BlockResult) error { + if e.resultSink == nil || result == nil { + return nil + } + if err := e.resultSink.StoreChangeSet(ctx, height, result.ChangeSet); err != nil { + return fmt.Errorf("store changeset for block %d: %w", height, err) } - if e.useOCC(req) { - return e.executeBlockOCC(ctx, req) + if err := e.resultSink.StoreReceipts(ctx, height, result.Receipts); err != nil { + return fmt.Errorf("store receipts for block %d: %w", height, err) } - return e.executeBlockSequential(ctx, req) + return nil } func (e *Executor) useOCC(req BlockRequest) bool { diff --git a/giga/evmonly/executor_test.go b/giga/evmonly/executor_test.go index 04ef979cdf..998a4783f0 100644 --- a/giga/evmonly/executor_test.go +++ b/giga/evmonly/executor_test.go @@ -20,6 +20,25 @@ import ( const testGasPriceWei = 1_000_000_000 +type recordingResultSink struct { + changeSetHeights []uint64 + receiptHeights []uint64 + changeSets []StateChangeSet + receipts []ethtypes.Receipts +} + +func (s *recordingResultSink) StoreChangeSet(_ context.Context, height uint64, changeSet StateChangeSet) error { + s.changeSetHeights = append(s.changeSetHeights, height) + s.changeSets = append(s.changeSets, changeSet) + return nil +} + +func (s *recordingResultSink) StoreReceipts(_ context.Context, height uint64, receipts ethtypes.Receipts) error { + s.receiptHeights = append(s.receiptHeights, height) + s.receipts = append(s.receipts, receipts) + return nil +} + func TestExecutorEmptyBlock(t *testing.T) { executor := NewExecutor(Config{}) @@ -59,6 +78,36 @@ func TestExecutorTransferTx(t *testing.T) { require.Equal(t, uint64(1), state.GetNonce(sender)) } +func TestExecutorInvokesResultSink(t *testing.T) { + chainID := big.NewInt(713715) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + recipient := common.HexToAddress("0x00000000000000000000000000000000000000a7") + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(200_000_000_000_000)) + sink := &recordingResultSink{} + + rawTx := signLegacyTx(t, key, chainID, 0, &recipient, big.NewInt(7), nil) + executor := NewExecutor(Config{}, WithState(state), WithResultSink(sink)) + ctx := blockContext(chainID) + ctx.Number = 77 + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: ctx, + Txs: [][]byte{rawTx}, + }) + + require.NoError(t, err) + require.Len(t, sink.changeSets, 1) + require.Len(t, sink.receipts, 1) + require.Equal(t, []uint64{ctx.Number}, sink.changeSetHeights) + require.Equal(t, []uint64{ctx.Number}, sink.receiptHeights) + require.Equal(t, result.ChangeSet, sink.changeSets[0]) + require.Equal(t, result.Receipts, sink.receipts[0]) +} + func TestExecutorDynamicFeeTx(t *testing.T) { chainID := big.NewInt(713715) key, err := crypto.GenerateKey() diff --git a/giga/evmonly/types.go b/giga/evmonly/types.go index 708c10e91f..f081c71a44 100644 --- a/giga/evmonly/types.go +++ b/giga/evmonly/types.go @@ -13,6 +13,12 @@ type BlockExecutor interface { ExecuteBlock(context.Context, BlockRequest) (*BlockResult, error) } +// ResultSink persists executor-produced block outputs. +type ResultSink interface { + StoreChangeSet(ctx context.Context, height uint64, changeSet StateChangeSet) error + StoreReceipts(ctx context.Context, height uint64, receipts ethtypes.Receipts) error +} + // BlockRequest contains all consensus/runtime inputs needed to execute a block. // Txs must be raw Ethereum transaction RLP bytes. type BlockRequest struct { From da985fc25d02f81e5ccdaf18981a0dcf08c72021 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Thu, 2 Jul 2026 16:37:25 +0800 Subject: [PATCH 15/29] pipeline evmonly sender recovery --- giga/evmonly/README.md | 13 ++++++ giga/evmonly/executor.go | 90 +++++++++++++++++++++++++++++----------- giga/evmonly/occ.go | 17 +++----- giga/evmonly/parser.go | 11 ++--- giga/evmonly/types.go | 22 ++++++++++ 5 files changed, 108 insertions(+), 45 deletions(-) diff --git a/giga/evmonly/README.md b/giga/evmonly/README.md index 9c2f9a9a5d..b748f13247 100644 --- a/giga/evmonly/README.md +++ b/giga/evmonly/README.md @@ -45,6 +45,19 @@ The boundary is: ExecuteBlock(context.Context, BlockRequest) (*BlockResult, error) ``` +For callers that can pipeline stateless work across blocks, the concrete +executor also supports: + +```go +PrepareBlock(context.Context, BlockRequest) (PreparedBlock, error) +ExecutePreparedBlock(context.Context, PreparedBlock) (*BlockResult, error) +``` + +`PrepareBlock` decodes transaction RLP and recovers senders. This work does not +touch state, so block N+1 can be prepared while block N is still executing. +`ExecuteBlock` remains the convenience path and performs prepare then execute in +one call. + The executor should be commit-neutral. It executes an ordered EVM block and returns the state writes and receipts produced by that block. The caller owns durable persistence, state commitment, block indexing, and receipt publication. diff --git a/giga/evmonly/executor.go b/giga/evmonly/executor.go index 8e8183fb42..13d0204960 100644 --- a/giga/evmonly/executor.go +++ b/giga/evmonly/executor.go @@ -55,11 +55,32 @@ func (e *Executor) Config() Config { } func (e *Executor) ExecuteBlock(ctx context.Context, req BlockRequest) (*BlockResult, error) { + prepared, err := e.PrepareBlock(ctx, req) + if err != nil { + return nil, err + } + return e.ExecutePreparedBlock(ctx, prepared) +} + +func (e *Executor) PrepareBlock(ctx context.Context, req BlockRequest) (PreparedBlock, error) { + chainConfig := e.chainConfig(req.Context) + signer := ethtypes.MakeSigner(chainConfig, new(big.Int).SetUint64(req.Context.Number), req.Context.Time) + parsed, err := parseBlockTxs(ctx, req.Txs, signer) + if err != nil { + return PreparedBlock{}, err + } + return PreparedBlock{ + Context: req.Context, + Txs: parsed, + }, nil +} + +func (e *Executor) ExecutePreparedBlock(ctx context.Context, req PreparedBlock) (*BlockResult, error) { var result *BlockResult var err error if len(req.Txs) == 0 { result = &BlockResult{} - } else if e.useOCC(req) { + } else if e.useOCC(len(req.Txs)) { result, err = e.executeBlockOCC(ctx, req) } else { result, err = e.executeBlockSequential(ctx, req) @@ -86,8 +107,8 @@ func (e *Executor) sinkBlockResult(ctx context.Context, height uint64, result *B return nil } -func (e *Executor) useOCC(req BlockRequest) bool { - if e.cfg.OCCWorkers <= 1 || len(req.Txs) <= 1 { +func (e *Executor) useOCC(txCount int) bool { + if e.cfg.OCCWorkers <= 1 || txCount <= 1 { return false } if e.cfg.CustomPrecompiles == nil { @@ -96,13 +117,8 @@ func (e *Executor) useOCC(req BlockRequest) bool { return len(e.cfg.CustomPrecompiles.Addresses()) == 0 } -func (e *Executor) executeBlockSequential(ctx context.Context, req BlockRequest) (*BlockResult, error) { +func (e *Executor) executeBlockSequential(ctx context.Context, req PreparedBlock) (*BlockResult, error) { chainConfig := e.chainConfig(req.Context) - signer := ethtypes.MakeSigner(chainConfig, new(big.Int).SetUint64(req.Context.Number), req.Context.Time) - parsed, err := parseBlockTxs(ctx, req.Txs, signer) - if err != nil { - return nil, err - } stateDB := newNativeStateDB(e.state) blockCtx := buildBlockContext(req.Context) @@ -117,19 +133,19 @@ func (e *Executor) executeBlockSequential(ctx context.Context, req BlockRequest) baseFee := cloneBig(req.Context.BaseFee) result := &BlockResult{ - Txs: make([]TxResult, 0, len(parsed)), - Receipts: make(ethtypes.Receipts, 0, len(parsed)), + Txs: make([]TxResult, 0, len(req.Txs)), + Receipts: make(ethtypes.Receipts, 0, len(req.Txs)), } var txIndexUint uint - for txIndex, p := range parsed { + for txIndex, p := range req.Txs { select { case <-ctx.Done(): return nil, ctx.Err() default: } - txResult, receipt, err := e.executeTx(evm, stateDB, gasPool, req.Context, p, txIndex, txIndexUint, baseFee, signer) + txResult, receipt, err := e.executeTx(evm, stateDB, gasPool, req.Context, p, txIndex, txIndexUint, baseFee) if err != nil { - return nil, fmt.Errorf("execute tx %d %s: %w", txIndex, p.tx.Hash(), err) + return nil, fmt.Errorf("execute tx %d %s: %w", txIndex, p.Tx.Hash(), err) } txResult.CumulativeGasUsed = result.GasUsed + txResult.GasUsed receipt.CumulativeGasUsed = txResult.CumulativeGasUsed @@ -149,27 +165,23 @@ func (e *Executor) executeTx( stateDB *nativeStateDB, gasPool *core.GasPool, block BlockContext, - p parsedTx, + p PreparedTx, txIndex int, txIndexUint uint, baseFee *big.Int, - signer ethtypes.Signer, ) (TxResult, *ethtypes.Receipt, error) { - tx := p.tx + tx := p.Tx if !e.cfg.DisableGasPriceCheck && e.cfg.MinGasPrice != nil { // MinGasPrice is block-validity policy; unlike EVM call failures, it // does not produce a receipt for an otherwise invalid block. if effectiveGasPrice(tx, baseFee).Cmp(e.cfg.MinGasPrice) < 0 { - return TxResult{Hash: tx.Hash(), Sender: p.sender, To: tx.To(), Err: errInsufficientGasPrice}, + return TxResult{Hash: tx.Hash(), Sender: p.Sender, To: tx.To(), Err: errInsufficientGasPrice}, nil, errInsufficientGasPrice } } - msg, err := core.TransactionToMessage(tx, signer, baseFee) - if err != nil { - return TxResult{Hash: tx.Hash(), Sender: p.sender, To: tx.To(), Err: err}, nil, err - } + msg := transactionToPreparedMessage(p, baseFee) msg.SkipNonceChecks = e.cfg.DisableNonceCheck stateDB.setTxContext(tx.Hash(), txIndex, txIndexUint) @@ -177,7 +189,7 @@ func (e *Executor) executeTx( evm.SetTxContext(core.NewEVMTxContext(msg)) execResult, err := core.ApplyMessage(evm, msg, gasPool) if err != nil { - return TxResult{Hash: tx.Hash(), Sender: p.sender, To: tx.To(), Err: err}, nil, err + return TxResult{Hash: tx.Hash(), Sender: p.Sender, To: tx.To(), Err: err}, nil, err } stateDB.clearSnapshots() stateDB.Finalise(true) @@ -206,13 +218,13 @@ func (e *Executor) executeTx( TransactionIndex: txIndexUint, } if tx.To() == nil { - receipt.ContractAddress = crypto.CreateAddress(p.sender, tx.Nonce()) + receipt.ContractAddress = crypto.CreateAddress(p.Sender, tx.Nonce()) } receipt.Bloom = ethtypes.CreateBloom(receipt) txResult := TxResult{ Hash: tx.Hash(), - Sender: p.sender, + Sender: p.Sender, To: tx.To(), ContractAddress: receipt.ContractAddress, Status: status, @@ -224,6 +236,34 @@ func (e *Executor) executeTx( return txResult, receipt, nil } +func transactionToPreparedMessage(p PreparedTx, baseFee *big.Int) *core.Message { + tx := p.Tx + msg := &core.Message{ + From: p.Sender, + Nonce: tx.Nonce(), + GasLimit: tx.Gas(), + GasPrice: new(big.Int).Set(tx.GasPrice()), + GasFeeCap: new(big.Int).Set(tx.GasFeeCap()), + GasTipCap: new(big.Int).Set(tx.GasTipCap()), + To: tx.To(), + Value: tx.Value(), + Data: tx.Data(), + AccessList: tx.AccessList(), + SetCodeAuthorizations: tx.SetCodeAuthorizations(), + SkipNonceChecks: false, + SkipFromEOACheck: false, + BlobHashes: tx.BlobHashes(), + BlobGasFeeCap: tx.BlobGasFeeCap(), + } + if baseFee != nil { + msg.GasPrice = msg.GasPrice.Add(msg.GasTipCap, baseFee) + if msg.GasPrice.Cmp(msg.GasFeeCap) > 0 { + msg.GasPrice = msg.GasFeeCap + } + } + return msg +} + func buildBlockContext(ctx BlockContext) vm.BlockContext { prevRandao := ctx.PrevRandao baseFee := cloneBig(ctx.BaseFee) diff --git a/giga/evmonly/occ.go b/giga/evmonly/occ.go index f37e6b71b7..4578cf6c4f 100644 --- a/giga/evmonly/occ.go +++ b/giga/evmonly/occ.go @@ -30,9 +30,8 @@ type occTxRange struct { end int } -func (e *Executor) executeBlockOCC(ctx context.Context, req BlockRequest) (*BlockResult, error) { +func (e *Executor) executeBlockOCC(ctx context.Context, req PreparedBlock) (*BlockResult, error) { chainConfig := e.chainConfig(req.Context) - signer := ethtypes.MakeSigner(chainConfig, new(big.Int).SetUint64(req.Context.Number), req.Context.Time) blockCtx := buildBlockContext(req.Context) baseFee := cloneBig(req.Context.BaseFee) gasLimit := req.Context.GasLimit @@ -76,7 +75,7 @@ func (e *Executor) executeBlockOCC(ctx context.Context, req BlockRequest) (*Bloc return nil } for idx := txRange.start; idx < txRange.end; idx++ { - result, err := e.executeTxSpeculative(groupCtx, req, idx, signer, chainConfig, blockCtx, baseFee, gasLimit) + result, err := e.executeTxSpeculative(groupCtx, req, idx, chainConfig, blockCtx, baseFee, gasLimit) if err != nil { return err } @@ -112,19 +111,14 @@ func occChunkSize(txCount int, workers int) int { func (e *Executor) executeTxSpeculative( ctx context.Context, - req BlockRequest, + req PreparedBlock, txIndex int, - signer ethtypes.Signer, chainConfig *params.ChainConfig, blockCtx vm.BlockContext, baseFee *big.Int, gasLimit uint64, ) (occTxExecution, error) { - tx, sender, err := parseTx(req.Txs[txIndex], signer) - if err != nil { - return occTxExecution{}, fmt.Errorf("parse tx %d: %w", txIndex, err) - } - p := parsedTx{tx: tx, sender: sender} + p := req.Txs[txIndex] stateDB := newNativeStateDB(e.state) stateDB.enableAccessTracking() evm := vm.NewEVM(blockCtx, stateDB, chainConfig, vm.Config{}, nil) @@ -139,10 +133,9 @@ func (e *Executor) executeTxSpeculative( txIndex, uint(txIndex), baseFee, - signer, ) if err != nil { - return occTxExecution{}, fmt.Errorf("execute tx %d %s: %w", txIndex, p.tx.Hash(), err) + return occTxExecution{}, fmt.Errorf("execute tx %d %s: %w", txIndex, p.Tx.Hash(), err) } readSet, writeSet := stateDB.accessSets() return occTxExecution{ diff --git a/giga/evmonly/parser.go b/giga/evmonly/parser.go index d5a47fffb6..99b366fc61 100644 --- a/giga/evmonly/parser.go +++ b/giga/evmonly/parser.go @@ -8,13 +8,8 @@ import ( ethtypes "github.com/ethereum/go-ethereum/core/types" ) -type parsedTx struct { - tx *ethtypes.Transaction - sender common.Address -} - -func parseBlockTxs(ctx context.Context, txs [][]byte, signer ethtypes.Signer) ([]parsedTx, error) { - parsed := make([]parsedTx, len(txs)) +func parseBlockTxs(ctx context.Context, txs [][]byte, signer ethtypes.Signer) ([]PreparedTx, error) { + parsed := make([]PreparedTx, len(txs)) for i, raw := range txs { select { case <-ctx.Done(): @@ -25,7 +20,7 @@ func parseBlockTxs(ctx context.Context, txs [][]byte, signer ethtypes.Signer) ([ if err != nil { return nil, fmt.Errorf("parse tx %d: %w", i, err) } - parsed[i] = parsedTx{tx: tx, sender: sender} + parsed[i] = PreparedTx{Tx: tx, Sender: sender} } return parsed, nil } diff --git a/giga/evmonly/types.go b/giga/evmonly/types.go index f081c71a44..639d3f1694 100644 --- a/giga/evmonly/types.go +++ b/giga/evmonly/types.go @@ -13,6 +13,15 @@ type BlockExecutor interface { ExecuteBlock(context.Context, BlockRequest) (*BlockResult, error) } +// PreparedBlockExecutor exposes the split transaction preparation and block +// execution phases. Preparation is stateless, so callers may pipeline it ahead +// of ordered block execution. +type PreparedBlockExecutor interface { + BlockExecutor + PrepareBlock(context.Context, BlockRequest) (PreparedBlock, error) + ExecutePreparedBlock(context.Context, PreparedBlock) (*BlockResult, error) +} + // ResultSink persists executor-produced block outputs. type ResultSink interface { StoreChangeSet(ctx context.Context, height uint64, changeSet StateChangeSet) error @@ -26,6 +35,19 @@ type BlockRequest struct { Txs [][]byte } +// PreparedBlock contains decoded transactions with recovered senders. The +// executor treats prepared transactions as immutable. +type PreparedBlock struct { + Context BlockContext + Txs []PreparedTx +} + +// PreparedTx is the stateless per-transaction work needed before EVM execution. +type PreparedTx struct { + Tx *ethtypes.Transaction + Sender common.Address +} + // BlockContext contains block-constant EVM execution data. type BlockContext struct { Number uint64 From 06e18a932c3871f12827d173f465df56567160f8 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Thu, 2 Jul 2026 17:50:56 +0800 Subject: [PATCH 16/29] reuse evmonly OCC workers --- giga/evmonly/affinity_linux.go | 32 +++++++ giga/evmonly/affinity_other.go | 9 ++ giga/evmonly/config.go | 2 + giga/evmonly/executor.go | 15 ++++ giga/evmonly/occ.go | 65 ++++++--------- giga/evmonly/occ_pool.go | 147 +++++++++++++++++++++++++++++++++ 6 files changed, 232 insertions(+), 38 deletions(-) create mode 100644 giga/evmonly/affinity_linux.go create mode 100644 giga/evmonly/affinity_other.go create mode 100644 giga/evmonly/occ_pool.go diff --git a/giga/evmonly/affinity_linux.go b/giga/evmonly/affinity_linux.go new file mode 100644 index 0000000000..d0753dfa3b --- /dev/null +++ b/giga/evmonly/affinity_linux.go @@ -0,0 +1,32 @@ +//go:build linux + +package evmonly + +import ( + "fmt" + "runtime" + + "golang.org/x/sys/unix" +) + +func pinCurrentWorkerThread(cpu int) (func(), error) { + runtime.LockOSThread() + unlock := runtime.UnlockOSThread + numCPU := runtime.NumCPU() + if numCPU <= 0 { + unlock() + return func() {}, fmt.Errorf("runtime reported no CPUs") + } + if cpu < 0 { + unlock() + return func() {}, fmt.Errorf("CPU index must be non-negative") + } + cpu %= numCPU + var set unix.CPUSet + set.Set(cpu) + if err := unix.SchedSetaffinity(0, &set); err != nil { + unlock() + return func() {}, err + } + return unlock, nil +} diff --git a/giga/evmonly/affinity_other.go b/giga/evmonly/affinity_other.go new file mode 100644 index 0000000000..88c140fe26 --- /dev/null +++ b/giga/evmonly/affinity_other.go @@ -0,0 +1,9 @@ +//go:build !linux + +package evmonly + +import "fmt" + +func pinCurrentWorkerThread(int) (func(), error) { + return func() {}, fmt.Errorf("worker CPU pinning is only supported on linux") +} diff --git a/giga/evmonly/config.go b/giga/evmonly/config.go index e376dfa2d3..0f5ebc093e 100644 --- a/giga/evmonly/config.go +++ b/giga/evmonly/config.go @@ -16,6 +16,8 @@ type Config struct { ChainConfig *params.ChainConfig CustomPrecompiles precompiles.Registry OCCWorkers int + PinOCCWorkers bool + OCCWorkerCPUOffset int } func DefaultConfig() Config { diff --git a/giga/evmonly/executor.go b/giga/evmonly/executor.go index 13d0204960..15367ea63e 100644 --- a/giga/evmonly/executor.go +++ b/giga/evmonly/executor.go @@ -5,6 +5,7 @@ import ( "fmt" "math" "math/big" + "runtime" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" @@ -21,6 +22,7 @@ type Executor struct { cfg Config state StateReader resultSink ResultSink + occPool *occWorkerPool } type Option func(*Executor) @@ -44,12 +46,25 @@ func NewExecutor(cfg Config, opts ...Option) *Executor { cfg: cfg.WithDefaults(), state: NewMemoryState(), } + if e.cfg.OCCWorkers > 1 { + e.occPool = newOCCWorkerPool(e.cfg.OCCWorkers, e.cfg.PinOCCWorkers, e.cfg.OCCWorkerCPUOffset) + runtime.SetFinalizer(e, (*Executor).Close) + } for _, opt := range opts { opt(e) } return e } +func (e *Executor) Close() { + if e == nil || e.occPool == nil { + return + } + runtime.SetFinalizer(e, nil) + e.occPool.Close() + e.occPool = nil +} + func (e *Executor) Config() Config { return e.cfg } diff --git a/giga/evmonly/occ.go b/giga/evmonly/occ.go index 4578cf6c4f..d212fe0f55 100644 --- a/giga/evmonly/occ.go +++ b/giga/evmonly/occ.go @@ -13,7 +13,6 @@ import ( ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/params" - "golang.org/x/sync/errgroup" ) type occTxExecution struct { @@ -46,46 +45,21 @@ func (e *Executor) executeBlockOCC(ctx context.Context, req PreparedBlock) (*Blo } results := make([]occTxExecution, txCount) chunkSize := occChunkSize(txCount, workers) - jobs := make(chan occTxRange) - group, groupCtx := errgroup.WithContext(ctx) - - group.Go(func() error { - defer close(jobs) - for start := 0; start < txCount; start += chunkSize { - end := start + chunkSize - if end > txCount { - end = txCount - } - select { - case jobs <- occTxRange{start: start, end: end}: - case <-groupCtx.Done(): - return groupCtx.Err() + pool := e.occPool + if pool == nil { + pool = newOCCWorkerPool(workers, e.cfg.PinOCCWorkers, e.cfg.OCCWorkerCPUOffset) + defer pool.Close() + } + if err := pool.Run(ctx, occRanges(txCount, chunkSize), func(workerCtx context.Context, txRange occTxRange) error { + for idx := txRange.start; idx < txRange.end; idx++ { + result, err := e.executeTxSpeculative(workerCtx, req, idx, chainConfig, blockCtx, baseFee, gasLimit) + if err != nil { + return err } + results[idx] = result } return nil - }) - for range workers { - group.Go(func() error { - for { - select { - case <-groupCtx.Done(): - return groupCtx.Err() - case txRange, ok := <-jobs: - if !ok { - return nil - } - for idx := txRange.start; idx < txRange.end; idx++ { - result, err := e.executeTxSpeculative(groupCtx, req, idx, chainConfig, blockCtx, baseFee, gasLimit) - if err != nil { - return err - } - results[idx] = result - } - } - } - }) - } - if err := group.Wait(); err != nil { + }); err != nil { return nil, err } if !validateOCCResults(results, gasLimit) { @@ -94,6 +68,21 @@ func (e *Executor) executeBlockOCC(ctx context.Context, req PreparedBlock) (*Blo return mergeOCCResults(results), nil } +func occRanges(txCount int, chunkSize int) []occTxRange { + if chunkSize <= 0 { + chunkSize = 1 + } + ranges := make([]occTxRange, 0, (txCount+chunkSize-1)/chunkSize) + for start := 0; start < txCount; start += chunkSize { + end := start + chunkSize + if end > txCount { + end = txCount + } + ranges = append(ranges, occTxRange{start: start, end: end}) + } + return ranges +} + func occChunkSize(txCount int, workers int) int { if txCount <= 0 || workers <= 0 { return 1 diff --git a/giga/evmonly/occ_pool.go b/giga/evmonly/occ_pool.go new file mode 100644 index 0000000000..54ed448c1f --- /dev/null +++ b/giga/evmonly/occ_pool.go @@ -0,0 +1,147 @@ +package evmonly + +import ( + "context" + "fmt" + "sync" +) + +type occWorkerPool struct { + jobs chan occPoolJob + stop chan struct{} + closed chan struct{} + once sync.Once + + pinErrMu sync.Mutex + pinErr error +} + +type occPoolJob struct { + ctx context.Context + txRange occTxRange + run func(context.Context, occTxRange) error + + done *sync.WaitGroup + cancel context.CancelFunc + errOnce *sync.Once + err *error +} + +func newOCCWorkerPool(workers int, pinWorkers bool, cpuOffset int) *occWorkerPool { + p := &occWorkerPool{ + jobs: make(chan occPoolJob, workers*2), + stop: make(chan struct{}), + closed: make(chan struct{}), + } + var workerWG sync.WaitGroup + workerWG.Add(workers) + for workerID := 0; workerID < workers; workerID++ { + workerID := workerID + go func() { + defer workerWG.Done() + p.runWorker(workerID, pinWorkers, cpuOffset) + }() + } + go func() { + workerWG.Wait() + close(p.closed) + }() + return p +} + +func (p *occWorkerPool) runWorker(workerID int, pinWorkers bool, cpuOffset int) { + if pinWorkers { + unlock, err := pinCurrentWorkerThread(cpuOffset + workerID) + if err != nil { + p.setPinErr(fmt.Errorf("pin OCC worker %d: %w", workerID, err)) + } + defer unlock() + } + for { + select { + case <-p.stop: + return + case job := <-p.jobs: + p.runJob(job) + } + } +} + +func (p *occWorkerPool) runJob(job occPoolJob) { + defer job.done.Done() + if err := job.ctx.Err(); err != nil { + return + } + if err := job.run(job.ctx, job.txRange); err != nil { + job.errOnce.Do(func() { + *job.err = err + job.cancel() + }) + } +} + +func (p *occWorkerPool) Run(ctx context.Context, ranges []occTxRange, run func(context.Context, occTxRange) error) error { + if err := p.getPinErr(); err != nil { + return err + } + jobCtx, cancel := context.WithCancel(ctx) + defer cancel() + + var done sync.WaitGroup + var err error + var errOnce sync.Once +dispatch: + for _, txRange := range ranges { + done.Add(1) + job := occPoolJob{ + ctx: jobCtx, + txRange: txRange, + run: run, + done: &done, + cancel: cancel, + errOnce: &errOnce, + err: &err, + } + select { + case p.jobs <- job: + case <-jobCtx.Done(): + done.Done() + break dispatch + case <-p.stop: + done.Done() + return fmt.Errorf("OCC worker pool is closed") + } + } + done.Wait() + if err != nil { + return err + } + if ctxErr := ctx.Err(); ctxErr != nil { + return ctxErr + } + return nil +} + +func (p *occWorkerPool) Close() { + if p == nil { + return + } + p.once.Do(func() { + close(p.stop) + <-p.closed + }) +} + +func (p *occWorkerPool) setPinErr(err error) { + p.pinErrMu.Lock() + defer p.pinErrMu.Unlock() + if p.pinErr == nil { + p.pinErr = err + } +} + +func (p *occWorkerPool) getPinErr() error { + p.pinErrMu.Lock() + defer p.pinErrMu.Unlock() + return p.pinErr +} From 12e1d83b829be54f35eecb78dddb523fec3580b7 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Thu, 2 Jul 2026 20:35:07 +0800 Subject: [PATCH 17/29] remove evmonly worker pinning --- giga/evmonly/affinity_linux.go | 32 ------------------------------ giga/evmonly/affinity_other.go | 9 --------- giga/evmonly/config.go | 2 -- giga/evmonly/executor.go | 2 +- giga/evmonly/occ.go | 2 +- giga/evmonly/occ_pool.go | 36 ++++------------------------------ 6 files changed, 6 insertions(+), 77 deletions(-) delete mode 100644 giga/evmonly/affinity_linux.go delete mode 100644 giga/evmonly/affinity_other.go diff --git a/giga/evmonly/affinity_linux.go b/giga/evmonly/affinity_linux.go deleted file mode 100644 index d0753dfa3b..0000000000 --- a/giga/evmonly/affinity_linux.go +++ /dev/null @@ -1,32 +0,0 @@ -//go:build linux - -package evmonly - -import ( - "fmt" - "runtime" - - "golang.org/x/sys/unix" -) - -func pinCurrentWorkerThread(cpu int) (func(), error) { - runtime.LockOSThread() - unlock := runtime.UnlockOSThread - numCPU := runtime.NumCPU() - if numCPU <= 0 { - unlock() - return func() {}, fmt.Errorf("runtime reported no CPUs") - } - if cpu < 0 { - unlock() - return func() {}, fmt.Errorf("CPU index must be non-negative") - } - cpu %= numCPU - var set unix.CPUSet - set.Set(cpu) - if err := unix.SchedSetaffinity(0, &set); err != nil { - unlock() - return func() {}, err - } - return unlock, nil -} diff --git a/giga/evmonly/affinity_other.go b/giga/evmonly/affinity_other.go deleted file mode 100644 index 88c140fe26..0000000000 --- a/giga/evmonly/affinity_other.go +++ /dev/null @@ -1,9 +0,0 @@ -//go:build !linux - -package evmonly - -import "fmt" - -func pinCurrentWorkerThread(int) (func(), error) { - return func() {}, fmt.Errorf("worker CPU pinning is only supported on linux") -} diff --git a/giga/evmonly/config.go b/giga/evmonly/config.go index 0f5ebc093e..e376dfa2d3 100644 --- a/giga/evmonly/config.go +++ b/giga/evmonly/config.go @@ -16,8 +16,6 @@ type Config struct { ChainConfig *params.ChainConfig CustomPrecompiles precompiles.Registry OCCWorkers int - PinOCCWorkers bool - OCCWorkerCPUOffset int } func DefaultConfig() Config { diff --git a/giga/evmonly/executor.go b/giga/evmonly/executor.go index 15367ea63e..3455c8eac6 100644 --- a/giga/evmonly/executor.go +++ b/giga/evmonly/executor.go @@ -47,7 +47,7 @@ func NewExecutor(cfg Config, opts ...Option) *Executor { state: NewMemoryState(), } if e.cfg.OCCWorkers > 1 { - e.occPool = newOCCWorkerPool(e.cfg.OCCWorkers, e.cfg.PinOCCWorkers, e.cfg.OCCWorkerCPUOffset) + e.occPool = newOCCWorkerPool(e.cfg.OCCWorkers) runtime.SetFinalizer(e, (*Executor).Close) } for _, opt := range opts { diff --git a/giga/evmonly/occ.go b/giga/evmonly/occ.go index d212fe0f55..533adfc494 100644 --- a/giga/evmonly/occ.go +++ b/giga/evmonly/occ.go @@ -47,7 +47,7 @@ func (e *Executor) executeBlockOCC(ctx context.Context, req PreparedBlock) (*Blo chunkSize := occChunkSize(txCount, workers) pool := e.occPool if pool == nil { - pool = newOCCWorkerPool(workers, e.cfg.PinOCCWorkers, e.cfg.OCCWorkerCPUOffset) + pool = newOCCWorkerPool(workers) defer pool.Close() } if err := pool.Run(ctx, occRanges(txCount, chunkSize), func(workerCtx context.Context, txRange occTxRange) error { diff --git a/giga/evmonly/occ_pool.go b/giga/evmonly/occ_pool.go index 54ed448c1f..0678968a96 100644 --- a/giga/evmonly/occ_pool.go +++ b/giga/evmonly/occ_pool.go @@ -11,9 +11,6 @@ type occWorkerPool struct { stop chan struct{} closed chan struct{} once sync.Once - - pinErrMu sync.Mutex - pinErr error } type occPoolJob struct { @@ -27,7 +24,7 @@ type occPoolJob struct { err *error } -func newOCCWorkerPool(workers int, pinWorkers bool, cpuOffset int) *occWorkerPool { +func newOCCWorkerPool(workers int) *occWorkerPool { p := &occWorkerPool{ jobs: make(chan occPoolJob, workers*2), stop: make(chan struct{}), @@ -35,11 +32,10 @@ func newOCCWorkerPool(workers int, pinWorkers bool, cpuOffset int) *occWorkerPoo } var workerWG sync.WaitGroup workerWG.Add(workers) - for workerID := 0; workerID < workers; workerID++ { - workerID := workerID + for range workers { go func() { defer workerWG.Done() - p.runWorker(workerID, pinWorkers, cpuOffset) + p.runWorker() }() } go func() { @@ -49,14 +45,7 @@ func newOCCWorkerPool(workers int, pinWorkers bool, cpuOffset int) *occWorkerPoo return p } -func (p *occWorkerPool) runWorker(workerID int, pinWorkers bool, cpuOffset int) { - if pinWorkers { - unlock, err := pinCurrentWorkerThread(cpuOffset + workerID) - if err != nil { - p.setPinErr(fmt.Errorf("pin OCC worker %d: %w", workerID, err)) - } - defer unlock() - } +func (p *occWorkerPool) runWorker() { for { select { case <-p.stop: @@ -81,9 +70,6 @@ func (p *occWorkerPool) runJob(job occPoolJob) { } func (p *occWorkerPool) Run(ctx context.Context, ranges []occTxRange, run func(context.Context, occTxRange) error) error { - if err := p.getPinErr(); err != nil { - return err - } jobCtx, cancel := context.WithCancel(ctx) defer cancel() @@ -131,17 +117,3 @@ func (p *occWorkerPool) Close() { <-p.closed }) } - -func (p *occWorkerPool) setPinErr(err error) { - p.pinErrMu.Lock() - defer p.pinErrMu.Unlock() - if p.pinErr == nil { - p.pinErr = err - } -} - -func (p *occWorkerPool) getPinErr() error { - p.pinErrMu.Lock() - defer p.pinErrMu.Unlock() - return p.pinErr -} From 1bccbe46ca2aa69951883b5c62c9f3789841cec6 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Thu, 2 Jul 2026 20:55:29 +0800 Subject: [PATCH 18/29] pool evmonly execution outputs --- giga/evmonly/config.go | 5 ++ giga/evmonly/executor.go | 49 ++++++++++++--- giga/evmonly/executor_test.go | 47 ++++++++++++++ giga/evmonly/occ.go | 51 +++++++++++---- giga/evmonly/occ_pool.go | 11 ++-- giga/evmonly/result_pool.go | 115 ++++++++++++++++++++++++++++++++++ giga/evmonly/state_db.go | 87 +++++++++++++++++++++++-- giga/evmonly/types.go | 22 +++++++ 8 files changed, 356 insertions(+), 31 deletions(-) create mode 100644 giga/evmonly/result_pool.go diff --git a/giga/evmonly/config.go b/giga/evmonly/config.go index e376dfa2d3..c79dbef842 100644 --- a/giga/evmonly/config.go +++ b/giga/evmonly/config.go @@ -16,6 +16,11 @@ type Config struct { ChainConfig *params.ChainConfig CustomPrecompiles precompiles.Registry OCCWorkers int + // BlockResultPoolSize enables a bounded reusable output pool. Callers that + // enable it must call BlockResult.Release when they are done with returned + // results. Async sinks should implement BlockResultSink so the executor can + // retain results until the sink releases them. + BlockResultPoolSize int } func DefaultConfig() Config { diff --git a/giga/evmonly/executor.go b/giga/evmonly/executor.go index 3455c8eac6..92bfd05561 100644 --- a/giga/evmonly/executor.go +++ b/giga/evmonly/executor.go @@ -23,6 +23,7 @@ type Executor struct { state StateReader resultSink ResultSink occPool *occWorkerPool + resultPool *blockResultPool } type Option func(*Executor) @@ -43,8 +44,9 @@ func WithResultSink(sink ResultSink) Option { func NewExecutor(cfg Config, opts ...Option) *Executor { e := &Executor{ - cfg: cfg.WithDefaults(), - state: NewMemoryState(), + cfg: cfg.WithDefaults(), + state: NewMemoryState(), + resultPool: newBlockResultPool(cfg.BlockResultPoolSize), } if e.cfg.OCCWorkers > 1 { e.occPool = newOCCWorkerPool(e.cfg.OCCWorkers) @@ -94,7 +96,7 @@ func (e *Executor) ExecutePreparedBlock(ctx context.Context, req PreparedBlock) var result *BlockResult var err error if len(req.Txs) == 0 { - result = &BlockResult{} + result, err = e.acquireBlockResult(ctx, 0) } else if e.useOCC(len(req.Txs)) { result, err = e.executeBlockOCC(ctx, req) } else { @@ -104,15 +106,41 @@ func (e *Executor) ExecutePreparedBlock(ctx context.Context, req PreparedBlock) return nil, err } if err := e.sinkBlockResult(ctx, req.Context.Number, result); err != nil { + result.Release() return nil, err } return result, nil } +func (e *Executor) acquireBlockResult(ctx context.Context, txCapacity int) (*BlockResult, error) { + if e.resultPool == nil || !e.canPoolBlockResults() { + result := &BlockResult{} + result.prepareForBlock(txCapacity) + return result, nil + } + return e.resultPool.acquire(ctx, txCapacity) +} + +func (e *Executor) canPoolBlockResults() bool { + if e.resultSink == nil { + return true + } + _, ok := e.resultSink.(BlockResultSink) + return ok +} + func (e *Executor) sinkBlockResult(ctx context.Context, height uint64, result *BlockResult) error { if e.resultSink == nil || result == nil { return nil } + if sink, ok := e.resultSink.(BlockResultSink); ok { + release := result.retain() + if err := sink.StoreBlockResult(ctx, height, result, release); err != nil { + release() + return fmt.Errorf("store block result for block %d: %w", height, err) + } + return nil + } if err := e.resultSink.StoreChangeSet(ctx, height, result.ChangeSet); err != nil { return fmt.Errorf("store changeset for block %d: %w", height, err) } @@ -147,10 +175,16 @@ func (e *Executor) executeBlockSequential(ctx context.Context, req PreparedBlock gasPool := new(core.GasPool).AddGas(gasLimit) baseFee := cloneBig(req.Context.BaseFee) - result := &BlockResult{ - Txs: make([]TxResult, 0, len(req.Txs)), - Receipts: make(ethtypes.Receipts, 0, len(req.Txs)), + result, err := e.acquireBlockResult(ctx, len(req.Txs)) + if err != nil { + return nil, err } + ok := false + defer func() { + if !ok { + result.Release() + } + }() var txIndexUint uint for txIndex, p := range req.Txs { select { @@ -171,7 +205,8 @@ func (e *Executor) executeBlockSequential(ctx context.Context, req PreparedBlock } stateDB.clearSnapshots() stateDB.Finalise(true) - result.ChangeSet = stateDB.ChangeSet() + stateDB.ChangeSetInto(&result.ChangeSet) + ok = true return result, nil } diff --git a/giga/evmonly/executor_test.go b/giga/evmonly/executor_test.go index 998a4783f0..911d184b41 100644 --- a/giga/evmonly/executor_test.go +++ b/giga/evmonly/executor_test.go @@ -39,6 +39,25 @@ func (s *recordingResultSink) StoreReceipts(_ context.Context, height uint64, re return nil } +type recordingBlockResultSink struct { + result *BlockResult + release func() +} + +func (s *recordingBlockResultSink) StoreChangeSet(context.Context, uint64, StateChangeSet) error { + return nil +} + +func (s *recordingBlockResultSink) StoreReceipts(context.Context, uint64, ethtypes.Receipts) error { + return nil +} + +func (s *recordingBlockResultSink) StoreBlockResult(_ context.Context, _ uint64, result *BlockResult, release func()) error { + s.result = result + s.release = release + return nil +} + func TestExecutorEmptyBlock(t *testing.T) { executor := NewExecutor(Config{}) @@ -108,6 +127,34 @@ func TestExecutorInvokesResultSink(t *testing.T) { require.Equal(t, result.Receipts, sink.receipts[0]) } +func TestExecutorPooledResultRelease(t *testing.T) { + chainID := big.NewInt(713715) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + recipient := common.HexToAddress("0x00000000000000000000000000000000000000a8") + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(200_000_000_000_000)) + sink := &recordingBlockResultSink{} + executor := NewExecutor(Config{BlockResultPoolSize: 1}, WithState(state), WithResultSink(sink)) + rawTx := signLegacyTx(t, key, chainID, 0, &recipient, big.NewInt(7), nil) + req := BlockRequest{Context: blockContext(chainID), Txs: [][]byte{rawTx}} + + first, err := executor.ExecuteBlock(context.Background(), req) + require.NoError(t, err) + require.Same(t, first, sink.result) + require.NotNil(t, sink.release) + sink.release() + first.Release() + + second, err := executor.ExecuteBlock(context.Background(), req) + require.NoError(t, err) + require.Same(t, first, second) + sink.release() + second.Release() +} + func TestExecutorDynamicFeeTx(t *testing.T) { chainID := big.NewInt(713715) key, err := crypto.GenerateKey() diff --git a/giga/evmonly/occ.go b/giga/evmonly/occ.go index 533adfc494..c9977a0628 100644 --- a/giga/evmonly/occ.go +++ b/giga/evmonly/occ.go @@ -29,6 +29,19 @@ type occTxRange struct { end int } +type occWorkerScratch struct { + stateDB *nativeStateDB +} + +func (s *occWorkerScratch) resetStateDB(source StateReader) *nativeStateDB { + if s.stateDB == nil { + s.stateDB = newNativeStateDB(source) + } else { + s.stateDB.reset(source) + } + return s.stateDB +} + func (e *Executor) executeBlockOCC(ctx context.Context, req PreparedBlock) (*BlockResult, error) { chainConfig := e.chainConfig(req.Context) blockCtx := buildBlockContext(req.Context) @@ -50,9 +63,9 @@ func (e *Executor) executeBlockOCC(ctx context.Context, req PreparedBlock) (*Blo pool = newOCCWorkerPool(workers) defer pool.Close() } - if err := pool.Run(ctx, occRanges(txCount, chunkSize), func(workerCtx context.Context, txRange occTxRange) error { + if err := pool.Run(ctx, occRanges(txCount, chunkSize), func(workerCtx context.Context, txRange occTxRange, scratch *occWorkerScratch) error { for idx := txRange.start; idx < txRange.end; idx++ { - result, err := e.executeTxSpeculative(workerCtx, req, idx, chainConfig, blockCtx, baseFee, gasLimit) + result, err := e.executeTxSpeculative(workerCtx, scratch, req, idx, chainConfig, blockCtx, baseFee, gasLimit) if err != nil { return err } @@ -65,7 +78,7 @@ func (e *Executor) executeBlockOCC(ctx context.Context, req PreparedBlock) (*Blo if !validateOCCResults(results, gasLimit) { return e.executeBlockSequential(ctx, req) } - return mergeOCCResults(results), nil + return e.mergeOCCResults(ctx, results) } func occRanges(txCount int, chunkSize int) []occTxRange { @@ -100,6 +113,7 @@ func occChunkSize(txCount int, workers int) int { func (e *Executor) executeTxSpeculative( ctx context.Context, + scratch *occWorkerScratch, req PreparedBlock, txIndex int, chainConfig *params.ChainConfig, @@ -107,8 +121,11 @@ func (e *Executor) executeTxSpeculative( baseFee *big.Int, gasLimit uint64, ) (occTxExecution, error) { + if scratch == nil { + scratch = &occWorkerScratch{} + } p := req.Txs[txIndex] - stateDB := newNativeStateDB(e.state) + stateDB := scratch.resetStateDB(e.state) stateDB.enableAccessTracking() evm := vm.NewEVM(blockCtx, stateDB, chainConfig, vm.Config{}, nil) stateDB.SetEVM(evm) @@ -127,10 +144,12 @@ func (e *Executor) executeTxSpeculative( return occTxExecution{}, fmt.Errorf("execute tx %d %s: %w", txIndex, p.Tx.Hash(), err) } readSet, writeSet := stateDB.accessSets() + var changeSet StateChangeSet + stateDB.ChangeSetInto(&changeSet) return occTxExecution{ txResult: txResult, receipt: receipt, - changeSet: stateDB.ChangeSet(), + changeSet: changeSet, readSet: readSet, writeSet: writeSet, gasUsed: txResult.GasUsed, @@ -156,11 +175,12 @@ func validateOCCResults(results []occTxExecution, gasLimit uint64) bool { return true } -func mergeOCCResults(results []occTxExecution) *BlockResult { - blockResult := &BlockResult{ - Txs: make([]TxResult, len(results)), - Receipts: make(ethtypes.Receipts, len(results)), +func (e *Executor) mergeOCCResults(ctx context.Context, results []occTxExecution) (*BlockResult, error) { + blockResult, err := e.acquireBlockResult(ctx, len(results)) + if err != nil { + return nil, err } + blockResult.prepareIndexedResults(len(results)) var logIndex uint for i, result := range results { blockResult.GasUsed += result.gasUsed @@ -173,8 +193,8 @@ func mergeOCCResults(results []occTxExecution) *BlockResult { blockResult.Txs[i] = result.txResult blockResult.Receipts[i] = result.receipt } - blockResult.ChangeSet = mergeChangeSets(results) - return blockResult + mergeChangeSetsInto(results, &blockResult.ChangeSet) + return blockResult, nil } type stateAccessIndex struct { @@ -230,6 +250,13 @@ type storageChangeKey struct { } func mergeChangeSets(results []occTxExecution) StateChangeSet { + var merged StateChangeSet + mergeChangeSetsInto(results, &merged) + return merged +} + +func mergeChangeSetsInto(results []occTxExecution, merged *StateChangeSet) { + merged.resetForReuse() balances := map[common.Address]*big.Int{} nonces := map[common.Address]uint64{} code := map[common.Address]CodeChange{} @@ -254,7 +281,6 @@ func mergeChangeSets(results []occTxExecution) StateChangeSet { } } - var merged StateChangeSet balanceAddrs := sortedAddressesFromBigMap(balances) for _, addr := range balanceAddrs { merged.Balances = append(merged.Balances, BalanceChange{Address: addr, Balance: cloneBig(balances[addr])}) @@ -282,7 +308,6 @@ func mergeChangeSets(results []occTxExecution) StateChangeSet { for _, key := range storageKeys { merged.Storage = append(merged.Storage, storage[key]) } - return merged } func sortedAddressesFromBigMap(values map[common.Address]*big.Int) []common.Address { diff --git a/giga/evmonly/occ_pool.go b/giga/evmonly/occ_pool.go index 0678968a96..900126bad1 100644 --- a/giga/evmonly/occ_pool.go +++ b/giga/evmonly/occ_pool.go @@ -16,7 +16,7 @@ type occWorkerPool struct { type occPoolJob struct { ctx context.Context txRange occTxRange - run func(context.Context, occTxRange) error + run func(context.Context, occTxRange, *occWorkerScratch) error done *sync.WaitGroup cancel context.CancelFunc @@ -46,22 +46,23 @@ func newOCCWorkerPool(workers int) *occWorkerPool { } func (p *occWorkerPool) runWorker() { + scratch := &occWorkerScratch{} for { select { case <-p.stop: return case job := <-p.jobs: - p.runJob(job) + p.runJob(job, scratch) } } } -func (p *occWorkerPool) runJob(job occPoolJob) { +func (p *occWorkerPool) runJob(job occPoolJob, scratch *occWorkerScratch) { defer job.done.Done() if err := job.ctx.Err(); err != nil { return } - if err := job.run(job.ctx, job.txRange); err != nil { + if err := job.run(job.ctx, job.txRange, scratch); err != nil { job.errOnce.Do(func() { *job.err = err job.cancel() @@ -69,7 +70,7 @@ func (p *occWorkerPool) runJob(job occPoolJob) { } } -func (p *occWorkerPool) Run(ctx context.Context, ranges []occTxRange, run func(context.Context, occTxRange) error) error { +func (p *occWorkerPool) Run(ctx context.Context, ranges []occTxRange, run func(context.Context, occTxRange, *occWorkerScratch) error) error { jobCtx, cancel := context.WithCancel(ctx) defer cancel() diff --git a/giga/evmonly/result_pool.go b/giga/evmonly/result_pool.go new file mode 100644 index 0000000000..31d2be5f83 --- /dev/null +++ b/giga/evmonly/result_pool.go @@ -0,0 +1,115 @@ +package evmonly + +import ( + "context" + "sync" + "sync/atomic" + + ethtypes "github.com/ethereum/go-ethereum/core/types" +) + +type blockResultPool struct { + free chan *BlockResult +} + +type blockResultLease struct { + pool *blockResultPool + result *BlockResult + refs atomic.Int32 +} + +func newBlockResultPool(size int) *blockResultPool { + if size <= 0 { + return nil + } + p := &blockResultPool{free: make(chan *BlockResult, size)} + for range size { + p.free <- &BlockResult{} + } + return p +} + +func (p *blockResultPool) acquire(ctx context.Context, txCapacity int) (*BlockResult, error) { + if p == nil { + result := &BlockResult{} + result.prepareForBlock(txCapacity) + return result, nil + } + select { + case result := <-p.free: + result.prepareForBlock(txCapacity) + lease := &blockResultLease{pool: p, result: result} + lease.refs.Store(1) + result.lease = lease + return result, nil + case <-ctx.Done(): + return nil, ctx.Err() + } +} + +func (l *blockResultLease) retain() func() { + if l == nil { + return func() {} + } + l.refs.Add(1) + var once sync.Once + return func() { + once.Do(l.release) + } +} + +func (l *blockResultLease) release() { + if l == nil { + return + } + if l.refs.Add(-1) != 0 { + return + } + result := l.result + result.resetForPool() + l.pool.free <- result +} + +func (r *BlockResult) retain() func() { + if r == nil || r.lease == nil { + return func() {} + } + return r.lease.retain() +} + +func (r *BlockResult) prepareForBlock(txCapacity int) { + r.resetForPool() + if cap(r.Txs) < txCapacity { + r.Txs = make([]TxResult, 0, txCapacity) + } + if cap(r.Receipts) < txCapacity { + r.Receipts = make(ethtypes.Receipts, 0, txCapacity) + } +} + +func (r *BlockResult) prepareIndexedResults(txCount int) { + r.prepareForBlock(txCount) + r.Txs = r.Txs[:txCount] + r.Receipts = r.Receipts[:txCount] +} + +func (r *BlockResult) resetForPool() { + r.ChangeSet.resetForReuse() + clear(r.Txs) + r.Txs = r.Txs[:0] + clear(r.Receipts) + r.Receipts = r.Receipts[:0] + r.GasUsed = 0 + r.lease = nil +} + +func (cs *StateChangeSet) resetForReuse() { + clear(cs.Balances) + cs.Balances = cs.Balances[:0] + clear(cs.Nonces) + cs.Nonces = cs.Nonces[:0] + clear(cs.Code) + cs.Code = cs.Code[:0] + clear(cs.Storage) + cs.Storage = cs.Storage[:0] +} diff --git a/giga/evmonly/state_db.go b/giga/evmonly/state_db.go index f3a929042a..e91fcd6ed1 100644 --- a/giga/evmonly/state_db.go +++ b/giga/evmonly/state_db.go @@ -114,6 +114,13 @@ func newNativeStateDB(source StateReader) *nativeStateDB { } func (s *nativeStateDB) ChangeSet() StateChangeSet { + var changes StateChangeSet + s.ChangeSetInto(&changes) + return changes +} + +func (s *nativeStateDB) ChangeSetInto(changes *StateChangeSet) { + changes.resetForReuse() addresses := make([]common.Address, 0, len(s.accounts)) for addr := range s.accounts { addresses = append(addresses, addr) @@ -122,7 +129,6 @@ func (s *nativeStateDB) ChangeSet() StateChangeSet { return bytes.Compare(addresses[i][:], addresses[j][:]) < 0 }) - var changes StateChangeSet for _, addr := range addresses { acct := s.accounts[addr] base := s.baseAccount(addr) @@ -161,7 +167,6 @@ func (s *nativeStateDB) ChangeSet() StateChangeSet { }) } } - return changes } func (s *nativeStateDB) CreateAccount(addr common.Address) { @@ -413,8 +418,8 @@ func (s *nativeStateDB) AddSlotToAccessList(addr common.Address, slot common.Has } func (s *nativeStateDB) Prepare(_ params.Rules, sender, coinbase common.Address, dest *common.Address, precompiles []common.Address, txAccesses ethtypes.AccessList) { - s.accessList = newAccessList() - s.transientStates = map[common.Address]map[common.Hash]common.Hash{} + s.accessList.reset() + clearNestedHashMaps(s.transientStates) s.AddAddressToAccessList(sender) s.AddAddressToAccessList(coinbase) if dest != nil { @@ -576,8 +581,16 @@ func (s *nativeStateDB) SetEVM(evm *vm.EVM) { } func (s *nativeStateDB) enableAccessTracking() { - s.readSet = map[stateAccessKey]struct{}{} - s.writeSet = map[stateAccessKey]struct{}{} + if s.readSet == nil { + s.readSet = map[stateAccessKey]struct{}{} + } else { + clear(s.readSet) + } + if s.writeSet == nil { + s.writeSet = map[stateAccessKey]struct{}{} + } else { + clear(s.writeSet) + } } func (s *nativeStateDB) accessSets() (map[stateAccessKey]struct{}, map[stateAccessKey]struct{}) { @@ -626,8 +639,41 @@ func (s *nativeStateDB) markForFinalise(addr common.Address) { } func (s *nativeStateDB) clearSnapshots() { + clear(s.journal) + s.journal = s.journal[:0] + clear(s.snapshots) + s.snapshots = s.snapshots[:0] +} + +func (s *nativeStateDB) reset(source StateReader) { + if source == nil { + source = NewMemoryState() + } + s.source = source + clear(s.accounts) + clear(s.base) + s.refund = 0 + clear(s.logs) + s.logs = s.logs[:0] + clearBytesMap(s.preimages) + s.accessList.reset() + clearNestedHashMaps(s.transientStates) + clear(s.finaliseAddrs) + clear(s.journal) s.journal = s.journal[:0] + clear(s.snapshots) s.snapshots = s.snapshots[:0] + if s.readSet != nil { + clear(s.readSet) + } + if s.writeSet != nil { + clear(s.writeSet) + } + s.txHash = common.Hash{} + s.txIndex = 0 + s.txIndexUint = 0 + s.err = nil + s.evm = nil } func (s *nativeStateDB) account(addr common.Address) *nativeAccount { @@ -705,6 +751,22 @@ func newAccessList() accessList { } } +func (al *accessList) reset() { + if al.addresses == nil { + al.addresses = map[common.Address]struct{}{} + } else { + clear(al.addresses) + } + if al.slots == nil { + al.slots = map[common.Address]map[common.Hash]struct{}{} + return + } + for _, slots := range al.slots { + clear(slots) + } + clear(al.slots) +} + func cloneAccessList(al accessList) accessList { cp := newAccessList() for addr := range al.addresses { @@ -765,6 +827,19 @@ func clonePreimages(preimages map[common.Hash][]byte) map[common.Hash][]byte { return cp } +func clearBytesMap(values map[common.Hash][]byte) { + for key := range values { + delete(values, key) + } +} + +func clearNestedHashMaps(values map[common.Address]map[common.Hash]common.Hash) { + for _, slots := range values { + clear(slots) + } + clear(values) +} + func cloneJournal(journal []nativeJournalEntry) []nativeJournalEntry { cp := make([]nativeJournalEntry, len(journal)) for i, entry := range journal { diff --git a/giga/evmonly/types.go b/giga/evmonly/types.go index 639d3f1694..6b5706e39a 100644 --- a/giga/evmonly/types.go +++ b/giga/evmonly/types.go @@ -28,6 +28,15 @@ type ResultSink interface { StoreReceipts(ctx context.Context, height uint64, receipts ethtypes.Receipts) error } +// BlockResultSink can retain a complete BlockResult without forcing the +// executor to copy changesets or receipts before handing them to an async sink. +// The sink must invoke release exactly once after it no longer references +// result. If StoreBlockResult returns an error, the executor releases that sink +// reference. +type BlockResultSink interface { + StoreBlockResult(ctx context.Context, height uint64, result *BlockResult, release func()) error +} + // BlockRequest contains all consensus/runtime inputs needed to execute a block. // Txs must be raw Ethereum transaction RLP bytes. type BlockRequest struct { @@ -68,6 +77,19 @@ type BlockResult struct { Txs []TxResult Receipts ethtypes.Receipts GasUsed uint64 + + lease *blockResultLease +} + +// Release returns a pooled BlockResult to its executor-owned pool. It is a +// no-op for results that were not allocated from a pool. +func (r *BlockResult) Release() { + if r == nil || r.lease == nil { + return + } + lease := r.lease + r.lease = nil + lease.release() } // StateChangeSet is the deterministic EVM-native state output for a block. From a38d56ec60ed02e452d930e22512c0027a6732d3 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Thu, 2 Jul 2026 21:15:34 +0800 Subject: [PATCH 19/29] preserve pooled result leases --- giga/evmonly/result_pool.go | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/giga/evmonly/result_pool.go b/giga/evmonly/result_pool.go index 31d2be5f83..f8617853f5 100644 --- a/giga/evmonly/result_pool.go +++ b/giga/evmonly/result_pool.go @@ -88,9 +88,18 @@ func (r *BlockResult) prepareForBlock(txCapacity int) { } func (r *BlockResult) prepareIndexedResults(txCount int) { - r.prepareForBlock(txCount) - r.Txs = r.Txs[:txCount] - r.Receipts = r.Receipts[:txCount] + if cap(r.Txs) < txCount { + r.Txs = make([]TxResult, txCount) + } else { + r.Txs = r.Txs[:txCount] + clear(r.Txs) + } + if cap(r.Receipts) < txCount { + r.Receipts = make(ethtypes.Receipts, txCount) + } else { + r.Receipts = r.Receipts[:txCount] + clear(r.Receipts) + } } func (r *BlockResult) resetForPool() { From 7ba41c4ff7f6e21cbeb6ddfd2822be58be0b04d1 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Thu, 2 Jul 2026 21:20:43 +0800 Subject: [PATCH 20/29] avoid evmonly statedb scratch reuse --- giga/evmonly/occ.go | 23 +++-------------------- giga/evmonly/occ_pool.go | 11 +++++------ 2 files changed, 8 insertions(+), 26 deletions(-) diff --git a/giga/evmonly/occ.go b/giga/evmonly/occ.go index c9977a0628..27c8032745 100644 --- a/giga/evmonly/occ.go +++ b/giga/evmonly/occ.go @@ -29,19 +29,6 @@ type occTxRange struct { end int } -type occWorkerScratch struct { - stateDB *nativeStateDB -} - -func (s *occWorkerScratch) resetStateDB(source StateReader) *nativeStateDB { - if s.stateDB == nil { - s.stateDB = newNativeStateDB(source) - } else { - s.stateDB.reset(source) - } - return s.stateDB -} - func (e *Executor) executeBlockOCC(ctx context.Context, req PreparedBlock) (*BlockResult, error) { chainConfig := e.chainConfig(req.Context) blockCtx := buildBlockContext(req.Context) @@ -63,9 +50,9 @@ func (e *Executor) executeBlockOCC(ctx context.Context, req PreparedBlock) (*Blo pool = newOCCWorkerPool(workers) defer pool.Close() } - if err := pool.Run(ctx, occRanges(txCount, chunkSize), func(workerCtx context.Context, txRange occTxRange, scratch *occWorkerScratch) error { + if err := pool.Run(ctx, occRanges(txCount, chunkSize), func(workerCtx context.Context, txRange occTxRange) error { for idx := txRange.start; idx < txRange.end; idx++ { - result, err := e.executeTxSpeculative(workerCtx, scratch, req, idx, chainConfig, blockCtx, baseFee, gasLimit) + result, err := e.executeTxSpeculative(workerCtx, req, idx, chainConfig, blockCtx, baseFee, gasLimit) if err != nil { return err } @@ -113,7 +100,6 @@ func occChunkSize(txCount int, workers int) int { func (e *Executor) executeTxSpeculative( ctx context.Context, - scratch *occWorkerScratch, req PreparedBlock, txIndex int, chainConfig *params.ChainConfig, @@ -121,11 +107,8 @@ func (e *Executor) executeTxSpeculative( baseFee *big.Int, gasLimit uint64, ) (occTxExecution, error) { - if scratch == nil { - scratch = &occWorkerScratch{} - } p := req.Txs[txIndex] - stateDB := scratch.resetStateDB(e.state) + stateDB := newNativeStateDB(e.state) stateDB.enableAccessTracking() evm := vm.NewEVM(blockCtx, stateDB, chainConfig, vm.Config{}, nil) stateDB.SetEVM(evm) diff --git a/giga/evmonly/occ_pool.go b/giga/evmonly/occ_pool.go index 900126bad1..0678968a96 100644 --- a/giga/evmonly/occ_pool.go +++ b/giga/evmonly/occ_pool.go @@ -16,7 +16,7 @@ type occWorkerPool struct { type occPoolJob struct { ctx context.Context txRange occTxRange - run func(context.Context, occTxRange, *occWorkerScratch) error + run func(context.Context, occTxRange) error done *sync.WaitGroup cancel context.CancelFunc @@ -46,23 +46,22 @@ func newOCCWorkerPool(workers int) *occWorkerPool { } func (p *occWorkerPool) runWorker() { - scratch := &occWorkerScratch{} for { select { case <-p.stop: return case job := <-p.jobs: - p.runJob(job, scratch) + p.runJob(job) } } } -func (p *occWorkerPool) runJob(job occPoolJob, scratch *occWorkerScratch) { +func (p *occWorkerPool) runJob(job occPoolJob) { defer job.done.Done() if err := job.ctx.Err(); err != nil { return } - if err := job.run(job.ctx, job.txRange, scratch); err != nil { + if err := job.run(job.ctx, job.txRange); err != nil { job.errOnce.Do(func() { *job.err = err job.cancel() @@ -70,7 +69,7 @@ func (p *occWorkerPool) runJob(job occPoolJob, scratch *occWorkerScratch) { } } -func (p *occWorkerPool) Run(ctx context.Context, ranges []occTxRange, run func(context.Context, occTxRange, *occWorkerScratch) error) error { +func (p *occWorkerPool) Run(ctx context.Context, ranges []occTxRange, run func(context.Context, occTxRange) error) error { jobCtx, cancel := context.WithCancel(ctx) defer cancel() From 9ae1cfb7369d52fa8117f4054ec4bc86c82fac5e Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Thu, 2 Jul 2026 21:42:26 +0800 Subject: [PATCH 21/29] add evmonly OCC fallback stats --- giga/evmonly/executor_test.go | 16 ++++ giga/evmonly/occ.go | 138 +++++++++++++++++++++++++++++++--- giga/evmonly/result_pool.go | 1 + giga/evmonly/types.go | 20 +++++ 4 files changed, 164 insertions(+), 11 deletions(-) diff --git a/giga/evmonly/executor_test.go b/giga/evmonly/executor_test.go index 911d184b41..4a4bf9d290 100644 --- a/giga/evmonly/executor_test.go +++ b/giga/evmonly/executor_test.go @@ -215,6 +215,9 @@ func TestExecutorOCCNonConflictingTransfersMatchSequential(t *testing.T) { require.Equal(t, seqResult.GasUsed, occResult.GasUsed) require.Len(t, occResult.Txs, txCount) require.Len(t, occResult.Receipts, txCount) + require.True(t, occResult.OCCStats.Attempted) + require.False(t, occResult.OCCStats.Fallback) + require.Zero(t, occResult.OCCStats.ConflictCount) for i := range txCount { require.Equal(t, seqResult.Txs[i].Hash, occResult.Txs[i].Hash) require.Equal(t, seqResult.Txs[i].Status, occResult.Txs[i].Status) @@ -256,6 +259,19 @@ func TestExecutorOCCConflictingTransfersMatchSequential(t *testing.T) { seqState.ApplyChangeSet(seqResult.ChangeSet) occState.ApplyChangeSet(occResult.ChangeSet) require.Equal(t, seqResult.GasUsed, occResult.GasUsed) + require.True(t, occResult.OCCStats.Attempted) + require.True(t, occResult.OCCStats.Fallback) + require.Equal(t, "conflict", occResult.OCCStats.FallbackReason) + require.Greater(t, occResult.OCCStats.ConflictCount, uint64(0)) + require.NotEmpty(t, occResult.OCCStats.ConflictSamples) + foundRecipientBalanceConflict := false + for _, conflict := range occResult.OCCStats.ConflictSamples { + if conflict.Kind == "balance" && conflict.Address == recipient { + foundRecipientBalanceConflict = true + require.Greater(t, conflict.Count, uint64(0)) + } + } + require.True(t, foundRecipientBalanceConflict) require.Equal(t, seqState.GetBalance(recipient), occState.GetBalance(recipient)) require.Equal(t, big.NewInt(int64(txCount*3)), occState.GetBalance(recipient)) } diff --git a/giga/evmonly/occ.go b/giga/evmonly/occ.go index 27c8032745..30654c46fd 100644 --- a/giga/evmonly/occ.go +++ b/giga/evmonly/occ.go @@ -62,10 +62,21 @@ func (e *Executor) executeBlockOCC(ctx context.Context, req PreparedBlock) (*Blo }); err != nil { return nil, err } - if !validateOCCResults(results, gasLimit) { - return e.executeBlockSequential(ctx, req) + validation := validateOCCResults(results, gasLimit) + if !validation.valid { + result, err := e.executeBlockSequential(ctx, req) + if err != nil { + return nil, err + } + result.OCCStats = validation.stats(true) + return result, nil } - return e.mergeOCCResults(ctx, results) + result, err := e.mergeOCCResults(ctx, results) + if err != nil { + return nil, err + } + result.OCCStats = validation.stats(false) + return result, nil } func occRanges(txCount int, chunkSize int) []occTxRange { @@ -139,23 +150,125 @@ func (e *Executor) executeTxSpeculative( }, nil } -func validateOCCResults(results []occTxExecution, gasLimit uint64) bool { +type occValidationResult struct { + valid bool + fallbackReason string + conflictCount uint64 + conflicts map[occConflictAggregationKey]uint64 +} + +type occConflictAggregationKey struct { + access string + kind stateAccessKind + address common.Address + slot common.Hash +} + +const ( + occFallbackReasonConflict = "conflict" + occFallbackReasonGasLimit = "gas_limit" + occFallbackReasonGasOverflow = "gas_overflow" +) + +func validateOCCResults(results []occTxExecution, gasLimit uint64) occValidationResult { writes := newStateAccessIndex() var totalGas uint64 + validation := occValidationResult{valid: true} for _, result := range results { if result.gasUsed > math.MaxUint64-totalGas { - return false + validation.valid = false + validation.fallbackReason = occFallbackReasonGasOverflow + return validation } totalGas += result.gasUsed if totalGas > gasLimit { - return false - } - if writes.conflictsWithAny(result.readSet) || writes.conflictsWithAny(result.writeSet) { - return false + validation.valid = false + validation.fallbackReason = occFallbackReasonGasLimit + return validation } + validation.addConflicts("read", writes, result.readSet) + validation.addConflicts("write", writes, result.writeSet) writes.addAll(result.writeSet) } - return true + if validation.conflictCount > 0 { + validation.valid = false + validation.fallbackReason = occFallbackReasonConflict + } + return validation +} + +func (r *occValidationResult) addConflicts(access string, writes *stateAccessIndex, set map[stateAccessKey]struct{}) { + for key := range set { + if !writes.conflictsWith(key) { + continue + } + if r.conflicts == nil { + r.conflicts = map[occConflictAggregationKey]uint64{} + } + r.conflictCount++ + r.conflicts[occConflictAggregationKey{ + access: access, + kind: key.kind, + address: key.address, + slot: key.slot, + }]++ + } +} + +func (r occValidationResult) stats(fallback bool) OCCStats { + stats := OCCStats{ + Attempted: true, + Fallback: fallback, + FallbackReason: r.fallbackReason, + ConflictCount: r.conflictCount, + } + if len(r.conflicts) == 0 { + return stats + } + keys := make([]occConflictAggregationKey, 0, len(r.conflicts)) + for key := range r.conflicts { + keys = append(keys, key) + } + sort.Slice(keys, func(i, j int) bool { + left, right := keys[i], keys[j] + if left.access != right.access { + return left.access < right.access + } + if left.kind != right.kind { + return left.kind < right.kind + } + if cmp := bytes.Compare(left.address[:], right.address[:]); cmp != 0 { + return cmp < 0 + } + return bytes.Compare(left.slot[:], right.slot[:]) < 0 + }) + for _, key := range keys { + stats.ConflictSamples = append(stats.ConflictSamples, OCCConflictCount{ + Access: key.access, + Kind: key.kind.String(), + Address: key.address, + Slot: key.slot, + Count: r.conflicts[key], + }) + } + return stats +} + +func (k stateAccessKind) String() string { + switch k { + case stateAccessAccount: + return "account" + case stateAccessBalance: + return "balance" + case stateAccessNonce: + return "nonce" + case stateAccessCode: + return "code" + case stateAccessStorage: + return "storage" + default: + return "unknown" + } } func (e *Executor) mergeOCCResults(ctx context.Context, results []occTxExecution) (*BlockResult, error) { @@ -220,7 +333,10 @@ func (i *stateAccessIndex) conflictsWith(key stateAccessKey) bool { func (i *stateAccessIndex) addAll(set map[stateAccessKey]struct{}) { for key := range set { i.exact[key] = struct{}{} - i.touched[key.address] = struct{}{} + // Exist/Empty account reads depend on account metadata, not storage slots. + if key.kind != stateAccessStorage { + i.touched[key.address] = struct{}{} + } if key.kind == stateAccessAccount { i.account[key.address] = struct{}{} } diff --git a/giga/evmonly/result_pool.go b/giga/evmonly/result_pool.go index f8617853f5..15e31adc4c 100644 --- a/giga/evmonly/result_pool.go +++ b/giga/evmonly/result_pool.go @@ -109,6 +109,7 @@ func (r *BlockResult) resetForPool() { clear(r.Receipts) r.Receipts = r.Receipts[:0] r.GasUsed = 0 + r.OCCStats = OCCStats{} r.lease = nil } diff --git a/giga/evmonly/types.go b/giga/evmonly/types.go index 6b5706e39a..730a64a02a 100644 --- a/giga/evmonly/types.go +++ b/giga/evmonly/types.go @@ -77,6 +77,7 @@ type BlockResult struct { Txs []TxResult Receipts ethtypes.Receipts GasUsed uint64 + OCCStats OCCStats lease *blockResultLease } @@ -92,6 +93,25 @@ func (r *BlockResult) Release() { lease.release() } +// OCCStats reports optimistic concurrency control behavior for a block. +type OCCStats struct { + Attempted bool + Fallback bool + FallbackReason string + ConflictCount uint64 + ConflictSamples []OCCConflictCount +} + +// OCCConflictCount aggregates conflicts by the access key that forced OCC to +// fall back to sequential execution. +type OCCConflictCount struct { + Access string + Kind string + Address common.Address + Slot common.Hash + Count uint64 +} + // StateChangeSet is the deterministic EVM-native state output for a block. // Values are post-block values, not deltas. type StateChangeSet struct { From 145f08917367e789642fd0f8c417c9619117e509 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Thu, 2 Jul 2026 21:59:39 +0800 Subject: [PATCH 22/29] fix evmonly scaffold review findings --- giga/evmonly/README.md | 2 + giga/evmonly/executor.go | 24 +++++- giga/evmonly/executor_test.go | 144 +++++++++++++++++++++++++++++++++- giga/evmonly/occ.go | 56 ++++++++----- giga/evmonly/result_pool.go | 2 + giga/evmonly/state.go | 11 +++ giga/evmonly/state_db.go | 69 +++++++++------- giga/evmonly/types.go | 9 ++- 8 files changed, 261 insertions(+), 56 deletions(-) diff --git a/giga/evmonly/README.md b/giga/evmonly/README.md index b748f13247..ae131631a5 100644 --- a/giga/evmonly/README.md +++ b/giga/evmonly/README.md @@ -109,10 +109,12 @@ runtime-provided hash source in a later integration step. - `Balances`: final balance for each changed EVM address - `Nonces`: final nonce for each changed EVM address - `Code`: final bytecode updates or deletions +- `StorageClears`: addresses whose persisted storage must be fully cleared - `Storage`: final storage slot updates or deletions The changeset should be deterministic and serializable by the runtime layer. The executor should not require `sdk.Context` or Cosmos stores to build it. +State writers should apply `StorageClears` before per-slot `Storage` updates. `Receipts` are emitted in transaction order and should contain the Ethereum receipt fields needed by RPC and indexing: status, cumulative gas used, bloom, diff --git a/giga/evmonly/executor.go b/giga/evmonly/executor.go index 92bfd05561..0a2cfea88f 100644 --- a/giga/evmonly/executor.go +++ b/giga/evmonly/executor.go @@ -81,6 +81,9 @@ func (e *Executor) ExecuteBlock(ctx context.Context, req BlockRequest) (*BlockRe func (e *Executor) PrepareBlock(ctx context.Context, req BlockRequest) (PreparedBlock, error) { chainConfig := e.chainConfig(req.Context) + if err := validateBlockContext(chainConfig, req.Context); err != nil { + return PreparedBlock{}, err + } signer := ethtypes.MakeSigner(chainConfig, new(big.Int).SetUint64(req.Context.Number), req.Context.Time) parsed, err := parseBlockTxs(ctx, req.Txs, signer) if err != nil { @@ -93,6 +96,9 @@ func (e *Executor) PrepareBlock(ctx context.Context, req BlockRequest) (Prepared } func (e *Executor) ExecutePreparedBlock(ctx context.Context, req PreparedBlock) (*BlockResult, error) { + if err := validateBlockContext(e.chainConfig(req.Context), req.Context); err != nil { + return nil, err + } var result *BlockResult var err error if len(req.Txs) == 0 { @@ -173,7 +179,7 @@ func (e *Executor) executeBlockSequential(ctx context.Context, req PreparedBlock gasLimit = math.MaxUint64 } gasPool := new(core.GasPool).AddGas(gasLimit) - baseFee := cloneBig(req.Context.BaseFee) + baseFee := cloneOptionalBig(req.Context.BaseFee) result, err := e.acquireBlockResult(ctx, len(req.Txs)) if err != nil { @@ -316,8 +322,8 @@ func transactionToPreparedMessage(p PreparedTx, baseFee *big.Int) *core.Message func buildBlockContext(ctx BlockContext) vm.BlockContext { prevRandao := ctx.PrevRandao - baseFee := cloneBig(ctx.BaseFee) - blobBaseFee := cloneBig(ctx.BlobBaseFee) + baseFee := cloneOptionalBig(ctx.BaseFee) + blobBaseFee := cloneOptionalBig(ctx.BlobBaseFee) gasLimit := ctx.GasLimit if gasLimit == 0 { gasLimit = math.MaxUint64 @@ -384,6 +390,13 @@ func (e *Executor) chainConfig(ctx BlockContext) *params.ChainConfig { return &cfg } +func validateBlockContext(chainConfig *params.ChainConfig, ctx BlockContext) error { + if chainConfig != nil && chainConfig.IsLondon(new(big.Int).SetUint64(ctx.Number)) && ctx.BaseFee == nil { + return errMissingBaseFee + } + return nil +} + func effectiveGasPrice(tx *ethtypes.Transaction, baseFee *big.Int) *big.Int { if baseFee == nil { return tx.GasPrice() @@ -394,4 +407,7 @@ func effectiveGasPrice(tx *ethtypes.Transaction, baseFee *big.Int) *big.Int { return tx.GasPrice() } -var errInsufficientGasPrice = fmt.Errorf("insufficient gas price") +var ( + errInsufficientGasPrice = fmt.Errorf("insufficient gas price") + errMissingBaseFee = fmt.Errorf("missing base fee for post-London block") +) diff --git a/giga/evmonly/executor_test.go b/giga/evmonly/executor_test.go index 4a4bf9d290..63072baaaa 100644 --- a/giga/evmonly/executor_test.go +++ b/giga/evmonly/executor_test.go @@ -61,7 +61,9 @@ func (s *recordingBlockResultSink) StoreBlockResult(_ context.Context, _ uint64, func TestExecutorEmptyBlock(t *testing.T) { executor := NewExecutor(Config{}) - result, err := executor.ExecuteBlock(context.Background(), BlockRequest{}) + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: blockContext(big.NewInt(713715)), + }) require.NoError(t, err) require.NotNil(t, result) @@ -181,6 +183,29 @@ func TestExecutorDynamicFeeTx(t *testing.T) { require.Equal(t, big.NewInt(11), state.GetBalance(recipient)) } +func TestExecutorRequiresBaseFeeAfterLondon(t *testing.T) { + chainID := big.NewInt(713715) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + recipient := testAddress(0xb4) + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(200_000_000_000_000)) + rawTx := signLegacyTx(t, key, chainID, 0, &recipient, big.NewInt(1), nil) + ctx := blockContext(chainID) + ctx.BaseFee = nil + + result, err := NewExecutor(Config{}, WithState(state)).ExecuteBlock(context.Background(), BlockRequest{ + Context: ctx, + Txs: [][]byte{rawTx}, + }) + + require.ErrorIs(t, err, errMissingBaseFee) + require.Nil(t, result) + require.Equal(t, big.NewInt(0), state.GetBalance(recipient)) +} + func TestExecutorOCCNonConflictingTransfersMatchSequential(t *testing.T) { chainID := big.NewInt(713715) txCount := 12 @@ -276,6 +301,34 @@ func TestExecutorOCCConflictingTransfersMatchSequential(t *testing.T) { require.Equal(t, big.NewInt(int64(txCount*3)), occState.GetBalance(recipient)) } +func TestExecutorOCCFallsBackWhenDeclaredGasExceedsBlockLimit(t *testing.T) { + chainID := big.NewInt(713715) + rawTxs := make([][]byte, 0, 2) + state := NewMemoryState() + + for i := 0; i < 2; i++ { + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + recipient := common.BigToAddress(big.NewInt(int64(20_000 + i))) + state.SetBalance(sender, big.NewInt(1_000_000)) + rawTxs = append(rawTxs, signLegacyTxWithGasPrice(t, key, chainID, 0, &recipient, big.NewInt(1), nil, 90_000, big.NewInt(0))) + } + + ctx := blockContext(chainID) + ctx.GasLimit = 100_000 + executor := NewExecutor(Config{MinGasPrice: big.NewInt(0), OCCWorkers: 2}, WithState(state)) + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: ctx, + Txs: rawTxs, + }) + + require.Error(t, err) + require.True(t, errors.Is(err, core.ErrGasLimitReached)) + require.Nil(t, result) +} + func TestExecutorReceiptAndLogMetadata(t *testing.T) { chainID := big.NewInt(713715) key, err := crypto.GenerateKey() @@ -739,6 +792,56 @@ func TestExecutorFinalisesAfterEachTx(t *testing.T) { require.Equal(t, big.NewInt(0), state.GetBalance(beneficiary)) } +func TestStateDBSelfDestructEmitsStorageClear(t *testing.T) { + contract := testAddress(0xc4) + loadedKey := testHash(0x01) + unreadKey := testHash(0x02) + loadedValue := testHash(0x11) + unreadValue := testHash(0x22) + + state := NewMemoryState() + state.SetCode(contract, []byte{0x60, 0x00, 0x00}) + state.SetState(contract, loadedKey, loadedValue) + state.SetState(contract, unreadKey, unreadValue) + stateDB := newNativeStateDB(state) + + require.Equal(t, loadedValue, stateDB.GetState(contract, loadedKey)) + stateDB.SelfDestruct(contract) + stateDB.Finalise(true) + + changes := stateDB.ChangeSet() + require.Contains(t, changes.StorageClears, contract) + state.ApplyChangeSet(changes) + require.Empty(t, state.GetCode(contract)) + require.Equal(t, common.Hash{}, state.GetState(contract, loadedKey)) + require.Equal(t, common.Hash{}, state.GetState(contract, unreadKey)) +} + +func TestStateDBStorageClearThenSameValueWriteIsEmitted(t *testing.T) { + contract := testAddress(0xc5) + key := testHash(0x01) + value := testHash(0x22) + + state := NewMemoryState() + state.SetState(contract, key, value) + stateDB := newNativeStateDB(state) + + require.Equal(t, value, stateDB.GetState(contract, key)) + stateDB.SelfDestruct(contract) + stateDB.Finalise(true) + stateDB.SetState(contract, key, value) + + changes := stateDB.ChangeSet() + require.Contains(t, changes.StorageClears, contract) + require.Len(t, changes.Storage, 1) + require.Equal(t, key, changes.Storage[0].Key) + require.Equal(t, value, changes.Storage[0].Value) + require.False(t, changes.Storage[0].Delete) + + state.ApplyChangeSet(changes) + require.Equal(t, value, state.GetState(contract, key)) +} + func TestPrepareClearsTransientStorage(t *testing.T) { stateDB := newNativeStateDB(NewMemoryState()) addr := common.HexToAddress("0x00000000000000000000000000000000000000a3") @@ -806,6 +909,45 @@ func TestStateDBFirstStorageReadPreservesBase(t *testing.T) { }) } +func TestStateDBZeroStorageWriteStaysDirty(t *testing.T) { + addr := testAddress(0xaa) + key := testHash(0x01) + value := testHash(0x02) + + state := NewMemoryState() + state.SetState(addr, key, value) + stateDB := newNativeStateDB(state) + + require.Equal(t, value, stateDB.GetState(addr, key)) + require.Equal(t, value, stateDB.SetState(addr, key, common.Hash{})) + require.Equal(t, common.Hash{}, stateDB.GetState(addr, key)) + + changes := stateDB.ChangeSet() + require.Len(t, changes.Storage, 1) + require.Equal(t, addr, changes.Storage[0].Address) + require.Equal(t, key, changes.Storage[0].Key) + require.True(t, changes.Storage[0].Delete) + + state.ApplyChangeSet(changes) + require.Equal(t, common.Hash{}, state.GetState(addr, key)) +} + +func TestStateDBGetCodeHashDistinguishesExistingCodelessAccounts(t *testing.T) { + missing := testAddress(0xab) + eoa := testAddress(0xac) + contract := testAddress(0xad) + code := []byte{0x60, 0x00, 0x00} + + state := NewMemoryState() + state.SetBalance(eoa, big.NewInt(1)) + state.SetCode(contract, code) + stateDB := newNativeStateDB(state) + + require.Equal(t, common.Hash{}, stateDB.GetCodeHash(missing)) + require.Equal(t, ethtypes.EmptyCodeHash, stateDB.GetCodeHash(eoa)) + require.Equal(t, crypto.Keccak256Hash(code), stateDB.GetCodeHash(contract)) +} + func TestFinaliseClearsRefund(t *testing.T) { stateDB := newNativeStateDB(NewMemoryState()) stateDB.AddRefund(12) diff --git a/giga/evmonly/occ.go b/giga/evmonly/occ.go index 30654c46fd..c7bd4a0d75 100644 --- a/giga/evmonly/occ.go +++ b/giga/evmonly/occ.go @@ -22,6 +22,7 @@ type occTxExecution struct { readSet map[stateAccessKey]struct{} writeSet map[stateAccessKey]struct{} gasUsed uint64 + gasLimit uint64 } type occTxRange struct { @@ -32,7 +33,7 @@ type occTxRange struct { func (e *Executor) executeBlockOCC(ctx context.Context, req PreparedBlock) (*BlockResult, error) { chainConfig := e.chainConfig(req.Context) blockCtx := buildBlockContext(req.Context) - baseFee := cloneBig(req.Context.BaseFee) + baseFee := cloneOptionalBig(req.Context.BaseFee) gasLimit := req.Context.GasLimit if gasLimit == 0 { gasLimit = math.MaxUint64 @@ -147,6 +148,7 @@ func (e *Executor) executeTxSpeculative( readSet: readSet, writeSet: writeSet, gasUsed: txResult.GasUsed, + gasLimit: p.Tx.Gas(), }, nil } @@ -172,16 +174,23 @@ const ( func validateOCCResults(results []occTxExecution, gasLimit uint64) occValidationResult { writes := newStateAccessIndex() - var totalGas uint64 + var totalGasLimit uint64 + var totalGasUsed uint64 validation := occValidationResult{valid: true} for _, result := range results { - if result.gasUsed > math.MaxUint64-totalGas { + if len(result.changeSet.StorageClears) > 0 { + validation.valid = false + validation.fallbackReason = occFallbackReasonConflict + return validation + } + if result.gasLimit > math.MaxUint64-totalGasLimit || result.gasUsed > math.MaxUint64-totalGasUsed { validation.valid = false validation.fallbackReason = occFallbackReasonGasOverflow return validation } - totalGas += result.gasUsed - if totalGas > gasLimit { + totalGasLimit += result.gasLimit + totalGasUsed += result.gasUsed + if totalGasLimit > gasLimit { validation.valid = false validation.fallbackReason = occFallbackReasonGasLimit return validation @@ -307,15 +316,6 @@ func newStateAccessIndex() *stateAccessIndex { } } -func (i *stateAccessIndex) conflictsWithAny(set map[stateAccessKey]struct{}) bool { - for key := range set { - if i.conflictsWith(key) { - return true - } - } - return false -} - func (i *stateAccessIndex) conflictsWith(key stateAccessKey) bool { if _, ok := i.exact[key]; ok { return true @@ -348,17 +348,12 @@ type storageChangeKey struct { key common.Hash } -func mergeChangeSets(results []occTxExecution) StateChangeSet { - var merged StateChangeSet - mergeChangeSetsInto(results, &merged) - return merged -} - func mergeChangeSetsInto(results []occTxExecution, merged *StateChangeSet) { merged.resetForReuse() balances := map[common.Address]*big.Int{} nonces := map[common.Address]uint64{} code := map[common.Address]CodeChange{} + storageClears := map[common.Address]struct{}{} storage := map[storageChangeKey]StorageChange{} for _, result := range results { @@ -375,6 +370,14 @@ func mergeChangeSetsInto(results []occTxExecution, merged *StateChangeSet) { Delete: change.Delete, } } + for _, addr := range result.changeSet.StorageClears { + storageClears[addr] = struct{}{} + for key := range storage { + if key.address == addr { + delete(storage, key) + } + } + } for _, change := range result.changeSet.Storage { storage[storageChangeKey{address: change.Address, key: change.Key}] = change } @@ -394,6 +397,8 @@ func mergeChangeSetsInto(results []occTxExecution, merged *StateChangeSet) { change.Code = cloneBytes(change.Code) merged.Code = append(merged.Code, change) } + storageClearAddrs := sortedAddressesFromSet(storageClears) + merged.StorageClears = append(merged.StorageClears, storageClearAddrs...) storageKeys := make([]storageChangeKey, 0, len(storage)) for key := range storage { storageKeys = append(storageKeys, key) @@ -441,3 +446,14 @@ func sortedAddressesFromCodeMap(values map[common.Address]CodeChange) []common.A }) return addrs } + +func sortedAddressesFromSet(values map[common.Address]struct{}) []common.Address { + addrs := make([]common.Address, 0, len(values)) + for addr := range values { + addrs = append(addrs, addr) + } + sort.Slice(addrs, func(i, j int) bool { + return bytes.Compare(addrs[i][:], addrs[j][:]) < 0 + }) + return addrs +} diff --git a/giga/evmonly/result_pool.go b/giga/evmonly/result_pool.go index 15e31adc4c..feb7424b4a 100644 --- a/giga/evmonly/result_pool.go +++ b/giga/evmonly/result_pool.go @@ -120,6 +120,8 @@ func (cs *StateChangeSet) resetForReuse() { cs.Nonces = cs.Nonces[:0] clear(cs.Code) cs.Code = cs.Code[:0] + clear(cs.StorageClears) + cs.StorageClears = cs.StorageClears[:0] clear(cs.Storage) cs.Storage = cs.Storage[:0] } diff --git a/giga/evmonly/state.go b/giga/evmonly/state.go index 8e92570166..88b415675b 100644 --- a/giga/evmonly/state.go +++ b/giga/evmonly/state.go @@ -134,6 +134,10 @@ func (s *MemoryState) ApplyChangeSet(cs StateChangeSet) { acct.Code = cloneBytes(change.Code) } } + for _, addr := range cs.StorageClears { + acct := s.getOrCreateAccountLocked(addr) + acct.Storage = map[common.Hash]common.Hash{} + } for _, change := range cs.Storage { acct := s.getOrCreateAccountLocked(change.Address) if acct.Storage == nil { @@ -163,6 +167,13 @@ func cloneBig(v *big.Int) *big.Int { return new(big.Int).Set(v) } +func cloneOptionalBig(v *big.Int) *big.Int { + if v == nil { + return nil + } + return new(big.Int).Set(v) +} + func cloneBytes(v []byte) []byte { if len(v) == 0 { return nil diff --git a/giga/evmonly/state_db.go b/giga/evmonly/state_db.go index e91fcd6ed1..2a0d39f7dc 100644 --- a/giga/evmonly/state_db.go +++ b/giga/evmonly/state_db.go @@ -48,11 +48,16 @@ type nativeAccount struct { Balance *uint256.Int Nonce uint64 Code []byte - Storage map[common.Hash]common.Hash + Storage map[common.Hash]storageValue + StorageCleared bool SelfDestructed bool Created bool } +type storageValue struct { + value common.Hash +} + type nativeSnapshot struct { journalLen int refund uint64 @@ -154,8 +159,11 @@ func (s *nativeStateDB) ChangeSetInto(changes *StateChangeSet) { } storageKeys := storageKeyUnion(base.Storage, acct.Storage) for _, key := range storageKeys { - oldValue := base.Storage[key] - newValue := acct.Storage[key] + oldValue := storageHash(base.Storage, key) + newValue := storageHash(acct.Storage, key) + if acct.StorageCleared { + oldValue = common.Hash{} + } if oldValue == newValue { continue } @@ -166,6 +174,9 @@ func (s *nativeStateDB) ChangeSetInto(changes *StateChangeSet) { Delete: newValue == (common.Hash{}), }) } + if acct.StorageCleared { + changes.StorageClears = append(changes.StorageClears, addr) + } } } @@ -176,7 +187,7 @@ func (s *nativeStateDB) CreateAccount(addr common.Address) { balance := acct.Balance.Clone() *acct = nativeAccount{ Balance: balance, - Storage: map[common.Hash]common.Hash{}, + Storage: map[common.Hash]storageValue{}, Created: true, } s.markForFinalise(addr) @@ -247,11 +258,15 @@ func (s *nativeStateDB) SetNonce(addr common.Address, nonce uint64, _ tracing.No } func (s *nativeStateDB) GetCodeHash(addr common.Address) common.Hash { - code := s.GetCode(addr) - if len(code) == 0 { + s.markRead(stateAccessKey{kind: stateAccessCode, address: addr}) + acct := s.account(addr) + if len(acct.Code) > 0 { + return crypto.Keccak256Hash(acct.Code) + } + if acct.Nonce == 0 && acct.Balance.IsZero() { return common.Hash{} } - return crypto.Keccak256Hash(code) + return ethtypes.EmptyCodeHash } func (s *nativeStateDB) GetCode(addr common.Address) []byte { @@ -291,26 +306,22 @@ func (s *nativeStateDB) GetRefund() uint64 { func (s *nativeStateDB) GetCommittedState(addr common.Address, key common.Hash) common.Hash { s.markRead(stateAccessKey{kind: stateAccessStorage, address: addr, slot: key}) s.ensureStorage(addr, key) - return s.baseAccount(addr).Storage[key] + return storageHash(s.baseAccount(addr).Storage, key) } func (s *nativeStateDB) GetState(addr common.Address, key common.Hash) common.Hash { s.markRead(stateAccessKey{kind: stateAccessStorage, address: addr, slot: key}) s.ensureStorage(addr, key) - return s.account(addr).Storage[key] + return storageHash(s.account(addr).Storage, key) } func (s *nativeStateDB) SetState(addr common.Address, key common.Hash, value common.Hash) common.Hash { s.ensureStorage(addr, key) acct := s.account(addr) - prev := acct.Storage[key] + prev := storageHash(acct.Storage, key) s.recordAccount(addr) s.markWrite(stateAccessKey{kind: stateAccessStorage, address: addr, slot: key}) - if value == (common.Hash{}) { - delete(acct.Storage, key) - } else { - acct.Storage[key] = value - } + acct.Storage[key] = storageValue{value: value} return prev } @@ -318,11 +329,9 @@ func (s *nativeStateDB) SetStorage(addr common.Address, states map[common.Hash]c acct := s.account(addr) s.recordAccount(addr) s.markWrite(stateAccessKey{kind: stateAccessAccount, address: addr}) - acct.Storage = map[common.Hash]common.Hash{} + acct.Storage = map[common.Hash]storageValue{} for key, value := range states { - if value != (common.Hash{}) { - acct.Storage[key] = value - } + acct.Storage[key] = storageValue{value: value} } } @@ -500,7 +509,8 @@ func (s *nativeStateDB) Finalise(bool) { if acct.SelfDestructed { s.recordAccount(addr) acct.Code = nil - acct.Storage = map[common.Hash]common.Hash{} + acct.Storage = map[common.Hash]storageValue{} + acct.StorageCleared = true acct.Nonce = 0 acct.SelfDestructed = false } @@ -702,13 +712,13 @@ func (s *nativeStateDB) ensureStorage(addr common.Address, key common.Hash) { base := s.baseAccount(addr) if _, ok := base.Storage[key]; !ok { if value := s.source.GetState(addr, key); value != (common.Hash{}) { - base.Storage[key] = value + base.Storage[key] = storageValue{value: value} } } acct := s.account(addr) if _, ok := acct.Storage[key]; !ok { - if value := base.Storage[key]; value != (common.Hash{}) { - acct.Storage[key] = value + if value := storageHash(base.Storage, key); value != (common.Hash{}) { + acct.Storage[key] = storageValue{value: value} } } } @@ -718,20 +728,21 @@ func (s *nativeStateDB) loadAccount(addr common.Address) *nativeAccount { Balance: uint256FromBig(s.source.GetBalance(addr)), Nonce: s.source.GetNonce(addr), Code: cloneBytes(s.source.GetCode(addr)), - Storage: map[common.Hash]common.Hash{}, + Storage: map[common.Hash]storageValue{}, } return acct } func (a *nativeAccount) clone() *nativeAccount { if a == nil { - return &nativeAccount{Balance: uint256.NewInt(0), Storage: map[common.Hash]common.Hash{}} + return &nativeAccount{Balance: uint256.NewInt(0), Storage: map[common.Hash]storageValue{}} } cp := &nativeAccount{ Balance: uint256.NewInt(0), Nonce: a.Nonce, Code: cloneBytes(a.Code), - Storage: map[common.Hash]common.Hash{}, + Storage: map[common.Hash]storageValue{}, + StorageCleared: a.StorageCleared, SelfDestructed: a.SelfDestructed, Created: a.Created, } @@ -870,7 +881,11 @@ func cloneSnapshots(snapshots []nativeSnapshot) []nativeSnapshot { return cp } -func storageKeyUnion(a, b map[common.Hash]common.Hash) []common.Hash { +func storageHash(values map[common.Hash]storageValue, key common.Hash) common.Hash { + return values[key].value +} + +func storageKeyUnion(a, b map[common.Hash]storageValue) []common.Hash { seen := map[common.Hash]struct{}{} for key := range a { seen[key] = struct{}{} diff --git a/giga/evmonly/types.go b/giga/evmonly/types.go index 730a64a02a..f68d1e254a 100644 --- a/giga/evmonly/types.go +++ b/giga/evmonly/types.go @@ -115,10 +115,11 @@ type OCCConflictCount struct { // StateChangeSet is the deterministic EVM-native state output for a block. // Values are post-block values, not deltas. type StateChangeSet struct { - Balances []BalanceChange - Nonces []NonceChange - Code []CodeChange - Storage []StorageChange + Balances []BalanceChange + Nonces []NonceChange + Code []CodeChange + StorageClears []common.Address + Storage []StorageChange } type BalanceChange struct { From 29047e5a21353a04ff1c3a7e4beaf39b0f568dc1 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Fri, 3 Jul 2026 11:15:22 +0800 Subject: [PATCH 23/29] address evmonly scaffold review threads --- giga/evmonly/config.go | 4 +- giga/evmonly/executor.go | 31 ++--- giga/evmonly/executor_test.go | 207 +++++++++++++++++++++++++++------- giga/evmonly/occ.go | 82 ++++++++++---- giga/evmonly/state_db.go | 158 +++++++++++++++++--------- giga/evmonly/types.go | 13 +-- 6 files changed, 349 insertions(+), 146 deletions(-) diff --git a/giga/evmonly/config.go b/giga/evmonly/config.go index c79dbef842..60a902c111 100644 --- a/giga/evmonly/config.go +++ b/giga/evmonly/config.go @@ -18,8 +18,8 @@ type Config struct { OCCWorkers int // BlockResultPoolSize enables a bounded reusable output pool. Callers that // enable it must call BlockResult.Release when they are done with returned - // results. Async sinks should implement BlockResultSink so the executor can - // retain results until the sink releases them. + // results. Result sinks receive a retained result and must release it after + // they finish async persistence. BlockResultPoolSize int } diff --git a/giga/evmonly/executor.go b/giga/evmonly/executor.go index 0a2cfea88f..577eccc209 100644 --- a/giga/evmonly/executor.go +++ b/giga/evmonly/executor.go @@ -119,7 +119,7 @@ func (e *Executor) ExecutePreparedBlock(ctx context.Context, req PreparedBlock) } func (e *Executor) acquireBlockResult(ctx context.Context, txCapacity int) (*BlockResult, error) { - if e.resultPool == nil || !e.canPoolBlockResults() { + if e.resultPool == nil { result := &BlockResult{} result.prepareForBlock(txCapacity) return result, nil @@ -127,31 +127,14 @@ func (e *Executor) acquireBlockResult(ctx context.Context, txCapacity int) (*Blo return e.resultPool.acquire(ctx, txCapacity) } -func (e *Executor) canPoolBlockResults() bool { - if e.resultSink == nil { - return true - } - _, ok := e.resultSink.(BlockResultSink) - return ok -} - func (e *Executor) sinkBlockResult(ctx context.Context, height uint64, result *BlockResult) error { if e.resultSink == nil || result == nil { return nil } - if sink, ok := e.resultSink.(BlockResultSink); ok { - release := result.retain() - if err := sink.StoreBlockResult(ctx, height, result, release); err != nil { - release() - return fmt.Errorf("store block result for block %d: %w", height, err) - } - return nil - } - if err := e.resultSink.StoreChangeSet(ctx, height, result.ChangeSet); err != nil { - return fmt.Errorf("store changeset for block %d: %w", height, err) - } - if err := e.resultSink.StoreReceipts(ctx, height, result.Receipts); err != nil { - return fmt.Errorf("store receipts for block %d: %w", height, err) + release := result.retain() + if err := e.resultSink.StoreBlockResult(ctx, height, result, release); err != nil { + release() + return fmt.Errorf("store block result for block %d: %w", height, err) } return nil } @@ -276,6 +259,10 @@ func (e *Executor) executeTx( if tx.To() == nil { receipt.ContractAddress = crypto.CreateAddress(p.Sender, tx.Nonce()) } + if tx.Type() == ethtypes.BlobTxType { + receipt.BlobGasUsed = tx.BlobGas() + receipt.BlobGasPrice = cloneOptionalBig(block.BlobBaseFee) + } receipt.Bloom = ethtypes.CreateBloom(receipt) txResult := TxResult{ diff --git a/giga/evmonly/executor_test.go b/giga/evmonly/executor_test.go index 63072baaaa..910982754f 100644 --- a/giga/evmonly/executor_test.go +++ b/giga/evmonly/executor_test.go @@ -13,6 +13,7 @@ import ( "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/params" + "github.com/holiman/uint256" "github.com/stretchr/testify/require" "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles" @@ -21,40 +22,15 @@ import ( const testGasPriceWei = 1_000_000_000 type recordingResultSink struct { - changeSetHeights []uint64 - receiptHeights []uint64 - changeSets []StateChangeSet - receipts []ethtypes.Receipts + heights []uint64 + results []*BlockResult + releases []func() } -func (s *recordingResultSink) StoreChangeSet(_ context.Context, height uint64, changeSet StateChangeSet) error { - s.changeSetHeights = append(s.changeSetHeights, height) - s.changeSets = append(s.changeSets, changeSet) - return nil -} - -func (s *recordingResultSink) StoreReceipts(_ context.Context, height uint64, receipts ethtypes.Receipts) error { - s.receiptHeights = append(s.receiptHeights, height) - s.receipts = append(s.receipts, receipts) - return nil -} - -type recordingBlockResultSink struct { - result *BlockResult - release func() -} - -func (s *recordingBlockResultSink) StoreChangeSet(context.Context, uint64, StateChangeSet) error { - return nil -} - -func (s *recordingBlockResultSink) StoreReceipts(context.Context, uint64, ethtypes.Receipts) error { - return nil -} - -func (s *recordingBlockResultSink) StoreBlockResult(_ context.Context, _ uint64, result *BlockResult, release func()) error { - s.result = result - s.release = release +func (s *recordingResultSink) StoreBlockResult(_ context.Context, height uint64, result *BlockResult, release func()) error { + s.heights = append(s.heights, height) + s.results = append(s.results, result) + s.releases = append(s.releases, release) return nil } @@ -121,12 +97,11 @@ func TestExecutorInvokesResultSink(t *testing.T) { }) require.NoError(t, err) - require.Len(t, sink.changeSets, 1) - require.Len(t, sink.receipts, 1) - require.Equal(t, []uint64{ctx.Number}, sink.changeSetHeights) - require.Equal(t, []uint64{ctx.Number}, sink.receiptHeights) - require.Equal(t, result.ChangeSet, sink.changeSets[0]) - require.Equal(t, result.Receipts, sink.receipts[0]) + require.Len(t, sink.results, 1) + require.Len(t, sink.releases, 1) + require.Equal(t, []uint64{ctx.Number}, sink.heights) + require.Same(t, result, sink.results[0]) + sink.releases[0]() } func TestExecutorPooledResultRelease(t *testing.T) { @@ -138,22 +113,22 @@ func TestExecutorPooledResultRelease(t *testing.T) { state := NewMemoryState() state.SetBalance(sender, big.NewInt(200_000_000_000_000)) - sink := &recordingBlockResultSink{} + sink := &recordingResultSink{} executor := NewExecutor(Config{BlockResultPoolSize: 1}, WithState(state), WithResultSink(sink)) rawTx := signLegacyTx(t, key, chainID, 0, &recipient, big.NewInt(7), nil) req := BlockRequest{Context: blockContext(chainID), Txs: [][]byte{rawTx}} first, err := executor.ExecuteBlock(context.Background(), req) require.NoError(t, err) - require.Same(t, first, sink.result) - require.NotNil(t, sink.release) - sink.release() + require.Same(t, first, sink.results[0]) + require.NotNil(t, sink.releases[0]) + sink.releases[0]() first.Release() second, err := executor.ExecuteBlock(context.Background(), req) require.NoError(t, err) require.Same(t, first, second) - sink.release() + sink.releases[1]() second.Release() } @@ -183,6 +158,47 @@ func TestExecutorDynamicFeeTx(t *testing.T) { require.Equal(t, big.NewInt(11), state.GetBalance(recipient)) } +func TestExecutorBlobTxReceiptMetadata(t *testing.T) { + chainID := big.NewInt(713715) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + recipient := testAddress(0xb5) + blobBaseFee := big.NewInt(3) + blobHash := common.Hash{0x01} + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000_000_000_000)) + rawTx := signBlobTxWithFees( + t, + key, + chainID, + 0, + recipient, + big.NewInt(1), + nil, + big.NewInt(1), + big.NewInt(3), + big.NewInt(3), + 100_000, + []common.Hash{blobHash}, + ) + ctx := blockContext(chainID) + ctx.BaseFee = big.NewInt(2) + ctx.BlobBaseFee = blobBaseFee + + result, err := NewExecutor(Config{MinGasPrice: big.NewInt(0)}, WithState(state)).ExecuteBlock(context.Background(), BlockRequest{ + Context: ctx, + Txs: [][]byte{rawTx}, + }) + + require.NoError(t, err) + require.Len(t, result.Receipts, 1) + require.Equal(t, uint8(ethtypes.BlobTxType), result.Receipts[0].Type) + require.Equal(t, uint64(params.BlobTxBlobGasPerBlob), result.Receipts[0].BlobGasUsed) + require.Equal(t, blobBaseFee, result.Receipts[0].BlobGasPrice) +} + func TestExecutorRequiresBaseFeeAfterLondon(t *testing.T) { chainID := big.NewInt(713715) key, err := crypto.GenerateKey() @@ -301,6 +317,77 @@ func TestExecutorOCCConflictingTransfersMatchSequential(t *testing.T) { require.Equal(t, big.NewInt(int64(txCount*3)), occState.GetBalance(recipient)) } +func TestExecutorOCCFeePayingTransfersDoNotConflictOnCoinbase(t *testing.T) { + chainID := big.NewInt(713715) + txCount := 4 + rawTxs := make([][]byte, 0, txCount) + senders := make([]common.Address, 0, txCount) + recipients := make([]common.Address, 0, txCount) + seqState := NewMemoryState() + occState := NewMemoryState() + + for i := 0; i < txCount; i++ { + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + recipient := common.BigToAddress(big.NewInt(int64(30_000 + i))) + senders = append(senders, sender) + recipients = append(recipients, recipient) + seqState.SetBalance(sender, big.NewInt(1_000_000_000)) + occState.SetBalance(sender, big.NewInt(1_000_000_000)) + rawTxs = append(rawTxs, signLegacyTxWithGasPrice(t, key, chainID, 0, &recipient, big.NewInt(7), nil, 100_000, big.NewInt(1))) + } + + cfg := Config{MinGasPrice: big.NewInt(0)} + req := BlockRequest{Context: blockContext(chainID), Txs: rawTxs} + seqResult, err := NewExecutor(cfg, WithState(seqState)).ExecuteBlock(context.Background(), req) + require.NoError(t, err) + occResult, err := NewExecutor(Config{MinGasPrice: big.NewInt(0), OCCWorkers: 4}, WithState(occState)).ExecuteBlock(context.Background(), req) + require.NoError(t, err) + + require.True(t, occResult.OCCStats.Attempted) + require.False(t, occResult.OCCStats.Fallback) + require.Equal(t, seqResult.GasUsed, occResult.GasUsed) + + seqState.ApplyChangeSet(seqResult.ChangeSet) + occState.ApplyChangeSet(occResult.ChangeSet) + require.Equal(t, seqState.GetBalance(req.Context.Coinbase), occState.GetBalance(req.Context.Coinbase)) + for i := range txCount { + require.Equal(t, seqState.GetBalance(senders[i]), occState.GetBalance(senders[i])) + require.Equal(t, seqState.GetBalance(recipients[i]), occState.GetBalance(recipients[i])) + } +} + +func TestExecutorOCCSpeculativeErrorFallsBackToSequential(t *testing.T) { + chainID := big.NewInt(713715) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + firstRecipient := testAddress(0xb6) + secondRecipient := testAddress(0xb7) + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000)) + firstTx := signLegacyTxWithGasPrice(t, key, chainID, 0, &firstRecipient, big.NewInt(1), nil, 100_000, big.NewInt(0)) + secondTx := signLegacyTxWithGasPrice(t, key, chainID, 1, &secondRecipient, big.NewInt(1), nil, 100_000, big.NewInt(0)) + executor := NewExecutor(Config{MinGasPrice: big.NewInt(0), OCCWorkers: 2}, WithState(state)) + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: blockContext(chainID), + Txs: [][]byte{firstTx, secondTx}, + }) + + require.NoError(t, err) + require.True(t, result.OCCStats.Attempted) + require.True(t, result.OCCStats.Fallback) + require.Equal(t, occFallbackReasonSpeculativeError, result.OCCStats.FallbackReason) + + state.ApplyChangeSet(result.ChangeSet) + require.Equal(t, uint64(2), state.GetNonce(sender)) + require.Equal(t, big.NewInt(1), state.GetBalance(firstRecipient)) + require.Equal(t, big.NewInt(1), state.GetBalance(secondRecipient)) +} + func TestExecutorOCCFallsBackWhenDeclaredGasExceedsBlockLimit(t *testing.T) { chainID := big.NewInt(713715) rawTxs := make([][]byte, 0, 2) @@ -1071,6 +1158,40 @@ func signDynamicFeeTxWithFees(t *testing.T, key *ecdsa.PrivateKey, chainID *big. return raw } +func signBlobTxWithFees( + t *testing.T, + key *ecdsa.PrivateKey, + chainID *big.Int, + nonce uint64, + to common.Address, + value *big.Int, + data []byte, + gasTipCap *big.Int, + gasFeeCap *big.Int, + blobFeeCap *big.Int, + gas uint64, + blobHashes []common.Hash, +) []byte { + t.Helper() + tx := ethtypes.NewTx(ðtypes.BlobTx{ + ChainID: uint256.MustFromBig(chainID), + Nonce: nonce, + GasTipCap: uint256.MustFromBig(gasTipCap), + GasFeeCap: uint256.MustFromBig(gasFeeCap), + Gas: gas, + To: to, + Value: uint256.MustFromBig(value), + Data: data, + BlobFeeCap: uint256.MustFromBig(blobFeeCap), + BlobHashes: append([]common.Hash(nil), blobHashes...), + }) + signed, err := ethtypes.SignTx(tx, ethtypes.LatestSignerForChainID(chainID), key) + require.NoError(t, err) + raw, err := signed.MarshalBinary() + require.NoError(t, err) + return raw +} + func blockContext(chainID *big.Int) BlockContext { return BlockContext{ Number: 1, diff --git a/giga/evmonly/occ.go b/giga/evmonly/occ.go index c7bd4a0d75..a2d67983bb 100644 --- a/giga/evmonly/occ.go +++ b/giga/evmonly/occ.go @@ -16,13 +16,14 @@ import ( ) type occTxExecution struct { - txResult TxResult - receipt *ethtypes.Receipt - changeSet StateChangeSet - readSet map[stateAccessKey]struct{} - writeSet map[stateAccessKey]struct{} - gasUsed uint64 - gasLimit uint64 + txResult TxResult + receipt *ethtypes.Receipt + changeSet StateChangeSet + readSet map[stateAccessKey]struct{} + writeSet map[stateAccessKey]struct{} + gasUsed uint64 + gasLimit uint64 + commutativeBalanceDeltas map[common.Address]*big.Int } type occTxRange struct { @@ -61,7 +62,19 @@ func (e *Executor) executeBlockOCC(ctx context.Context, req PreparedBlock) (*Blo } return nil }); err != nil { - return nil, err + if ctxErr := ctx.Err(); ctxErr != nil { + return nil, ctxErr + } + result, seqErr := e.executeBlockSequential(ctx, req) + if seqErr != nil { + return nil, seqErr + } + result.OCCStats = OCCStats{ + Attempted: true, + Fallback: true, + FallbackReason: occFallbackReasonSpeculativeError, + } + return result, nil } validation := validateOCCResults(results, gasLimit) if !validation.valid { @@ -142,13 +155,14 @@ func (e *Executor) executeTxSpeculative( var changeSet StateChangeSet stateDB.ChangeSetInto(&changeSet) return occTxExecution{ - txResult: txResult, - receipt: receipt, - changeSet: changeSet, - readSet: readSet, - writeSet: writeSet, - gasUsed: txResult.GasUsed, - gasLimit: p.Tx.Gas(), + txResult: txResult, + receipt: receipt, + changeSet: changeSet, + readSet: readSet, + writeSet: writeSet, + gasUsed: txResult.GasUsed, + gasLimit: p.Tx.Gas(), + commutativeBalanceDeltas: stateDB.commutativeBalanceDeltasBig(), }, nil } @@ -167,9 +181,10 @@ type occConflictAggregationKey struct { } const ( - occFallbackReasonConflict = "conflict" - occFallbackReasonGasLimit = "gas_limit" - occFallbackReasonGasOverflow = "gas_overflow" + occFallbackReasonConflict = "conflict" + occFallbackReasonGasLimit = "gas_limit" + occFallbackReasonGasOverflow = "gas_overflow" + occFallbackReasonSpeculativeError = "speculative_error" ) func validateOCCResults(results []occTxExecution, gasLimit uint64) occValidationResult { @@ -351,6 +366,8 @@ type storageChangeKey struct { func mergeChangeSetsInto(results []occTxExecution, merged *StateChangeSet) { merged.resetForReuse() balances := map[common.Address]*big.Int{} + balanceBases := map[common.Address]*big.Int{} + balanceDeltas := map[common.Address]*big.Int{} nonces := map[common.Address]uint64{} code := map[common.Address]CodeChange{} storageClears := map[common.Address]struct{}{} @@ -358,7 +375,22 @@ func mergeChangeSetsInto(results []occTxExecution, merged *StateChangeSet) { for _, result := range results { for _, change := range result.changeSet.Balances { - balances[change.Address] = cloneBig(change.Balance) + delta := result.commutativeBalanceDeltas[change.Address] + if delta == nil { + balances[change.Address] = cloneBig(change.Balance) + continue + } + base := new(big.Int).Sub(cloneBig(change.Balance), delta) + balanceBases[change.Address] = base + if _, normalWrite := result.writeSet[stateAccessKey{kind: stateAccessBalance, address: change.Address}]; normalWrite { + balances[change.Address] = cloneBig(base) + } + } + for addr, delta := range result.commutativeBalanceDeltas { + if balanceDeltas[addr] == nil { + balanceDeltas[addr] = new(big.Int) + } + balanceDeltas[addr].Add(balanceDeltas[addr], delta) } for _, change := range result.changeSet.Nonces { nonces[change.Address] = change.Nonce @@ -382,6 +414,18 @@ func mergeChangeSetsInto(results []occTxExecution, merged *StateChangeSet) { storage[storageChangeKey{address: change.Address, key: change.Key}] = change } } + for addr, delta := range balanceDeltas { + base := balances[addr] + if base == nil { + base = balanceBases[addr] + } + if base == nil { + base = new(big.Int) + } else { + base = cloneBig(base) + } + balances[addr] = base.Add(base, delta) + } balanceAddrs := sortedAddressesFromBigMap(balances) for _, addr := range balanceAddrs { diff --git a/giga/evmonly/state_db.go b/giga/evmonly/state_db.go index 2a0d39f7dc..674f818ea9 100644 --- a/giga/evmonly/state_db.go +++ b/giga/evmonly/state_db.go @@ -29,13 +29,14 @@ type nativeStateDB struct { logs []*ethtypes.Log preimages map[common.Hash][]byte - accessList accessList - transientStates map[common.Address]map[common.Hash]common.Hash - finaliseAddrs map[common.Address]struct{} - journal []nativeJournalEntry - snapshots []nativeSnapshot - readSet map[stateAccessKey]struct{} - writeSet map[stateAccessKey]struct{} + accessList accessList + transientStates map[common.Address]map[common.Hash]common.Hash + finaliseAddrs map[common.Address]struct{} + commutativeBalanceDeltas map[common.Address]*uint256.Int + journal []nativeJournalEntry + snapshots []nativeSnapshot + readSet map[stateAccessKey]struct{} + writeSet map[stateAccessKey]struct{} txHash common.Hash txIndex int @@ -59,15 +60,16 @@ type storageValue struct { } type nativeSnapshot struct { - journalLen int - refund uint64 - logsLen int - accessList accessList - transientStates map[common.Address]map[common.Hash]common.Hash - finaliseAddrs map[common.Address]struct{} - preimages map[common.Hash][]byte - journaledAddrs map[common.Address]struct{} - err error + journalLen int + refund uint64 + logsLen int + accessList accessList + transientStates map[common.Address]map[common.Hash]common.Hash + finaliseAddrs map[common.Address]struct{} + commutativeBalanceDeltas map[common.Address]*uint256.Int + preimages map[common.Hash][]byte + journaledAddrs map[common.Address]struct{} + err error } type nativeJournalKind uint8 @@ -108,13 +110,14 @@ func newNativeStateDB(source StateReader) *nativeStateDB { source = NewMemoryState() } return &nativeStateDB{ - source: source, - accounts: map[common.Address]*nativeAccount{}, - base: map[common.Address]*nativeAccount{}, - preimages: map[common.Hash][]byte{}, - accessList: newAccessList(), - transientStates: map[common.Address]map[common.Hash]common.Hash{}, - finaliseAddrs: map[common.Address]struct{}{}, + source: source, + accounts: map[common.Address]*nativeAccount{}, + base: map[common.Address]*nativeAccount{}, + preimages: map[common.Hash][]byte{}, + accessList: newAccessList(), + transientStates: map[common.Address]map[common.Hash]common.Hash{}, + finaliseAddrs: map[common.Address]struct{}{}, + commutativeBalanceDeltas: map[common.Address]*uint256.Int{}, } } @@ -217,7 +220,10 @@ func (s *nativeStateDB) SubBalance(addr common.Address, amount *uint256.Int, _ t return prev } -func (s *nativeStateDB) AddBalance(addr common.Address, amount *uint256.Int, _ tracing.BalanceChangeReason) uint256.Int { +func (s *nativeStateDB) AddBalance(addr common.Address, amount *uint256.Int, reason tracing.BalanceChangeReason) uint256.Int { + if reason == tracing.BalanceIncreaseRewardTransactionFee { + return s.addCommutativeBalance(addr, amount) + } prev := *s.GetBalance(addr) if amount == nil || amount.IsZero() { return prev @@ -229,6 +235,37 @@ func (s *nativeStateDB) AddBalance(addr common.Address, amount *uint256.Int, _ t return prev } +func (s *nativeStateDB) addCommutativeBalance(addr common.Address, amount *uint256.Int) uint256.Int { + acct := s.account(addr) + prev := *acct.Balance.Clone() + if amount == nil || amount.IsZero() { + return prev + } + s.recordAccount(addr) + acct.Balance.Add(acct.Balance, amount) + if s.commutativeBalanceDeltas == nil { + s.commutativeBalanceDeltas = map[common.Address]*uint256.Int{} + } + delta, ok := s.commutativeBalanceDeltas[addr] + if !ok { + delta = uint256.NewInt(0) + s.commutativeBalanceDeltas[addr] = delta + } + delta.Add(delta, amount) + return prev +} + +func (s *nativeStateDB) commutativeBalanceDeltasBig() map[common.Address]*big.Int { + if len(s.commutativeBalanceDeltas) == 0 { + return nil + } + deltas := make(map[common.Address]*big.Int, len(s.commutativeBalanceDeltas)) + for addr, delta := range s.commutativeBalanceDeltas { + deltas[addr] = delta.ToBig() + } + return deltas +} + func (s *nativeStateDB) GetBalance(addr common.Address) *uint256.Int { s.markRead(stateAccessKey{kind: stateAccessBalance, address: addr}) return s.account(addr).Balance.Clone() @@ -452,15 +489,16 @@ func (s *nativeStateDB) PointCache() *ethutils.PointCache { func (s *nativeStateDB) Snapshot() int { id := len(s.snapshots) s.snapshots = append(s.snapshots, nativeSnapshot{ - journalLen: len(s.journal), - refund: s.refund, - logsLen: len(s.logs), - accessList: cloneAccessList(s.accessList), - transientStates: cloneTransientStates(s.transientStates), - finaliseAddrs: cloneAddressSet(s.finaliseAddrs), - preimages: clonePreimages(s.preimages), - journaledAddrs: map[common.Address]struct{}{}, - err: s.err, + journalLen: len(s.journal), + refund: s.refund, + logsLen: len(s.logs), + accessList: cloneAccessList(s.accessList), + transientStates: cloneTransientStates(s.transientStates), + finaliseAddrs: cloneAddressSet(s.finaliseAddrs), + commutativeBalanceDeltas: cloneUint256Map(s.commutativeBalanceDeltas), + preimages: clonePreimages(s.preimages), + journaledAddrs: map[common.Address]struct{}{}, + err: s.err, }) return id } @@ -479,6 +517,7 @@ func (s *nativeStateDB) RevertToSnapshot(id int) { s.accessList = cloneAccessList(snapshot.accessList) s.transientStates = cloneTransientStates(snapshot.transientStates) s.finaliseAddrs = cloneAddressSet(snapshot.finaliseAddrs) + s.commutativeBalanceDeltas = cloneUint256Map(snapshot.commutativeBalanceDeltas) s.preimages = clonePreimages(snapshot.preimages) s.err = snapshot.err s.snapshots = s.snapshots[:id] @@ -544,24 +583,25 @@ func (s *nativeStateDB) setTxContext(hash common.Hash, index int, indexUint uint func (s *nativeStateDB) Copy() vm.StateDB { cp := &nativeStateDB{ - source: s.source, - accounts: cloneAccounts(s.accounts), - base: cloneAccounts(s.base), - refund: s.refund, - logs: append([]*ethtypes.Log(nil), s.logs...), - preimages: clonePreimages(s.preimages), - accessList: cloneAccessList(s.accessList), - transientStates: cloneTransientStates(s.transientStates), - finaliseAddrs: cloneAddressSet(s.finaliseAddrs), - journal: cloneJournal(s.journal), - snapshots: cloneSnapshots(s.snapshots), - readSet: cloneAccessSet(s.readSet), - writeSet: cloneAccessSet(s.writeSet), - txHash: s.txHash, - txIndex: s.txIndex, - txIndexUint: s.txIndexUint, - err: s.err, - evm: s.evm, + source: s.source, + accounts: cloneAccounts(s.accounts), + base: cloneAccounts(s.base), + refund: s.refund, + logs: append([]*ethtypes.Log(nil), s.logs...), + preimages: clonePreimages(s.preimages), + accessList: cloneAccessList(s.accessList), + transientStates: cloneTransientStates(s.transientStates), + finaliseAddrs: cloneAddressSet(s.finaliseAddrs), + commutativeBalanceDeltas: cloneUint256Map(s.commutativeBalanceDeltas), + journal: cloneJournal(s.journal), + snapshots: cloneSnapshots(s.snapshots), + readSet: cloneAccessSet(s.readSet), + writeSet: cloneAccessSet(s.writeSet), + txHash: s.txHash, + txIndex: s.txIndex, + txIndexUint: s.txIndexUint, + err: s.err, + evm: s.evm, } return cp } @@ -669,6 +709,7 @@ func (s *nativeStateDB) reset(source StateReader) { s.accessList.reset() clearNestedHashMaps(s.transientStates) clear(s.finaliseAddrs) + clear(s.commutativeBalanceDeltas) clear(s.journal) s.journal = s.journal[:0] clear(s.snapshots) @@ -819,6 +860,21 @@ func cloneAddressSet(addrs map[common.Address]struct{}) map[common.Address]struc return cp } +func cloneUint256Map(values map[common.Address]*uint256.Int) map[common.Address]*uint256.Int { + if values == nil { + return nil + } + cp := make(map[common.Address]*uint256.Int, len(values)) + for addr, value := range values { + if value == nil { + cp[addr] = uint256.NewInt(0) + } else { + cp[addr] = value.Clone() + } + } + return cp +} + func cloneAccessSet(set map[stateAccessKey]struct{}) map[stateAccessKey]struct{} { if set == nil { return nil diff --git a/giga/evmonly/types.go b/giga/evmonly/types.go index f68d1e254a..2d05001fce 100644 --- a/giga/evmonly/types.go +++ b/giga/evmonly/types.go @@ -22,18 +22,13 @@ type PreparedBlockExecutor interface { ExecutePreparedBlock(context.Context, PreparedBlock) (*BlockResult, error) } -// ResultSink persists executor-produced block outputs. -type ResultSink interface { - StoreChangeSet(ctx context.Context, height uint64, changeSet StateChangeSet) error - StoreReceipts(ctx context.Context, height uint64, receipts ethtypes.Receipts) error -} - -// BlockResultSink can retain a complete BlockResult without forcing the -// executor to copy changesets or receipts before handing them to an async sink. +// ResultSink persists executor-produced block outputs. The sink can retain the +// complete BlockResult without forcing the executor to copy changesets or +// receipts before handing them to an async sink. // The sink must invoke release exactly once after it no longer references // result. If StoreBlockResult returns an error, the executor releases that sink // reference. -type BlockResultSink interface { +type ResultSink interface { StoreBlockResult(ctx context.Context, height uint64, result *BlockResult, release func()) error } From 904bd9a555e980ff4a0b4d349d557f775205386c Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Fri, 3 Jul 2026 12:10:27 +0800 Subject: [PATCH 24/29] fix evmonly occ review edge cases --- giga/evmonly/executor_test.go | 163 ++++++++++++++++++++++++++++++ giga/evmonly/occ.go | 35 +++++-- giga/evmonly/state_db.go | 182 ++++++++++++++++++++++++++++++---- 3 files changed, 352 insertions(+), 28 deletions(-) diff --git a/giga/evmonly/executor_test.go b/giga/evmonly/executor_test.go index 910982754f..020891d28d 100644 --- a/giga/evmonly/executor_test.go +++ b/giga/evmonly/executor_test.go @@ -358,6 +358,89 @@ func TestExecutorOCCFeePayingTransfersDoNotConflictOnCoinbase(t *testing.T) { } } +func TestExecutorOCCFallsBackWhenLaterTxReadsFeeCreditedCoinbase(t *testing.T) { + chainID := big.NewInt(713715) + feePayerKey, err := crypto.GenerateKey() + require.NoError(t, err) + readerKey, err := crypto.GenerateKey() + require.NoError(t, err) + feePayer := crypto.PubkeyToAddress(feePayerKey.PublicKey) + reader := crypto.PubkeyToAddress(readerKey.PublicKey) + recipient := testAddress(0xbe) + contract := testAddress(0xcf) + storageKey := testHash(0x0b) + + seqState := NewMemoryState() + occState := NewMemoryState() + for _, state := range []*MemoryState{seqState, occState} { + state.SetBalance(feePayer, big.NewInt(1_000_000_000)) + state.SetBalance(reader, big.NewInt(1_000_000_000)) + state.SetCode(contract, balanceStoreCode(blockContext(chainID).Coinbase, storageKey)) + } + + feeTx := signLegacyTxWithGasPrice(t, feePayerKey, chainID, 0, &recipient, big.NewInt(1), nil, 100_000, big.NewInt(1)) + readCoinbaseTx := signLegacyTxWithGasPrice(t, readerKey, chainID, 0, &contract, big.NewInt(0), nil, 100_000, big.NewInt(0)) + req := BlockRequest{Context: blockContext(chainID), Txs: [][]byte{feeTx, readCoinbaseTx}} + seqResult, err := NewExecutor(Config{MinGasPrice: big.NewInt(0)}, WithState(seqState)).ExecuteBlock(context.Background(), req) + require.NoError(t, err) + occResult, err := NewExecutor(Config{MinGasPrice: big.NewInt(0), OCCWorkers: 2}, WithState(occState)).ExecuteBlock(context.Background(), req) + require.NoError(t, err) + + require.True(t, occResult.OCCStats.Attempted) + require.True(t, occResult.OCCStats.Fallback) + require.Equal(t, occFallbackReasonConflict, occResult.OCCStats.FallbackReason) + foundCoinbaseBalanceRead := false + for _, conflict := range occResult.OCCStats.ConflictSamples { + if conflict.Access == "read" && conflict.Kind == "balance" && conflict.Address == req.Context.Coinbase { + foundCoinbaseBalanceRead = true + } + } + require.True(t, foundCoinbaseBalanceRead) + + seqState.ApplyChangeSet(seqResult.ChangeSet) + occState.ApplyChangeSet(occResult.ChangeSet) + require.Equal(t, seqState.GetState(contract, storageKey), occState.GetState(contract, storageKey)) + require.Equal(t, common.BigToHash(big.NewInt(21_000)), occState.GetState(contract, storageKey)) +} + +func TestExecutorOCCMergesCoinbaseSenderFeeWithoutDoubleCount(t *testing.T) { + chainID := big.NewInt(713715) + coinbaseKey, err := crypto.GenerateKey() + require.NoError(t, err) + otherKey, err := crypto.GenerateKey() + require.NoError(t, err) + coinbase := crypto.PubkeyToAddress(coinbaseKey.PublicKey) + other := crypto.PubkeyToAddress(otherKey.PublicKey) + coinbaseRecipient := testAddress(0xba) + otherRecipient := testAddress(0xbb) + initialCoinbaseBalance := big.NewInt(1_000_000_000) + + seqState := NewMemoryState() + occState := NewMemoryState() + for _, state := range []*MemoryState{seqState, occState} { + state.SetBalance(coinbase, initialCoinbaseBalance) + state.SetBalance(other, big.NewInt(1_000_000_000)) + } + + coinbaseTx := signLegacyTxWithGasPrice(t, coinbaseKey, chainID, 0, &coinbaseRecipient, big.NewInt(0), nil, 100_000, big.NewInt(1)) + otherTx := signLegacyTxWithGasPrice(t, otherKey, chainID, 0, &otherRecipient, big.NewInt(0), nil, 100_000, big.NewInt(1)) + ctx := blockContext(chainID) + ctx.Coinbase = coinbase + req := BlockRequest{Context: ctx, Txs: [][]byte{coinbaseTx, otherTx}} + seqResult, err := NewExecutor(Config{MinGasPrice: big.NewInt(0)}, WithState(seqState)).ExecuteBlock(context.Background(), req) + require.NoError(t, err) + occResult, err := NewExecutor(Config{MinGasPrice: big.NewInt(0), OCCWorkers: 2}, WithState(occState)).ExecuteBlock(context.Background(), req) + require.NoError(t, err) + + require.True(t, occResult.OCCStats.Attempted) + require.False(t, occResult.OCCStats.Fallback) + + seqState.ApplyChangeSet(seqResult.ChangeSet) + occState.ApplyChangeSet(occResult.ChangeSet) + require.Equal(t, seqState.GetBalance(coinbase), occState.GetBalance(coinbase)) + require.Equal(t, new(big.Int).Add(initialCoinbaseBalance, big.NewInt(21_000)), occState.GetBalance(coinbase)) +} + func TestExecutorOCCSpeculativeErrorFallsBackToSequential(t *testing.T) { chainID := big.NewInt(713715) key, err := crypto.GenerateKey() @@ -904,6 +987,27 @@ func TestStateDBSelfDestructEmitsStorageClear(t *testing.T) { require.Equal(t, common.Hash{}, state.GetState(contract, unreadKey)) } +func TestStateDBCreateAccountPreservesStorageClear(t *testing.T) { + contract := testAddress(0xc6) + unreadKey := testHash(0x02) + unreadValue := testHash(0x22) + + state := NewMemoryState() + state.SetCode(contract, []byte{0x60, 0x00, 0x00}) + state.SetState(contract, unreadKey, unreadValue) + stateDB := newNativeStateDB(state) + + stateDB.SelfDestruct(contract) + stateDB.Finalise(true) + stateDB.CreateAccount(contract) + stateDB.Finalise(true) + + changes := stateDB.ChangeSet() + require.Contains(t, changes.StorageClears, contract) + state.ApplyChangeSet(changes) + require.Equal(t, common.Hash{}, state.GetState(contract, unreadKey)) +} + func TestStateDBStorageClearThenSameValueWriteIsEmitted(t *testing.T) { contract := testAddress(0xc5) key := testHash(0x01) @@ -929,6 +1033,43 @@ func TestStateDBStorageClearThenSameValueWriteIsEmitted(t *testing.T) { require.Equal(t, value, state.GetState(contract, key)) } +func TestStateDBGetCommittedStateAdvancesAtFinalise(t *testing.T) { + addr := testAddress(0xc7) + key := testHash(0x01) + first := testHash(0x11) + second := testHash(0x22) + third := testHash(0x33) + + state := NewMemoryState() + state.SetState(addr, key, first) + stateDB := newNativeStateDB(state) + + require.Equal(t, first, stateDB.GetCommittedState(addr, key)) + require.Equal(t, first, stateDB.SetState(addr, key, second)) + require.Equal(t, first, stateDB.GetCommittedState(addr, key)) + stateDB.Finalise(true) + require.Equal(t, second, stateDB.GetCommittedState(addr, key)) + require.Equal(t, second, stateDB.SetState(addr, key, third)) + require.Equal(t, second, stateDB.GetCommittedState(addr, key)) +} + +func TestStateDBGetStateAfterStorageClearDoesNotReloadPersistedSlot(t *testing.T) { + contract := testAddress(0xc8) + key := testHash(0x01) + value := testHash(0x22) + + state := NewMemoryState() + state.SetState(contract, key, value) + stateDB := newNativeStateDB(state) + + require.Equal(t, value, stateDB.GetState(contract, key)) + stateDB.SelfDestruct(contract) + stateDB.Finalise(true) + + require.Equal(t, common.Hash{}, stateDB.GetCommittedState(contract, key)) + require.Equal(t, common.Hash{}, stateDB.GetState(contract, key)) +} + func TestPrepareClearsTransientStorage(t *testing.T) { stateDB := newNativeStateDB(NewMemoryState()) addr := common.HexToAddress("0x00000000000000000000000000000000000000a3") @@ -960,6 +1101,21 @@ func TestSnapshotRevertRestoresBaseState(t *testing.T) { require.Empty(t, stateDB.ChangeSet().Storage) } +func TestStateDBCopyPreservesSnapshotCommutativeBalanceDeltas(t *testing.T) { + coinbase := testAddress(0xc9) + stateDB := newNativeStateDB(NewMemoryState()) + stateDB.addCommutativeBalance(coinbase, uint256.NewInt(5)) + snapshot := stateDB.Snapshot() + stateDB.addCommutativeBalance(coinbase, uint256.NewInt(7)) + + copied := stateDB.Copy().(*nativeStateDB) + copied.RevertToSnapshot(snapshot) + + require.Equal(t, map[common.Address]*big.Int{ + coinbase: big.NewInt(5), + }, copied.commutativeBalanceDeltasBig()) +} + func TestStateDBFirstStorageReadPreservesBase(t *testing.T) { addr := testAddress(0xa9) key := testHash(0x01) @@ -1237,6 +1393,13 @@ func storeCode(key, value common.Hash) []byte { return append(code, 0x55, 0x00) } +func balanceStoreCode(addr common.Address, key common.Hash) []byte { + code := append([]byte{0x73}, addr.Bytes()...) + code = append(code, 0x31, 0x7f) + code = append(code, key.Bytes()...) + return append(code, 0x55, 0x00) +} + func initCode(runtime []byte) []byte { if len(runtime) > 255 { panic("test runtime too large") diff --git a/giga/evmonly/occ.go b/giga/evmonly/occ.go index a2d67983bb..1dbc7f769f 100644 --- a/giga/evmonly/occ.go +++ b/giga/evmonly/occ.go @@ -213,6 +213,7 @@ func validateOCCResults(results []occTxExecution, gasLimit uint64) occValidation validation.addConflicts("read", writes, result.readSet) validation.addConflicts("write", writes, result.writeSet) writes.addAll(result.writeSet) + writes.addCommutativeBalanceDeltas(result.commutativeBalanceDeltas) } if validation.conflictCount > 0 { validation.valid = false @@ -318,16 +319,18 @@ func (e *Executor) mergeOCCResults(ctx context.Context, results []occTxExecution } type stateAccessIndex struct { - exact map[stateAccessKey]struct{} - account map[common.Address]struct{} - touched map[common.Address]struct{} + exact map[stateAccessKey]struct{} + account map[common.Address]struct{} + touched map[common.Address]struct{} + commutativeBalance map[common.Address]struct{} } func newStateAccessIndex() *stateAccessIndex { return &stateAccessIndex{ - exact: map[stateAccessKey]struct{}{}, - account: map[common.Address]struct{}{}, - touched: map[common.Address]struct{}{}, + exact: map[stateAccessKey]struct{}{}, + account: map[common.Address]struct{}{}, + touched: map[common.Address]struct{}{}, + commutativeBalance: map[common.Address]struct{}{}, } } @@ -339,10 +342,15 @@ func (i *stateAccessIndex) conflictsWith(key stateAccessKey) bool { return true } if key.kind == stateAccessAccount { - _, ok := i.touched[key.address] - return ok + if _, ok := i.touched[key.address]; ok { + return true + } + } + if key.kind == stateAccessStorage { + return false } - return false + _, ok := i.commutativeBalance[key.address] + return ok } func (i *stateAccessIndex) addAll(set map[stateAccessKey]struct{}) { @@ -358,6 +366,15 @@ func (i *stateAccessIndex) addAll(set map[stateAccessKey]struct{}) { } } +func (i *stateAccessIndex) addCommutativeBalanceDeltas(deltas map[common.Address]*big.Int) { + for addr, delta := range deltas { + if delta == nil || delta.Sign() == 0 { + continue + } + i.commutativeBalance[addr] = struct{}{} + } +} + type storageChangeKey struct { address common.Address key common.Hash diff --git a/giga/evmonly/state_db.go b/giga/evmonly/state_db.go index 674f818ea9..7166ba41ef 100644 --- a/giga/evmonly/state_db.go +++ b/giga/evmonly/state_db.go @@ -32,6 +32,9 @@ type nativeStateDB struct { accessList accessList transientStates map[common.Address]map[common.Hash]common.Hash finaliseAddrs map[common.Address]struct{} + committedStorage map[common.Address]map[common.Hash]storageValue + txStorageWrites map[common.Address]map[common.Hash]struct{} + txStorageClears map[common.Address]struct{} commutativeBalanceDeltas map[common.Address]*uint256.Int journal []nativeJournalEntry snapshots []nativeSnapshot @@ -66,6 +69,8 @@ type nativeSnapshot struct { accessList accessList transientStates map[common.Address]map[common.Hash]common.Hash finaliseAddrs map[common.Address]struct{} + txStorageWrites map[common.Address]map[common.Hash]struct{} + txStorageClears map[common.Address]struct{} commutativeBalanceDeltas map[common.Address]*uint256.Int preimages map[common.Hash][]byte journaledAddrs map[common.Address]struct{} @@ -117,6 +122,9 @@ func newNativeStateDB(source StateReader) *nativeStateDB { accessList: newAccessList(), transientStates: map[common.Address]map[common.Hash]common.Hash{}, finaliseAddrs: map[common.Address]struct{}{}, + committedStorage: map[common.Address]map[common.Hash]storageValue{}, + txStorageWrites: map[common.Address]map[common.Hash]struct{}{}, + txStorageClears: map[common.Address]struct{}{}, commutativeBalanceDeltas: map[common.Address]*uint256.Int{}, } } @@ -141,7 +149,8 @@ func (s *nativeStateDB) ChangeSetInto(changes *StateChangeSet) { acct := s.accounts[addr] base := s.baseAccount(addr) - if !acct.Balance.Eq(base.Balance) { + _, hasCommutativeDelta := s.commutativeBalanceDeltas[addr] + if !acct.Balance.Eq(base.Balance) || hasCommutativeDelta { changes.Balances = append(changes.Balances, BalanceChange{ Address: addr, Balance: acct.Balance.ToBig(), @@ -188,10 +197,14 @@ func (s *nativeStateDB) CreateAccount(addr common.Address) { s.recordAccount(addr) s.markWrite(stateAccessKey{kind: stateAccessAccount, address: addr}) balance := acct.Balance.Clone() + storageCleared := acct.StorageCleared + selfDestructed := acct.SelfDestructed *acct = nativeAccount{ - Balance: balance, - Storage: map[common.Hash]storageValue{}, - Created: true, + Balance: balance, + Storage: map[common.Hash]storageValue{}, + StorageCleared: storageCleared, + SelfDestructed: selfDestructed, + Created: true, } s.markForFinalise(addr) } @@ -342,8 +355,7 @@ func (s *nativeStateDB) GetRefund() uint64 { func (s *nativeStateDB) GetCommittedState(addr common.Address, key common.Hash) common.Hash { s.markRead(stateAccessKey{kind: stateAccessStorage, address: addr, slot: key}) - s.ensureStorage(addr, key) - return storageHash(s.baseAccount(addr).Storage, key) + return s.committedState(addr, key) } func (s *nativeStateDB) GetState(addr common.Address, key common.Hash) common.Hash { @@ -359,6 +371,7 @@ func (s *nativeStateDB) SetState(addr common.Address, key common.Hash, value com s.recordAccount(addr) s.markWrite(stateAccessKey{kind: stateAccessStorage, address: addr, slot: key}) acct.Storage[key] = storageValue{value: value} + s.markTxStorageWrite(addr, key) return prev } @@ -366,9 +379,12 @@ func (s *nativeStateDB) SetStorage(addr common.Address, states map[common.Hash]c acct := s.account(addr) s.recordAccount(addr) s.markWrite(stateAccessKey{kind: stateAccessAccount, address: addr}) + s.markTxStorageClear(addr) acct.Storage = map[common.Hash]storageValue{} + acct.StorageCleared = true for key, value := range states { acct.Storage[key] = storageValue{value: value} + s.markTxStorageWrite(addr, key) } } @@ -495,6 +511,8 @@ func (s *nativeStateDB) Snapshot() int { accessList: cloneAccessList(s.accessList), transientStates: cloneTransientStates(s.transientStates), finaliseAddrs: cloneAddressSet(s.finaliseAddrs), + txStorageWrites: cloneStorageWriteSet(s.txStorageWrites), + txStorageClears: cloneAddressSet(s.txStorageClears), commutativeBalanceDeltas: cloneUint256Map(s.commutativeBalanceDeltas), preimages: clonePreimages(s.preimages), journaledAddrs: map[common.Address]struct{}{}, @@ -517,6 +535,8 @@ func (s *nativeStateDB) RevertToSnapshot(id int) { s.accessList = cloneAccessList(snapshot.accessList) s.transientStates = cloneTransientStates(snapshot.transientStates) s.finaliseAddrs = cloneAddressSet(snapshot.finaliseAddrs) + s.txStorageWrites = cloneStorageWriteSet(snapshot.txStorageWrites) + s.txStorageClears = cloneAddressSet(snapshot.txStorageClears) s.commutativeBalanceDeltas = cloneUint256Map(snapshot.commutativeBalanceDeltas) s.preimages = clonePreimages(snapshot.preimages) s.err = snapshot.err @@ -552,12 +572,14 @@ func (s *nativeStateDB) Finalise(bool) { acct.StorageCleared = true acct.Nonce = 0 acct.SelfDestructed = false + s.markTxStorageClear(addr) } if acct.Created { s.recordAccount(addr) acct.Created = false } } + s.finaliseTxStorage() clear(s.finaliseAddrs) s.refund = 0 } @@ -592,6 +614,9 @@ func (s *nativeStateDB) Copy() vm.StateDB { accessList: cloneAccessList(s.accessList), transientStates: cloneTransientStates(s.transientStates), finaliseAddrs: cloneAddressSet(s.finaliseAddrs), + committedStorage: cloneStorageValueMaps(s.committedStorage), + txStorageWrites: cloneStorageWriteSet(s.txStorageWrites), + txStorageClears: cloneAddressSet(s.txStorageClears), commutativeBalanceDeltas: cloneUint256Map(s.commutativeBalanceDeltas), journal: cloneJournal(s.journal), snapshots: cloneSnapshots(s.snapshots), @@ -709,6 +734,9 @@ func (s *nativeStateDB) reset(source StateReader) { s.accessList.reset() clearNestedHashMaps(s.transientStates) clear(s.finaliseAddrs) + clearNestedStorageValueMaps(s.committedStorage) + clearNestedStorageWriteSets(s.txStorageWrites) + clear(s.txStorageClears) clear(s.commutativeBalanceDeltas) clear(s.journal) s.journal = s.journal[:0] @@ -751,17 +779,94 @@ func (s *nativeStateDB) baseAccount(addr common.Address) *nativeAccount { func (s *nativeStateDB) ensureStorage(addr common.Address, key common.Hash) { base := s.baseAccount(addr) + acct := s.account(addr) + if _, ok := acct.Storage[key]; ok { + return + } + if slots, ok := s.committedStorage[addr]; ok { + if value, committed := slots[key]; committed { + acct.Storage[key] = value + return + } + } + if acct.StorageCleared { + return + } if _, ok := base.Storage[key]; !ok { if value := s.source.GetState(addr, key); value != (common.Hash{}) { base.Storage[key] = storageValue{value: value} } } - acct := s.account(addr) - if _, ok := acct.Storage[key]; !ok { - if value := storageHash(base.Storage, key); value != (common.Hash{}) { - acct.Storage[key] = storageValue{value: value} + if value := storageHash(base.Storage, key); value != (common.Hash{}) { + acct.Storage[key] = storageValue{value: value} + } +} + +func (s *nativeStateDB) committedState(addr common.Address, key common.Hash) common.Hash { + if slots, ok := s.committedStorage[addr]; ok { + if value, committed := slots[key]; committed { + return value.value } } + if acct, ok := s.accounts[addr]; ok && acct.StorageCleared { + return common.Hash{} + } + base := s.baseAccount(addr) + if _, ok := base.Storage[key]; !ok { + if value := s.source.GetState(addr, key); value != (common.Hash{}) { + base.Storage[key] = storageValue{value: value} + } + } + return storageHash(base.Storage, key) +} + +func (s *nativeStateDB) markTxStorageWrite(addr common.Address, key common.Hash) { + if s.txStorageWrites == nil { + s.txStorageWrites = map[common.Address]map[common.Hash]struct{}{} + } + slots, ok := s.txStorageWrites[addr] + if !ok { + slots = map[common.Hash]struct{}{} + s.txStorageWrites[addr] = slots + } + slots[key] = struct{}{} +} + +func (s *nativeStateDB) markTxStorageClear(addr common.Address) { + if s.txStorageClears == nil { + s.txStorageClears = map[common.Address]struct{}{} + } + s.txStorageClears[addr] = struct{}{} +} + +func (s *nativeStateDB) finaliseTxStorage() { + if s.committedStorage == nil { + s.committedStorage = map[common.Address]map[common.Hash]storageValue{} + } + for addr := range s.txStorageClears { + s.committedStorage[addr] = map[common.Hash]storageValue{} + } + for addr, keys := range s.txStorageWrites { + if len(keys) == 0 { + continue + } + slots, ok := s.committedStorage[addr] + if !ok { + slots = map[common.Hash]storageValue{} + s.committedStorage[addr] = slots + } + acct := s.account(addr) + for key := range keys { + value, ok := acct.Storage[key] + if !ok { + delete(slots, key) + continue + } + slots[key] = value + } + } + clearNestedStorageWriteSets(s.txStorageWrites) + clear(s.txStorageClears) } func (s *nativeStateDB) loadAccount(addr common.Address) *nativeAccount { @@ -852,6 +957,28 @@ func cloneTransientStates(states map[common.Address]map[common.Hash]common.Hash) return cp } +func cloneStorageValueMaps(states map[common.Address]map[common.Hash]storageValue) map[common.Address]map[common.Hash]storageValue { + cp := make(map[common.Address]map[common.Hash]storageValue, len(states)) + for addr, slots := range states { + cp[addr] = map[common.Hash]storageValue{} + for key, value := range slots { + cp[addr][key] = value + } + } + return cp +} + +func cloneStorageWriteSet(states map[common.Address]map[common.Hash]struct{}) map[common.Address]map[common.Hash]struct{} { + cp := make(map[common.Address]map[common.Hash]struct{}, len(states)) + for addr, slots := range states { + cp[addr] = map[common.Hash]struct{}{} + for key := range slots { + cp[addr][key] = struct{}{} + } + } + return cp +} + func cloneAddressSet(addrs map[common.Address]struct{}) map[common.Address]struct{} { cp := make(map[common.Address]struct{}, len(addrs)) for addr := range addrs { @@ -907,6 +1034,20 @@ func clearNestedHashMaps(values map[common.Address]map[common.Hash]common.Hash) clear(values) } +func clearNestedStorageValueMaps(values map[common.Address]map[common.Hash]storageValue) { + for _, slots := range values { + clear(slots) + } + clear(values) +} + +func clearNestedStorageWriteSets(values map[common.Address]map[common.Hash]struct{}) { + for _, slots := range values { + clear(slots) + } + clear(values) +} + func cloneJournal(journal []nativeJournalEntry) []nativeJournalEntry { cp := make([]nativeJournalEntry, len(journal)) for i, entry := range journal { @@ -923,15 +1064,18 @@ func cloneSnapshots(snapshots []nativeSnapshot) []nativeSnapshot { cp := make([]nativeSnapshot, len(snapshots)) for i, snapshot := range snapshots { cp[i] = nativeSnapshot{ - journalLen: snapshot.journalLen, - refund: snapshot.refund, - logsLen: snapshot.logsLen, - accessList: cloneAccessList(snapshot.accessList), - transientStates: cloneTransientStates(snapshot.transientStates), - finaliseAddrs: cloneAddressSet(snapshot.finaliseAddrs), - preimages: clonePreimages(snapshot.preimages), - journaledAddrs: cloneAddressSet(snapshot.journaledAddrs), - err: snapshot.err, + journalLen: snapshot.journalLen, + refund: snapshot.refund, + logsLen: snapshot.logsLen, + accessList: cloneAccessList(snapshot.accessList), + transientStates: cloneTransientStates(snapshot.transientStates), + finaliseAddrs: cloneAddressSet(snapshot.finaliseAddrs), + txStorageWrites: cloneStorageWriteSet(snapshot.txStorageWrites), + txStorageClears: cloneAddressSet(snapshot.txStorageClears), + commutativeBalanceDeltas: cloneUint256Map(snapshot.commutativeBalanceDeltas), + preimages: clonePreimages(snapshot.preimages), + journaledAddrs: cloneAddressSet(snapshot.journaledAddrs), + err: snapshot.err, } } return cp From cdf544ac7ade6dc8195830f8120821d498e13b32 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Fri, 3 Jul 2026 12:28:00 +0800 Subject: [PATCH 25/29] track codeless getcodehash account reads --- giga/evmonly/executor_test.go | 29 +++++++++++++++++++++++++++++ giga/evmonly/state_db.go | 2 ++ 2 files changed, 31 insertions(+) diff --git a/giga/evmonly/executor_test.go b/giga/evmonly/executor_test.go index 020891d28d..bba004e6f3 100644 --- a/giga/evmonly/executor_test.go +++ b/giga/evmonly/executor_test.go @@ -1191,6 +1191,35 @@ func TestStateDBGetCodeHashDistinguishesExistingCodelessAccounts(t *testing.T) { require.Equal(t, crypto.Keccak256Hash(code), stateDB.GetCodeHash(contract)) } +func TestStateDBGetCodeHashTracksCodelessAccountExistenceReads(t *testing.T) { + eoa := testAddress(0xbd) + state := NewMemoryState() + state.SetBalance(eoa, big.NewInt(1)) + stateDB := newNativeStateDB(state) + stateDB.enableAccessTracking() + + require.Equal(t, ethtypes.EmptyCodeHash, stateDB.GetCodeHash(eoa)) + readSet, _ := stateDB.accessSets() + require.Contains(t, readSet, stateAccessKey{kind: stateAccessCode, address: eoa}) + require.Contains(t, readSet, stateAccessKey{kind: stateAccessBalance, address: eoa}) + require.Contains(t, readSet, stateAccessKey{kind: stateAccessNonce, address: eoa}) + + validation := validateOCCResults([]occTxExecution{ + { + gasLimit: 1, + writeSet: map[stateAccessKey]struct{}{ + {kind: stateAccessBalance, address: eoa}: {}, + }, + }, + { + gasLimit: 1, + readSet: readSet, + }, + }, 10) + require.False(t, validation.valid) + require.Equal(t, occFallbackReasonConflict, validation.fallbackReason) +} + func TestFinaliseClearsRefund(t *testing.T) { stateDB := newNativeStateDB(NewMemoryState()) stateDB.AddRefund(12) diff --git a/giga/evmonly/state_db.go b/giga/evmonly/state_db.go index 7166ba41ef..78ba7f6c7e 100644 --- a/giga/evmonly/state_db.go +++ b/giga/evmonly/state_db.go @@ -313,6 +313,8 @@ func (s *nativeStateDB) GetCodeHash(addr common.Address) common.Hash { if len(acct.Code) > 0 { return crypto.Keccak256Hash(acct.Code) } + s.markRead(stateAccessKey{kind: stateAccessBalance, address: addr}) + s.markRead(stateAccessKey{kind: stateAccessNonce, address: addr}) if acct.Nonce == 0 && acct.Balance.IsZero() { return common.Hash{} } From ec0ff1a1e9c512ee1c696479dbef7f1e09d6b236 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Fri, 3 Jul 2026 14:06:28 +0800 Subject: [PATCH 26/29] address evmonly nonblocking review issues --- giga/evmonly/README.md | 17 ++++++++++---- giga/evmonly/config.go | 9 +++++--- giga/evmonly/executor.go | 3 +++ giga/evmonly/occ.go | 21 ++++++++++++------ giga/evmonly/state_db.go | 48 ---------------------------------------- giga/evmonly/types.go | 6 +++-- 6 files changed, 40 insertions(+), 64 deletions(-) diff --git a/giga/evmonly/README.md b/giga/evmonly/README.md index ae131631a5..6588ab04d1 100644 --- a/giga/evmonly/README.md +++ b/giga/evmonly/README.md @@ -35,7 +35,10 @@ The `evmonly` package currently provides: - fail-closed custom precompile placeholders The executor accepts config for nonce checks, gas-price checks, minimum gas -price, chain config, and the custom precompile registry. +price, chain config, and the custom precompile registry. A nil chain config +defaults to geth's `params.AllDevChainProtocolChanges`, which is convenient for +tests and scaffold wiring; production callers should provide the chain's +explicit EVM config. ## Executor interface @@ -56,13 +59,17 @@ ExecutePreparedBlock(context.Context, PreparedBlock) (*BlockResult, error) `PrepareBlock` decodes transaction RLP and recovers senders. This work does not touch state, so block N+1 can be prepared while block N is still executing. `ExecuteBlock` remains the convenience path and performs prepare then execute in -one call. +one call. `PreparedBlock` is trusted executor-produced data: callers should pass +the result of `PrepareBlock` unchanged, because `ExecutePreparedBlock` does not +recover senders again. The executor should be commit-neutral. It executes an ordered EVM block and returns the state writes and receipts produced by that block. The caller owns durable persistence, state commitment, block indexing, and receipt publication. The concrete `Executor` accepts a `StateReader` backend through `WithState(...)`; callers can persist the returned `ChangeSet` with a matching `StateWriter`. +Call `Close()` when an executor is no longer used so OCC worker goroutines are +stopped promptly. A non-nil `error` means block validation failed and the caller must not commit a partial output. EVM call failures inside an otherwise valid transaction are @@ -141,10 +148,12 @@ custom precompile addresses return `ErrCustomPrecompilesOpen`. ## Current limitations -- The current port is sequential. The EVM-native state boundary and changeset - shape are intended to be replaceable with the sei-v3 OCC scheduler/store. +- OCC is optional and conservative; conflicting or unsupported blocks fall back + to sequential execution. - State input is key-addressable only. The executor lazily reads storage slots by `(address, slot)` and does not require or expose range iteration. - The map-backed `MemoryState` is for tests and early integration; production should provide a durable native state backend. - Historical `BLOCKHASH` lookups beyond the parent block are not wired yet. +- Block-level blob gas accounting and `MaxBlobGasPerBlock` enforcement are not + wired yet. diff --git a/giga/evmonly/config.go b/giga/evmonly/config.go index 60a902c111..af44ad0492 100644 --- a/giga/evmonly/config.go +++ b/giga/evmonly/config.go @@ -13,9 +13,12 @@ type Config struct { DisableNonceCheck bool DisableGasPriceCheck bool MinGasPrice *big.Int - ChainConfig *params.ChainConfig - CustomPrecompiles precompiles.Registry - OCCWorkers int + // ChainConfig defaults to params.AllDevChainProtocolChanges when nil. Test + // and scaffold callers can use the default, but production wiring should pass + // the chain's explicit config. + ChainConfig *params.ChainConfig + CustomPrecompiles precompiles.Registry + OCCWorkers int // BlockResultPoolSize enables a bounded reusable output pool. Callers that // enable it must call BlockResult.Release when they are done with returned // results. Result sinks receive a retained result and must release it after diff --git a/giga/evmonly/executor.go b/giga/evmonly/executor.go index 577eccc209..68c622880c 100644 --- a/giga/evmonly/executor.go +++ b/giga/evmonly/executor.go @@ -42,6 +42,9 @@ func WithResultSink(sink ResultSink) Option { } } +// NewExecutor constructs an EVM-only executor. Call Close when the executor is +// no longer used; with OCCWorkers > 1, the executor owns worker goroutines that +// otherwise live until the GC finalizer runs. func NewExecutor(cfg Config, opts ...Option) *Executor { e := &Executor{ cfg: cfg.WithDefaults(), diff --git a/giga/evmonly/occ.go b/giga/evmonly/occ.go index 1dbc7f769f..7f04ba2f98 100644 --- a/giga/evmonly/occ.go +++ b/giga/evmonly/occ.go @@ -27,8 +27,9 @@ type occTxExecution struct { } type occTxRange struct { - start int - end int + start int + end int + startUint uint } func (e *Executor) executeBlockOCC(ctx context.Context, req PreparedBlock) (*BlockResult, error) { @@ -53,8 +54,8 @@ func (e *Executor) executeBlockOCC(ctx context.Context, req PreparedBlock) (*Blo defer pool.Close() } if err := pool.Run(ctx, occRanges(txCount, chunkSize), func(workerCtx context.Context, txRange occTxRange) error { - for idx := txRange.start; idx < txRange.end; idx++ { - result, err := e.executeTxSpeculative(workerCtx, req, idx, chainConfig, blockCtx, baseFee, gasLimit) + for idx, idxUint := txRange.start, txRange.startUint; idx < txRange.end; idx, idxUint = idx+1, idxUint+1 { + result, err := e.executeTxSpeculative(workerCtx, req, idx, idxUint, chainConfig, blockCtx, baseFee, gasLimit) if err != nil { return err } @@ -98,12 +99,17 @@ func occRanges(txCount int, chunkSize int) []occTxRange { chunkSize = 1 } ranges := make([]occTxRange, 0, (txCount+chunkSize-1)/chunkSize) - for start := 0; start < txCount; start += chunkSize { + startUint := uint(0) + for start := 0; start < txCount; { end := start + chunkSize if end > txCount { end = txCount } - ranges = append(ranges, occTxRange{start: start, end: end}) + ranges = append(ranges, occTxRange{start: start, end: end, startUint: startUint}) + for start < end { + start++ + startUint++ + } } return ranges } @@ -127,6 +133,7 @@ func (e *Executor) executeTxSpeculative( ctx context.Context, req PreparedBlock, txIndex int, + txIndexUint uint, chainConfig *params.ChainConfig, blockCtx vm.BlockContext, baseFee *big.Int, @@ -145,7 +152,7 @@ func (e *Executor) executeTxSpeculative( req.Context, p, txIndex, - uint(txIndex), + txIndexUint, baseFee, ) if err != nil { diff --git a/giga/evmonly/state_db.go b/giga/evmonly/state_db.go index 78ba7f6c7e..8f5a6134de 100644 --- a/giga/evmonly/state_db.go +++ b/giga/evmonly/state_db.go @@ -722,41 +722,6 @@ func (s *nativeStateDB) clearSnapshots() { s.snapshots = s.snapshots[:0] } -func (s *nativeStateDB) reset(source StateReader) { - if source == nil { - source = NewMemoryState() - } - s.source = source - clear(s.accounts) - clear(s.base) - s.refund = 0 - clear(s.logs) - s.logs = s.logs[:0] - clearBytesMap(s.preimages) - s.accessList.reset() - clearNestedHashMaps(s.transientStates) - clear(s.finaliseAddrs) - clearNestedStorageValueMaps(s.committedStorage) - clearNestedStorageWriteSets(s.txStorageWrites) - clear(s.txStorageClears) - clear(s.commutativeBalanceDeltas) - clear(s.journal) - s.journal = s.journal[:0] - clear(s.snapshots) - s.snapshots = s.snapshots[:0] - if s.readSet != nil { - clear(s.readSet) - } - if s.writeSet != nil { - clear(s.writeSet) - } - s.txHash = common.Hash{} - s.txIndex = 0 - s.txIndexUint = 0 - s.err = nil - s.evm = nil -} - func (s *nativeStateDB) account(addr common.Address) *nativeAccount { if acct, ok := s.accounts[addr]; ok { return acct @@ -1023,12 +988,6 @@ func clonePreimages(preimages map[common.Hash][]byte) map[common.Hash][]byte { return cp } -func clearBytesMap(values map[common.Hash][]byte) { - for key := range values { - delete(values, key) - } -} - func clearNestedHashMaps(values map[common.Address]map[common.Hash]common.Hash) { for _, slots := range values { clear(slots) @@ -1036,13 +995,6 @@ func clearNestedHashMaps(values map[common.Address]map[common.Hash]common.Hash) clear(values) } -func clearNestedStorageValueMaps(values map[common.Address]map[common.Hash]storageValue) { - for _, slots := range values { - clear(slots) - } - clear(values) -} - func clearNestedStorageWriteSets(values map[common.Address]map[common.Hash]struct{}) { for _, slots := range values { clear(slots) diff --git a/giga/evmonly/types.go b/giga/evmonly/types.go index 2d05001fce..b59acfe290 100644 --- a/giga/evmonly/types.go +++ b/giga/evmonly/types.go @@ -39,14 +39,16 @@ type BlockRequest struct { Txs [][]byte } -// PreparedBlock contains decoded transactions with recovered senders. The -// executor treats prepared transactions as immutable. +// PreparedBlock contains decoded transactions with recovered senders. It is a +// trusted executor-produced value: ExecutePreparedBlock assumes callers pass the +// result of PrepareBlock unchanged and does not recover senders again. type PreparedBlock struct { Context BlockContext Txs []PreparedTx } // PreparedTx is the stateless per-transaction work needed before EVM execution. +// Sender is trusted data recovered by PrepareBlock. type PreparedTx struct { Tx *ethtypes.Transaction Sender common.Address From d16e1338325bc74483fcffaa67f84fcfb8f41331 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Fri, 3 Jul 2026 14:42:15 +0800 Subject: [PATCH 27/29] reject evmonly blob txs until accounting is wired --- giga/evmonly/README.md | 2 +- giga/evmonly/executor.go | 11 +++++++++++ giga/evmonly/executor_test.go | 25 +++++++++++++++++++------ giga/evmonly/parser.go | 3 +++ 4 files changed, 34 insertions(+), 7 deletions(-) diff --git a/giga/evmonly/README.md b/giga/evmonly/README.md index 6588ab04d1..650f70d3db 100644 --- a/giga/evmonly/README.md +++ b/giga/evmonly/README.md @@ -156,4 +156,4 @@ custom precompile addresses return `ErrCustomPrecompilesOpen`. should provide a durable native state backend. - Historical `BLOCKHASH` lookups beyond the parent block are not wired yet. - Block-level blob gas accounting and `MaxBlobGasPerBlock` enforcement are not - wired yet. + wired yet, so blob transactions are rejected fail-closed. diff --git a/giga/evmonly/executor.go b/giga/evmonly/executor.go index 68c622880c..bab7ed2eb0 100644 --- a/giga/evmonly/executor.go +++ b/giga/evmonly/executor.go @@ -213,6 +213,9 @@ func (e *Executor) executeTx( baseFee *big.Int, ) (TxResult, *ethtypes.Receipt, error) { tx := p.Tx + if err := validateSupportedTx(tx); err != nil { + return TxResult{Hash: tx.Hash(), Sender: p.Sender, To: tx.To(), Err: err}, nil, err + } if !e.cfg.DisableGasPriceCheck && e.cfg.MinGasPrice != nil { // MinGasPrice is block-validity policy; unlike EVM call failures, it // does not produce a receipt for an otherwise invalid block. @@ -380,6 +383,13 @@ func (e *Executor) chainConfig(ctx BlockContext) *params.ChainConfig { return &cfg } +func validateSupportedTx(tx *ethtypes.Transaction) error { + if tx.Type() == ethtypes.BlobTxType { + return errUnsupportedBlobTx + } + return nil +} + func validateBlockContext(chainConfig *params.ChainConfig, ctx BlockContext) error { if chainConfig != nil && chainConfig.IsLondon(new(big.Int).SetUint64(ctx.Number)) && ctx.BaseFee == nil { return errMissingBaseFee @@ -400,4 +410,5 @@ func effectiveGasPrice(tx *ethtypes.Transaction, baseFee *big.Int) *big.Int { var ( errInsufficientGasPrice = fmt.Errorf("insufficient gas price") errMissingBaseFee = fmt.Errorf("missing base fee for post-London block") + errUnsupportedBlobTx = fmt.Errorf("blob transactions require block-level blob gas accounting") ) diff --git a/giga/evmonly/executor_test.go b/giga/evmonly/executor_test.go index bba004e6f3..d9087d55ed 100644 --- a/giga/evmonly/executor_test.go +++ b/giga/evmonly/executor_test.go @@ -158,7 +158,7 @@ func TestExecutorDynamicFeeTx(t *testing.T) { require.Equal(t, big.NewInt(11), state.GetBalance(recipient)) } -func TestExecutorBlobTxReceiptMetadata(t *testing.T) { +func TestExecutorRejectsBlobTxUntilBlockAccountingIsWired(t *testing.T) { chainID := big.NewInt(713715) key, err := crypto.GenerateKey() require.NoError(t, err) @@ -187,16 +187,29 @@ func TestExecutorBlobTxReceiptMetadata(t *testing.T) { ctx.BaseFee = big.NewInt(2) ctx.BlobBaseFee = blobBaseFee - result, err := NewExecutor(Config{MinGasPrice: big.NewInt(0)}, WithState(state)).ExecuteBlock(context.Background(), BlockRequest{ + executor := NewExecutor(Config{MinGasPrice: big.NewInt(0)}, WithState(state)) + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ Context: ctx, Txs: [][]byte{rawTx}, }) + require.ErrorIs(t, err, errUnsupportedBlobTx) + require.Nil(t, result) + require.Equal(t, big.NewInt(0), state.GetBalance(recipient)) + + tx, sender, err := parseTx(rawTx, ethtypes.LatestSignerForChainID(chainID)) require.NoError(t, err) - require.Len(t, result.Receipts, 1) - require.Equal(t, uint8(ethtypes.BlobTxType), result.Receipts[0].Type) - require.Equal(t, uint64(params.BlobTxBlobGasPerBlob), result.Receipts[0].BlobGasUsed) - require.Equal(t, blobBaseFee, result.Receipts[0].BlobGasPrice) + result, err = executor.ExecutePreparedBlock(context.Background(), PreparedBlock{ + Context: ctx, + Txs: []PreparedTx{{ + Tx: tx, + Sender: sender, + }}, + }) + + require.ErrorIs(t, err, errUnsupportedBlobTx) + require.Nil(t, result) + require.Equal(t, big.NewInt(0), state.GetBalance(recipient)) } func TestExecutorRequiresBaseFeeAfterLondon(t *testing.T) { diff --git a/giga/evmonly/parser.go b/giga/evmonly/parser.go index 99b366fc61..16a6581f5d 100644 --- a/giga/evmonly/parser.go +++ b/giga/evmonly/parser.go @@ -20,6 +20,9 @@ func parseBlockTxs(ctx context.Context, txs [][]byte, signer ethtypes.Signer) ([ if err != nil { return nil, fmt.Errorf("parse tx %d: %w", i, err) } + if err := validateSupportedTx(tx); err != nil { + return nil, fmt.Errorf("parse tx %d: %w", i, err) + } parsed[i] = PreparedTx{Tx: tx, Sender: sender} } return parsed, nil From d555bd480f5c6e7ae6ee55bdbf198ba0d340b771 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Fri, 3 Jul 2026 17:17:41 +0800 Subject: [PATCH 28/29] close rootmulti hash consistency test stores --- sei-cosmos/storev2/rootmulti/hash_consistency_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sei-cosmos/storev2/rootmulti/hash_consistency_test.go b/sei-cosmos/storev2/rootmulti/hash_consistency_test.go index 7ab5b95064..e01485c0f0 100644 --- a/sei-cosmos/storev2/rootmulti/hash_consistency_test.go +++ b/sei-cosmos/storev2/rootmulti/hash_consistency_test.go @@ -17,6 +17,7 @@ func TestCommitAndHistoricalQueryHashConsistency(t *testing.T) { ssConfig := seidbconfig.StateStoreConfig{} store := NewStore(t.TempDir(), scConfig, ssConfig, nil) + defer func() { require.NoError(t, store.Close()) }() keys := []string{"acc", "bank", "distribution", "staking", "ibc", "upgrade"} storeKeys := make(map[string]*types.KVStoreKey) @@ -102,6 +103,7 @@ func TestCommitAndHistoricalQueryWithDoubleFlush(t *testing.T) { ssConfig := seidbconfig.StateStoreConfig{} store := NewStore(t.TempDir(), scConfig, ssConfig, nil) + defer func() { require.NoError(t, store.Close()) }() keys := []string{"acc", "bank", "distribution", "staking", "ibc", "upgrade"} storeKeys := make(map[string]*types.KVStoreKey) From b949ff0448cdc367973634e43a6aa8a32b85cfad Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Mon, 6 Jul 2026 12:48:37 +0800 Subject: [PATCH 29/29] test(evmonly): add executor parity edge cases --- giga/evmonly/executor_parity_test.go | 1279 ++++++++++++++++++++++++++ 1 file changed, 1279 insertions(+) create mode 100644 giga/evmonly/executor_parity_test.go diff --git a/giga/evmonly/executor_parity_test.go b/giga/evmonly/executor_parity_test.go new file mode 100644 index 0000000000..ffc2919397 --- /dev/null +++ b/giga/evmonly/executor_parity_test.go @@ -0,0 +1,1279 @@ +package evmonly + +import ( + "context" + "crypto/ecdsa" + "errors" + "fmt" + "math" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/rawdb" + gethstate "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/tracing" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/triedb" + "github.com/holiman/uint256" + "github.com/stretchr/testify/require" +) + +type gethReferenceResult struct { + state *gethstate.StateDB + txs []TxResult + receipts ethtypes.Receipts + gasUsed uint64 +} + +func TestExecutorNativeTransferFeeAccountingMatchesGeth(t *testing.T) { + chainID := big.NewInt(713715) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + recipient := testAddress(0xd1) + initialBalance := big.NewInt(1_000_000_000) + value := big.NewInt(1234) + baseFee := big.NewInt(7) + tipCap := big.NewInt(3) + feeCap := big.NewInt(20) + + state := NewMemoryState() + state.SetBalance(sender, initialBalance) + + rawTx := signDynamicFeeTxWithFees(t, key, chainID, 0, &recipient, value, nil, tipCap, feeCap, 100_000) + ctx := blockContext(chainID) + ctx.BaseFee = baseFee + ctx.Coinbase = testAddress(0xd2) + cfg := Config{MinGasPrice: big.NewInt(0)} + + gethResult, err := executeGethReferenceBlock(t, state, cfg, ctx, [][]byte{rawTx}) + require.NoError(t, err) + execResult, err := NewExecutor(cfg, WithState(state)).ExecuteBlock(context.Background(), BlockRequest{ + Context: ctx, + Txs: [][]byte{rawTx}, + }) + require.NoError(t, err) + state.ApplyChangeSet(execResult.ChangeSet) + + requireExecutionParity(t, execResult, gethResult) + requireAddressParity(t, state, gethResult.state, sender) + requireAddressParity(t, state, gethResult.state, recipient) + requireAddressParity(t, state, gethResult.state, ctx.Coinbase) + + gasUsed := execResult.Txs[0].GasUsed + effectivePrice := new(big.Int).Add(baseFee, tipCap) + require.Equal(t, effectivePrice, execResult.Txs[0].EffectiveGasPrice) + totalFee := new(big.Int).Mul(new(big.Int).SetUint64(gasUsed), effectivePrice) + expectedSender := new(big.Int).Sub(new(big.Int).Sub(new(big.Int).Set(initialBalance), value), totalFee) + require.Equal(t, expectedSender, state.GetBalance(sender)) + require.Equal(t, value, state.GetBalance(recipient)) + require.Equal(t, totalFee, state.GetBalance(ctx.Coinbase)) +} + +func TestExecutorNativeTransferEdgeCasesMatchGeth(t *testing.T) { + chainID := big.NewInt(713715) + initialBalance := big.NewInt(1_000_000_000_000) + + tests := []struct { + name string + value *big.Int + configureCtx func(*BlockContext, common.Address) + sign func(t *testing.T, key *ecdsa.PrivateKey, ctx BlockContext, recipient common.Address) []byte + wantEffective *big.Int + assert func(t *testing.T, state *MemoryState, sender, recipient, coinbase common.Address, gasUsed uint64) + }{ + { + name: "zero value transfer touches nonce and fees only", + value: big.NewInt(0), + sign: func(t *testing.T, key *ecdsa.PrivateKey, _ BlockContext, recipient common.Address) []byte { + t.Helper() + return signLegacyTxWithGasPrice(t, key, chainID, 0, &recipient, big.NewInt(0), nil, 100_000, big.NewInt(1)) + }, + wantEffective: big.NewInt(1), + assert: func(t *testing.T, state *MemoryState, sender, recipient, coinbase common.Address, gasUsed uint64) { + t.Helper() + fee := new(big.Int).SetUint64(gasUsed) + require.Equal(t, new(big.Int).Sub(initialBalance, fee), state.GetBalance(sender)) + require.Equal(t, big.NewInt(0), state.GetBalance(recipient)) + require.Equal(t, fee, state.GetBalance(coinbase)) + }, + }, + { + name: "dynamic fee effective price is capped by fee cap", + value: big.NewInt(17), + configureCtx: func(ctx *BlockContext, _ common.Address) { + ctx.BaseFee = big.NewInt(7) + }, + sign: func(t *testing.T, key *ecdsa.PrivateKey, _ BlockContext, recipient common.Address) []byte { + t.Helper() + return signDynamicFeeTxWithFees(t, key, chainID, 0, &recipient, big.NewInt(17), nil, big.NewInt(10), big.NewInt(12), 100_000) + }, + wantEffective: big.NewInt(12), + assert: func(t *testing.T, state *MemoryState, sender, recipient, coinbase common.Address, gasUsed uint64) { + t.Helper() + fee := new(big.Int).Mul(new(big.Int).SetUint64(gasUsed), big.NewInt(12)) + require.Equal(t, new(big.Int).Sub(new(big.Int).Sub(initialBalance, big.NewInt(17)), fee), state.GetBalance(sender)) + require.Equal(t, big.NewInt(17), state.GetBalance(recipient)) + require.Equal(t, fee, state.GetBalance(coinbase)) + }, + }, + { + name: "coinbase sender receives its own fee reward", + value: big.NewInt(23), + configureCtx: func(ctx *BlockContext, sender common.Address) { + ctx.Coinbase = sender + }, + sign: func(t *testing.T, key *ecdsa.PrivateKey, _ BlockContext, recipient common.Address) []byte { + t.Helper() + return signLegacyTxWithGasPrice(t, key, chainID, 0, &recipient, big.NewInt(23), nil, 100_000, big.NewInt(5)) + }, + wantEffective: big.NewInt(5), + assert: func(t *testing.T, state *MemoryState, sender, recipient, _ common.Address, _ uint64) { + t.Helper() + require.Equal(t, new(big.Int).Sub(initialBalance, big.NewInt(23)), state.GetBalance(sender)) + require.Equal(t, big.NewInt(23), state.GetBalance(recipient)) + }, + }, + } + + for i, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + recipient := common.BigToAddress(big.NewInt(int64(0xe0 + i))) + state := NewMemoryState() + state.SetBalance(sender, initialBalance) + ctx := blockContext(chainID) + if tc.configureCtx != nil { + tc.configureCtx(&ctx, sender) + } + rawTx := tc.sign(t, key, ctx, recipient) + cfg := Config{MinGasPrice: big.NewInt(0)} + + gethResult, err := executeGethReferenceBlock(t, state, cfg, ctx, [][]byte{rawTx}) + require.NoError(t, err) + execResult, err := NewExecutor(cfg, WithState(state)).ExecuteBlock(context.Background(), BlockRequest{ + Context: ctx, + Txs: [][]byte{rawTx}, + }) + require.NoError(t, err) + state.ApplyChangeSet(execResult.ChangeSet) + + requireExecutionParity(t, execResult, gethResult) + requireAddressParity(t, state, gethResult.state, sender) + requireAddressParity(t, state, gethResult.state, recipient) + requireAddressParity(t, state, gethResult.state, ctx.Coinbase) + require.Equal(t, tc.wantEffective, execResult.Txs[0].EffectiveGasPrice) + require.Equal(t, uint64(1), state.GetNonce(sender)) + tc.assert(t, state, sender, recipient, ctx.Coinbase, execResult.Txs[0].GasUsed) + }) + } +} + +func TestExecutorERC20StyleTransferMatchesGeth(t *testing.T) { + chainID := big.NewInt(713715) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + token := testAddress(0xd3) + from := testAddress(0xd4) + to := testAddress(0xd5) + fromSlot := common.BytesToHash(from.Bytes()) + toSlot := common.BytesToHash(to.Bytes()) + amount := byte(7) + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000_000_000)) + state.SetCode(token, erc20TransferRuntime(fromSlot, toSlot, from, to, amount)) + state.SetState(token, fromSlot, common.BigToHash(big.NewInt(1000))) + + rawTx := signLegacyTxWithGasPrice(t, key, chainID, 0, &token, big.NewInt(0), nil, 200_000, big.NewInt(1)) + cfg := Config{MinGasPrice: big.NewInt(0)} + ctx := blockContext(chainID) + + gethResult, err := executeGethReferenceBlock(t, state, cfg, ctx, [][]byte{rawTx}) + require.NoError(t, err) + execResult, err := NewExecutor(cfg, WithState(state)).ExecuteBlock(context.Background(), BlockRequest{ + Context: ctx, + Txs: [][]byte{rawTx}, + }) + require.NoError(t, err) + state.ApplyChangeSet(execResult.ChangeSet) + + requireExecutionParity(t, execResult, gethResult) + requireAddressParity(t, state, gethResult.state, token, fromSlot, toSlot) + require.Equal(t, common.BigToHash(big.NewInt(993)), state.GetState(token, fromSlot)) + require.Equal(t, common.BigToHash(big.NewInt(7)), state.GetState(token, toSlot)) + + require.Len(t, execResult.Receipts[0].Logs, 1) + log := execResult.Receipts[0].Logs[0] + require.Equal(t, token, log.Address) + require.Equal(t, []common.Hash{ + erc20TransferTopic(), + common.BytesToHash(from.Bytes()), + common.BytesToHash(to.Bytes()), + }, log.Topics) + require.Equal(t, common.LeftPadBytes([]byte{amount}, common.HashLength), log.Data) + require.Equal(t, ethtypes.ReceiptStatusSuccessful, execResult.Receipts[0].Status) +} + +func TestExecutorSelfDestructCreatedInSameTxMatchesGeth(t *testing.T) { + chainID := big.NewInt(713715) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + factory := testAddress(0xd6) + beneficiary := testAddress(0xd7) + child := crypto.CreateAddress(factory, 0) + endowment := byte(17) + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000_000_000)) + state.SetCode(factory, createAndDestroyChildRuntime(beneficiary, endowment)) + + rawTx := signLegacyTxWithGasPrice(t, key, chainID, 0, &factory, big.NewInt(int64(endowment)), nil, 500_000, big.NewInt(1)) + cfg := Config{MinGasPrice: big.NewInt(0)} + ctx := blockContext(chainID) + + gethResult, err := executeGethReferenceBlock(t, state, cfg, ctx, [][]byte{rawTx}) + require.NoError(t, err) + execResult, err := NewExecutor(cfg, WithState(state)).ExecuteBlock(context.Background(), BlockRequest{ + Context: ctx, + Txs: [][]byte{rawTx}, + }) + require.NoError(t, err) + state.ApplyChangeSet(execResult.ChangeSet) + + requireExecutionParity(t, execResult, gethResult) + requireAddressParity(t, state, gethResult.state, factory) + requireAddressParity(t, state, gethResult.state, child) + requireAddressParity(t, state, gethResult.state, beneficiary) + require.Empty(t, state.GetCode(child)) + require.Equal(t, uint64(0), state.GetNonce(child)) + require.Equal(t, big.NewInt(0), state.GetBalance(child)) + require.Equal(t, big.NewInt(int64(endowment)), state.GetBalance(beneficiary)) +} + +func TestExecutorPragueSelfDestructEdgeCasesMatchGeth(t *testing.T) { + chainID := big.NewInt(713715) + slot := testHash(0x47) + value := testHash(0x48) + contractBalance := big.NewInt(99) + + tests := []struct { + name string + beneficiary func(common.Address) common.Address + wantContractBalance *big.Int + wantBeneficiaryCredit *big.Int + }{ + { + name: "preexisting contract sends balance but keeps code and storage", + beneficiary: func(common.Address) common.Address { + return testAddress(0xdb) + }, + wantContractBalance: big.NewInt(0), + wantBeneficiaryCredit: contractBalance, + }, + { + name: "preexisting contract self beneficiary keeps balance code and storage", + beneficiary: func(contract common.Address) common.Address { + return contract + }, + wantContractBalance: contractBalance, + wantBeneficiaryCredit: contractBalance, + }, + } + + for i, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + contract := common.BigToAddress(big.NewInt(int64(0xf0 + i))) + beneficiary := tc.beneficiary(contract) + runtime := selfDestructCode(beneficiary) + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000_000_000)) + state.SetBalance(contract, contractBalance) + state.SetNonce(contract, 1) + state.SetCode(contract, runtime) + state.SetState(contract, slot, value) + + rawTx := signLegacyTxWithGasPrice(t, key, chainID, 0, &contract, big.NewInt(0), nil, 100_000, big.NewInt(1)) + cfg := Config{MinGasPrice: big.NewInt(0)} + ctx := blockContext(chainID) + + gethResult, err := executeGethReferenceBlock(t, state, cfg, ctx, [][]byte{rawTx}) + require.NoError(t, err) + execResult, err := NewExecutor(cfg, WithState(state)).ExecuteBlock(context.Background(), BlockRequest{ + Context: ctx, + Txs: [][]byte{rawTx}, + }) + require.NoError(t, err) + state.ApplyChangeSet(execResult.ChangeSet) + + requireExecutionParity(t, execResult, gethResult) + requireAddressParity(t, state, gethResult.state, sender) + requireAddressParity(t, state, gethResult.state, contract, slot) + requireAddressParity(t, state, gethResult.state, beneficiary) + require.Equal(t, runtime, state.GetCode(contract)) + require.Equal(t, uint64(1), state.GetNonce(contract)) + require.Equal(t, value, state.GetState(contract, slot)) + require.Equal(t, tc.wantContractBalance, state.GetBalance(contract)) + if beneficiary != contract { + require.Equal(t, tc.wantBeneficiaryCredit, state.GetBalance(beneficiary)) + } + }) + } +} + +func TestExecutorAccessListGasAccountingMatchesGeth(t *testing.T) { + chainID := big.NewInt(713715) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + contract := testAddress(0xd8) + slot := testHash(0x44) + cfg := Config{MinGasPrice: big.NewInt(0)} + ctx := blockContext(chainID) + + run := func(t *testing.T, accessList ethtypes.AccessList) (uint64, *BlockResult) { + t.Helper() + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000_000_000)) + state.SetCode(contract, sloadRuntime(slot)) + state.SetState(contract, slot, testHash(0x45)) + rawTx := signAccessListTx(t, key, chainID, 0, &contract, big.NewInt(0), nil, 120_000, big.NewInt(1), accessList) + + gethResult, err := executeGethReferenceBlock(t, state, cfg, ctx, [][]byte{rawTx}) + require.NoError(t, err) + execResult, err := NewExecutor(cfg, WithState(state)).ExecuteBlock(context.Background(), BlockRequest{ + Context: ctx, + Txs: [][]byte{rawTx}, + }) + require.NoError(t, err) + requireExecutionParity(t, execResult, gethResult) + return execResult.Txs[0].GasUsed, execResult + } + + coldGas, _ := run(t, nil) + tests := []struct { + name string + accessList ethtypes.AccessList + expectedDelta uint64 + }{ + { + name: "contract slot warmed", + accessList: ethtypes.AccessList{{ + Address: contract, + StorageKeys: []common.Hash{slot}, + }}, + expectedDelta: params.TxAccessListAddressGas + + params.TxAccessListStorageKeyGas - + (params.ColdSloadCostEIP2929 - params.WarmStorageReadCostEIP2929), + }, + { + name: "address only still pays intrinsic cost because destination is already warm", + accessList: ethtypes.AccessList{{ + Address: contract, + }}, + expectedDelta: params.TxAccessListAddressGas, + }, + { + name: "duplicate storage keys are charged twice but warm once", + accessList: ethtypes.AccessList{{ + Address: contract, + StorageKeys: []common.Hash{slot, slot}, + }}, + expectedDelta: params.TxAccessListAddressGas + + 2*params.TxAccessListStorageKeyGas - + (params.ColdSloadCostEIP2929 - params.WarmStorageReadCostEIP2929), + }, + { + name: "unrelated entry only adds intrinsic access list gas", + accessList: ethtypes.AccessList{{ + Address: testAddress(0xdc), + StorageKeys: []common.Hash{testHash(0x49)}, + }}, + expectedDelta: params.TxAccessListAddressGas + params.TxAccessListStorageKeyGas, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + gasUsed, execResult := run(t, tc.accessList) + + require.Equal(t, coldGas+tc.expectedDelta, gasUsed) + require.Equal(t, uint8(ethtypes.AccessListTxType), execResult.Receipts[0].Type) + }) + } +} + +func TestExecutorLogOpcodeCorpusMatchesGeth(t *testing.T) { + chainID := big.NewInt(713715) + data := testHash(0x60) + cfg := Config{MinGasPrice: big.NewInt(0)} + ctx := blockContext(chainID) + + for topicCount := 0; topicCount <= 4; topicCount++ { + t.Run(fmt.Sprintf("LOG%d", topicCount), func(t *testing.T) { + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + contract := common.BigToAddress(big.NewInt(int64(0x100 + topicCount))) + topics := make([]common.Hash, topicCount) + for i := range topics { + topics[i] = common.BigToHash(big.NewInt(int64(0x700 + i))) + } + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000_000_000)) + state.SetCode(contract, logRuntime(data, topics)) + rawTx := signLegacyTxWithGasPrice(t, key, chainID, 0, &contract, big.NewInt(0), nil, 200_000, big.NewInt(1)) + + gethResult, err := executeGethReferenceBlock(t, state, cfg, ctx, [][]byte{rawTx}) + require.NoError(t, err) + execResult, err := NewExecutor(cfg, WithState(state)).ExecuteBlock(context.Background(), BlockRequest{ + Context: ctx, + Txs: [][]byte{rawTx}, + }) + require.NoError(t, err) + state.ApplyChangeSet(execResult.ChangeSet) + + requireExecutionParity(t, execResult, gethResult) + requireAddressParity(t, state, gethResult.state, sender) + requireAddressParity(t, state, gethResult.state, contract) + require.Len(t, execResult.Receipts[0].Logs, 1) + log := execResult.Receipts[0].Logs[0] + require.Equal(t, contract, log.Address) + require.Equal(t, topics, log.Topics) + require.Equal(t, data.Bytes(), log.Data) + require.Equal(t, ethtypes.CreateBloom(execResult.Receipts[0]), execResult.Receipts[0].Bloom) + }) + } +} + +func TestExecutorCallOpcodeCorpusMatchesGeth(t *testing.T) { + chainID := big.NewInt(713715) + targetSlot := testHash(0x61) + successSlot := testHash(0x62) + targetValue := testHash(0x63) + cfg := Config{MinGasPrice: big.NewInt(0)} + ctx := blockContext(chainID) + + tests := []struct { + name string + op byte + wantSuccess common.Hash + wantCallerTarget common.Hash + wantTargetTarget common.Hash + wantTargetUnchanged bool + }{ + { + name: "CALL writes callee storage", + op: 0xf1, + wantSuccess: common.BigToHash(big.NewInt(1)), + wantTargetTarget: targetValue, + }, + { + name: "CALLCODE writes caller storage", + op: 0xf2, + wantSuccess: common.BigToHash(big.NewInt(1)), + wantCallerTarget: targetValue, + }, + { + name: "DELEGATECALL writes caller storage", + op: 0xf4, + wantSuccess: common.BigToHash(big.NewInt(1)), + wantCallerTarget: targetValue, + }, + { + name: "STATICCALL reports failure and blocks callee storage writes", + op: 0xfa, + wantSuccess: common.Hash{}, + wantTargetUnchanged: true, + }, + } + + for i, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + caller := common.BigToAddress(big.NewInt(int64(0x120 + i))) + target := common.BigToAddress(big.NewInt(int64(0x130 + i))) + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000_000_000)) + state.SetCode(caller, callOpcodeRuntime(tc.op, target, successSlot)) + state.SetCode(target, storeCode(targetSlot, targetValue)) + rawTx := signLegacyTxWithGasPrice(t, key, chainID, 0, &caller, big.NewInt(0), nil, 500_000, big.NewInt(1)) + + gethResult, err := executeGethReferenceBlock(t, state, cfg, ctx, [][]byte{rawTx}) + require.NoError(t, err) + execResult, err := NewExecutor(cfg, WithState(state)).ExecuteBlock(context.Background(), BlockRequest{ + Context: ctx, + Txs: [][]byte{rawTx}, + }) + require.NoError(t, err) + state.ApplyChangeSet(execResult.ChangeSet) + + requireExecutionParity(t, execResult, gethResult) + requireAddressParity(t, state, gethResult.state, sender) + requireAddressParity(t, state, gethResult.state, caller, targetSlot, successSlot) + requireAddressParity(t, state, gethResult.state, target, targetSlot) + require.Equal(t, tc.wantSuccess, state.GetState(caller, successSlot)) + require.Equal(t, tc.wantCallerTarget, state.GetState(caller, targetSlot)) + require.Equal(t, tc.wantTargetTarget, state.GetState(target, targetSlot)) + if tc.wantTargetUnchanged { + require.Equal(t, common.Hash{}, state.GetState(target, targetSlot)) + } + }) + } +} + +func TestExecutorEnvironmentOpcodeCorpusMatchesGeth(t *testing.T) { + chainID := big.NewInt(713715) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + contract := testAddress(0xdf) + contractBalance := big.NewInt(123) + slots := environmentSlots() + ctx := blockContext(chainID) + ctx.Number = 99 + ctx.Time = 12345 + ctx.GasLimit = 9_000_000 + ctx.BaseFee = big.NewInt(11) + ctx.BlobBaseFee = big.NewInt(13) + ctx.Coinbase = testAddress(0xe1) + ctx.ParentHash = testHash(0x64) + ctx.PrevRandao = testHash(0x65) + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000_000_000)) + state.SetBalance(contract, contractBalance) + state.SetCode(contract, environmentRuntime(ctx.Number-1, slots)) + rawTx := signLegacyTxWithGasPrice(t, key, chainID, 0, &contract, big.NewInt(0), nil, 500_000, big.NewInt(20)) + cfg := Config{MinGasPrice: big.NewInt(0)} + + gethResult, err := executeGethReferenceBlock(t, state, cfg, ctx, [][]byte{rawTx}) + require.NoError(t, err) + execResult, err := NewExecutor(cfg, WithState(state)).ExecuteBlock(context.Background(), BlockRequest{ + Context: ctx, + Txs: [][]byte{rawTx}, + }) + require.NoError(t, err) + state.ApplyChangeSet(execResult.ChangeSet) + + requireExecutionParity(t, execResult, gethResult) + requireAddressParity(t, state, gethResult.state, sender) + requireAddressParity(t, state, gethResult.state, contract, slots.all()...) + require.Equal(t, ctx.ParentHash, state.GetState(contract, slots.blockHash)) + require.Equal(t, common.BytesToHash(ctx.Coinbase.Bytes()), state.GetState(contract, slots.coinbase)) + require.Equal(t, common.BigToHash(new(big.Int).SetUint64(ctx.Time)), state.GetState(contract, slots.timestamp)) + require.Equal(t, common.BigToHash(new(big.Int).SetUint64(ctx.Number)), state.GetState(contract, slots.number)) + require.Equal(t, ctx.PrevRandao, state.GetState(contract, slots.prevRandao)) + require.Equal(t, common.BigToHash(new(big.Int).SetUint64(ctx.GasLimit)), state.GetState(contract, slots.gasLimit)) + require.Equal(t, common.BigToHash(chainID), state.GetState(contract, slots.chainID)) + require.Equal(t, common.BigToHash(contractBalance), state.GetState(contract, slots.selfBalance)) + require.Equal(t, common.BigToHash(ctx.BaseFee), state.GetState(contract, slots.baseFee)) + require.Equal(t, common.BigToHash(ctx.BlobBaseFee), state.GetState(contract, slots.blobBaseFee)) +} + +func TestExecutorVMFailureReceiptsAndFeesMatchGeth(t *testing.T) { + chainID := big.NewInt(713715) + contract := testAddress(0xdd) + coinbase := testAddress(0xde) + storageSlot := testHash(0x50) + + tests := []struct { + name string + code []byte + gasLimit uint64 + wantErr error + checkState func(t *testing.T, state *MemoryState) + }{ + { + name: "revert refunds unused gas and keeps receipt failure", + code: revertRuntime(), + gasLimit: 100_000, + wantErr: vm.ErrExecutionReverted, + }, + { + name: "invalid opcode consumes the transaction gas limit", + code: []byte{0xfe}, + gasLimit: 100_000, + checkState: func(t *testing.T, state *MemoryState) { + t.Helper() + require.Equal(t, big.NewInt(100_000), state.GetBalance(coinbase)) + }, + }, + { + name: "out of gas rolls back storage", + code: storeCode(storageSlot, testHash(0x51)), + gasLimit: 22_000, + wantErr: vm.ErrOutOfGas, + checkState: func(t *testing.T, state *MemoryState) { + t.Helper() + require.Equal(t, common.Hash{}, state.GetState(contract, storageSlot)) + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + initialBalance := big.NewInt(1_000_000_000) + state := NewMemoryState() + state.SetBalance(sender, initialBalance) + state.SetCode(contract, tc.code) + rawTx := signLegacyTxWithGasPrice(t, key, chainID, 0, &contract, big.NewInt(0), nil, tc.gasLimit, big.NewInt(1)) + cfg := Config{MinGasPrice: big.NewInt(0)} + ctx := blockContext(chainID) + ctx.Coinbase = coinbase + + gethResult, err := executeGethReferenceBlock(t, state, cfg, ctx, [][]byte{rawTx}) + require.NoError(t, err) + execResult, err := NewExecutor(cfg, WithState(state)).ExecuteBlock(context.Background(), BlockRequest{ + Context: ctx, + Txs: [][]byte{rawTx}, + }) + require.NoError(t, err) + state.ApplyChangeSet(execResult.ChangeSet) + + requireExecutionParity(t, execResult, gethResult) + requireAddressParity(t, state, gethResult.state, sender) + requireAddressParity(t, state, gethResult.state, contract, storageSlot) + requireAddressParity(t, state, gethResult.state, coinbase) + require.Equal(t, ethtypes.ReceiptStatusFailed, execResult.Receipts[0].Status) + require.Equal(t, ethtypes.ReceiptStatusFailed, execResult.Txs[0].Status) + if tc.wantErr != nil { + require.ErrorIs(t, execResult.Txs[0].Err, tc.wantErr) + } else { + require.Error(t, execResult.Txs[0].Err) + } + require.Equal(t, uint64(1), state.GetNonce(sender)) + fee := new(big.Int).SetUint64(execResult.Txs[0].GasUsed) + require.Equal(t, new(big.Int).Sub(initialBalance, fee), state.GetBalance(sender)) + require.Equal(t, fee, state.GetBalance(coinbase)) + require.Empty(t, execResult.Receipts[0].Logs) + if tc.checkState != nil { + tc.checkState(t, state) + } + }) + } +} + +func TestExecutorPreVMFailuresMatchGeth(t *testing.T) { + chainID := big.NewInt(713715) + recipient := testAddress(0xd9) + ctx := blockContext(chainID) + + tests := []struct { + name string + cfg Config + setup func(t *testing.T) (*MemoryState, []byte) + want error + }{ + { + name: "nonce too high", + setup: func(t *testing.T) (*MemoryState, []byte) { + t.Helper() + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000_000_000)) + return state, signLegacyTxWithGasPrice(t, key, chainID, 1, &recipient, big.NewInt(1), nil, 100_000, big.NewInt(1)) + }, + want: core.ErrNonceTooHigh, + }, + { + name: "nonce too low", + setup: func(t *testing.T) (*MemoryState, []byte) { + t.Helper() + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000_000_000)) + state.SetNonce(sender, 1) + return state, signLegacyTxWithGasPrice(t, key, chainID, 0, &recipient, big.NewInt(1), nil, 100_000, big.NewInt(1)) + }, + want: core.ErrNonceTooLow, + }, + { + name: "insufficient funds", + setup: func(t *testing.T) (*MemoryState, []byte) { + t.Helper() + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1)) + return state, signLegacyTxWithGasPrice(t, key, chainID, 0, &recipient, big.NewInt(1), nil, 100_000, big.NewInt(1)) + }, + want: core.ErrInsufficientFunds, + }, + { + name: "intrinsic gas too low", + setup: func(t *testing.T) (*MemoryState, []byte) { + t.Helper() + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000_000_000)) + return state, signLegacyTxWithGasPrice(t, key, chainID, 0, &recipient, big.NewInt(1), nil, 20_000, big.NewInt(1)) + }, + want: core.ErrIntrinsicGas, + }, + { + name: "fee cap below base fee", + cfg: Config{DisableGasPriceCheck: true}, + setup: func(t *testing.T) (*MemoryState, []byte) { + t.Helper() + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000_000_000)) + return state, signDynamicFeeTxWithFees(t, key, chainID, 0, &recipient, big.NewInt(1), nil, big.NewInt(1), big.NewInt(1), 100_000) + }, + want: core.ErrFeeCapTooLow, + }, + { + name: "tip cap above fee cap", + cfg: Config{DisableGasPriceCheck: true}, + setup: func(t *testing.T) (*MemoryState, []byte) { + t.Helper() + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000_000_000)) + return state, signDynamicFeeTxWithFees(t, key, chainID, 0, &recipient, big.NewInt(1), nil, big.NewInt(3), big.NewInt(2), 100_000) + }, + want: core.ErrTipAboveFeeCap, + }, + { + name: "sender has contract code", + setup: func(t *testing.T) (*MemoryState, []byte) { + t.Helper() + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000_000_000)) + state.SetCode(sender, []byte{0x00}) + return state, signLegacyTxWithGasPrice(t, key, chainID, 0, &recipient, big.NewInt(1), nil, 100_000, big.NewInt(1)) + }, + want: core.ErrSenderNoEOA, + }, + { + name: "sender nonce already max uint64", + setup: func(t *testing.T) (*MemoryState, []byte) { + t.Helper() + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000_000_000)) + state.SetNonce(sender, math.MaxUint64) + return state, signLegacyTxWithGasPrice(t, key, chainID, math.MaxUint64, &recipient, big.NewInt(1), nil, 100_000, big.NewInt(1)) + }, + want: core.ErrNonceMax, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + state, rawTx := tc.setup(t) + testCtx := ctx + if errors.Is(tc.want, core.ErrFeeCapTooLow) { + testCtx.BaseFee = big.NewInt(2) + } + cfg := tc.cfg + if cfg.MinGasPrice == nil { + cfg.MinGasPrice = big.NewInt(0) + } + + gethResult, gethErr := executeGethReferenceBlock(t, state, cfg, testCtx, [][]byte{rawTx}) + execResult, execErr := NewExecutor(cfg, WithState(state)).ExecuteBlock(context.Background(), BlockRequest{ + Context: testCtx, + Txs: [][]byte{rawTx}, + }) + + require.ErrorIs(t, gethErr, tc.want) + require.ErrorIs(t, execErr, tc.want) + require.Nil(t, gethResult) + require.Nil(t, execResult) + require.Equal(t, big.NewInt(0), state.GetBalance(recipient)) + }) + } +} + +func TestExecutorPrepareBlockRejectsBlobTransactions(t *testing.T) { + chainID := big.NewInt(713715) + key, err := crypto.GenerateKey() + require.NoError(t, err) + recipient := testAddress(0xda) + rawTx := signBlobTxWithFees( + t, + key, + chainID, + 0, + recipient, + big.NewInt(1), + nil, + big.NewInt(1), + big.NewInt(3), + big.NewInt(3), + 100_000, + []common.Hash{testHash(0x46)}, + ) + ctx := blockContext(chainID) + ctx.BlobBaseFee = big.NewInt(1) + + prepared, err := NewExecutor(Config{MinGasPrice: big.NewInt(0)}).PrepareBlock(context.Background(), BlockRequest{ + Context: ctx, + Txs: [][]byte{rawTx}, + }) + + require.ErrorIs(t, err, errUnsupportedBlobTx) + require.Empty(t, prepared.Txs) +} + +func TestExecutorOCCDeterministicAcrossRuns(t *testing.T) { + chainID := big.NewInt(713715) + txCount := 24 + rawTxs := make([][]byte, 0, txCount) + senders := make([]common.Address, 0, txCount) + recipients := make([]common.Address, 0, txCount) + + for i := range txCount { + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + recipient := common.BigToAddress(big.NewInt(int64(40_000 + i))) + senders = append(senders, sender) + recipients = append(recipients, recipient) + rawTxs = append(rawTxs, signLegacyTxWithGasPrice(t, key, chainID, 0, &recipient, big.NewInt(int64(i+1)), nil, 100_000, big.NewInt(1))) + } + + newState := func() *MemoryState { + state := NewMemoryState() + for _, sender := range senders { + state.SetBalance(sender, big.NewInt(1_000_000_000)) + } + return state + } + + var baseline *BlockResult + var baselineState *MemoryState + cfg := Config{MinGasPrice: big.NewInt(0), OCCWorkers: 4} + req := BlockRequest{Context: blockContext(chainID), Txs: rawTxs} + for iteration := 0; iteration < 8; iteration++ { + state := newState() + executor := NewExecutor(cfg, WithState(state)) + result, err := executor.ExecuteBlock(context.Background(), req) + executor.Close() + require.NoError(t, err) + require.True(t, result.OCCStats.Attempted) + require.False(t, result.OCCStats.Fallback) + require.Zero(t, result.OCCStats.ConflictCount) + state.ApplyChangeSet(result.ChangeSet) + + if iteration == 0 { + baseline = result + baselineState = state + continue + } + require.Equal(t, baseline.GasUsed, result.GasUsed) + require.Equal(t, baseline.Txs, result.Txs) + require.Equal(t, baseline.Receipts, result.Receipts) + require.Equal(t, baseline.ChangeSet, result.ChangeSet) + require.Equal(t, baseline.OCCStats, result.OCCStats) + for i := range txCount { + require.Equal(t, baselineState.GetBalance(senders[i]), state.GetBalance(senders[i])) + require.Equal(t, baselineState.GetBalance(recipients[i]), state.GetBalance(recipients[i])) + } + require.Equal(t, baselineState.GetBalance(req.Context.Coinbase), state.GetBalance(req.Context.Coinbase)) + } +} + +func executeGethReferenceBlock(t *testing.T, initial *MemoryState, cfg Config, ctx BlockContext, rawTxs [][]byte) (*gethReferenceResult, error) { + t.Helper() + chainConfig := chainConfigForTest(cfg, ctx) + if err := validateBlockContext(chainConfig, ctx); err != nil { + return nil, err + } + stateDB := newGethStateFromMemory(t, initial) + evm := vm.NewEVM(buildBlockContext(ctx), stateDB, chainConfig, vm.Config{}, nil) + gasLimit := ctx.GasLimit + if gasLimit == 0 { + gasLimit = math.MaxUint64 + } + gasPool := new(core.GasPool).AddGas(gasLimit) + baseFee := cloneOptionalBig(ctx.BaseFee) + signer := ethtypes.MakeSigner(chainConfig, new(big.Int).SetUint64(ctx.Number), ctx.Time) + result := &gethReferenceResult{} + + for txIndex, rawTx := range rawTxs { + var tx ethtypes.Transaction + if err := tx.UnmarshalBinary(rawTx); err != nil { + return nil, err + } + if err := validateSupportedTx(&tx); err != nil { + return nil, err + } + sender, err := ethtypes.Sender(signer, &tx) + if err != nil { + return nil, err + } + msg, err := core.TransactionToMessage(&tx, signer, baseFee) + if err != nil { + return nil, err + } + stateDB.SetTxContext(tx.Hash(), txIndex) + evm.SetTxContext(core.NewEVMTxContext(msg)) + execResult, err := core.ApplyMessage(evm, msg, gasPool) + if err != nil { + return nil, err + } + stateDB.Finalise(true) + + txIndexUint := uint(txIndex) + txLogs := stateDB.GetLogs(tx.Hash(), ctx.Number, ctx.BlockHash) + status := ethtypes.ReceiptStatusSuccessful + if execResult.Failed() { + status = ethtypes.ReceiptStatusFailed + } + receipt := ðtypes.Receipt{ + Type: tx.Type(), + Status: status, + Logs: txLogs, + TxHash: tx.Hash(), + GasUsed: execResult.UsedGas, + EffectiveGasPrice: effectiveGasPrice(&tx, baseFee), + BlockHash: ctx.BlockHash, + BlockNumber: new(big.Int).SetUint64(ctx.Number), + TransactionIndex: txIndexUint, + } + if tx.To() == nil { + receipt.ContractAddress = crypto.CreateAddress(sender, tx.Nonce()) + } + receipt.Bloom = ethtypes.CreateBloom(receipt) + + result.gasUsed += execResult.UsedGas + receipt.CumulativeGasUsed = result.gasUsed + result.txs = append(result.txs, TxResult{ + Hash: tx.Hash(), + Sender: sender, + To: tx.To(), + ContractAddress: receipt.ContractAddress, + Status: status, + GasUsed: execResult.UsedGas, + CumulativeGasUsed: result.gasUsed, + EffectiveGasPrice: new(big.Int).Set(receipt.EffectiveGasPrice), + Logs: txLogs, + Err: execResult.Err, + }) + result.receipts = append(result.receipts, receipt) + } + result.state = stateDB + return result, nil +} + +func newGethStateFromMemory(t *testing.T, initial *MemoryState) *gethstate.StateDB { + t.Helper() + db := gethstate.NewDatabase(triedb.NewDatabase(rawdb.NewMemoryDatabase(), nil), nil) + seed, err := gethstate.New(ethtypes.EmptyRootHash, db) + require.NoError(t, err) + if initial != nil { + initial.mu.RLock() + for addr, acct := range initial.accounts { + if acct.Balance != nil { + seed.SetBalance(addr, uint256.MustFromBig(acct.Balance), tracing.BalanceChangeUnspecified) + } + if acct.Nonce != 0 { + seed.SetNonce(addr, acct.Nonce, tracing.NonceChangeUnspecified) + } + if len(acct.Code) != 0 { + seed.SetCode(addr, acct.Code) + } + for key, value := range acct.Storage { + seed.SetState(addr, key, value) + } + } + initial.mu.RUnlock() + } + root, err := seed.Commit(0, true, false) + require.NoError(t, err) + stateDB, err := gethstate.New(root, db) + require.NoError(t, err) + return stateDB +} + +func chainConfigForTest(cfg Config, ctx BlockContext) *params.ChainConfig { + executor := &Executor{cfg: cfg.WithDefaults()} + return executor.chainConfig(ctx) +} + +func requireExecutionParity(t *testing.T, execResult *BlockResult, gethResult *gethReferenceResult) { + t.Helper() + require.Equal(t, gethResult.gasUsed, execResult.GasUsed) + require.Len(t, execResult.Txs, len(gethResult.txs)) + require.Len(t, execResult.Receipts, len(gethResult.receipts)) + for i := range gethResult.txs { + require.Equal(t, gethResult.txs[i], execResult.Txs[i]) + require.Equal(t, gethResult.receipts[i].Type, execResult.Receipts[i].Type) + require.Equal(t, gethResult.receipts[i].Status, execResult.Receipts[i].Status) + require.Equal(t, gethResult.receipts[i].CumulativeGasUsed, execResult.Receipts[i].CumulativeGasUsed) + require.Equal(t, gethResult.receipts[i].Bloom, execResult.Receipts[i].Bloom) + require.Equal(t, gethResult.receipts[i].Logs, execResult.Receipts[i].Logs) + require.Equal(t, gethResult.receipts[i].TxHash, execResult.Receipts[i].TxHash) + require.Equal(t, gethResult.receipts[i].ContractAddress, execResult.Receipts[i].ContractAddress) + require.Equal(t, gethResult.receipts[i].GasUsed, execResult.Receipts[i].GasUsed) + require.Equal(t, gethResult.receipts[i].EffectiveGasPrice, execResult.Receipts[i].EffectiveGasPrice) + require.Equal(t, gethResult.receipts[i].BlockHash, execResult.Receipts[i].BlockHash) + require.Equal(t, gethResult.receipts[i].BlockNumber, execResult.Receipts[i].BlockNumber) + require.Equal(t, gethResult.receipts[i].TransactionIndex, execResult.Receipts[i].TransactionIndex) + } +} + +func requireAddressParity(t *testing.T, state *MemoryState, gethState *gethstate.StateDB, addr common.Address, slots ...common.Hash) { + t.Helper() + require.Zero(t, gethState.GetBalance(addr).ToBig().Cmp(state.GetBalance(addr))) + require.Equal(t, gethState.GetNonce(addr), state.GetNonce(addr)) + require.Equal(t, gethState.GetCode(addr), state.GetCode(addr)) + for _, slot := range slots { + require.Equal(t, gethState.GetState(addr, slot), state.GetState(addr, slot)) + } +} + +func signAccessListTx( + t *testing.T, + key *ecdsa.PrivateKey, + chainID *big.Int, + nonce uint64, + to *common.Address, + value *big.Int, + data []byte, + gas uint64, + gasPrice *big.Int, + accessList ethtypes.AccessList, +) []byte { + t.Helper() + tx := ethtypes.NewTx(ðtypes.AccessListTx{ + ChainID: chainID, + Nonce: nonce, + GasPrice: new(big.Int).Set(gasPrice), + Gas: gas, + To: to, + Value: value, + Data: data, + AccessList: accessList, + }) + signed, err := ethtypes.SignTx(tx, ethtypes.LatestSignerForChainID(chainID), key) + require.NoError(t, err) + raw, err := signed.MarshalBinary() + require.NoError(t, err) + return raw +} + +func sloadRuntime(slot common.Hash) []byte { + code := appendPush32(nil, slot) + return append(code, 0x54, 0x00) +} + +func revertRuntime() []byte { + return []byte{0x60, 0x00, 0x60, 0x00, 0xfd} +} + +func logRuntime(data common.Hash, topics []common.Hash) []byte { + code := appendPush32(nil, data) + code = appendPush1(code, 0) + code = append(code, 0x52) + for i := len(topics) - 1; i >= 0; i-- { + code = appendPush32(code, topics[i]) + } + code = appendPush1(code, common.HashLength) + code = appendPush1(code, 0) + return append(code, 0xa0+byte(len(topics)), 0x00) +} + +func callOpcodeRuntime(op byte, target common.Address, successSlot common.Hash) []byte { + code := appendPush1(nil, 0) + code = appendPush1(code, 0) + code = appendPush1(code, 0) + code = appendPush1(code, 0) + if op == 0xf1 || op == 0xf2 { + code = appendPush1(code, 0) + } + code = appendPush20(code, target) + code = appendPush2(code, 0xffff) + code = append(code, op) + code = appendPush32(code, successSlot) + return append(code, 0x55, 0x00) +} + +type envOpcodeSlots struct { + blockHash common.Hash + coinbase common.Hash + timestamp common.Hash + number common.Hash + prevRandao common.Hash + gasLimit common.Hash + chainID common.Hash + selfBalance common.Hash + baseFee common.Hash + blobBaseFee common.Hash +} + +func environmentSlots() envOpcodeSlots { + return envOpcodeSlots{ + blockHash: testHash(0x70), + coinbase: testHash(0x71), + timestamp: testHash(0x72), + number: testHash(0x73), + prevRandao: testHash(0x74), + gasLimit: testHash(0x75), + chainID: testHash(0x76), + selfBalance: testHash(0x77), + baseFee: testHash(0x78), + blobBaseFee: testHash(0x79), + } +} + +func (s envOpcodeSlots) all() []common.Hash { + return []common.Hash{ + s.blockHash, + s.coinbase, + s.timestamp, + s.number, + s.prevRandao, + s.gasLimit, + s.chainID, + s.selfBalance, + s.baseFee, + s.blobBaseFee, + } +} + +func environmentRuntime(blockNumberForHash uint64, slots envOpcodeSlots) []byte { + code := appendPushUint64(nil, blockNumberForHash) + code = append(code, 0x40) + code = appendStoreTop(code, slots.blockHash) + for _, op := range []struct { + op byte + slot common.Hash + }{ + {op: 0x41, slot: slots.coinbase}, + {op: 0x42, slot: slots.timestamp}, + {op: 0x43, slot: slots.number}, + {op: 0x44, slot: slots.prevRandao}, + {op: 0x45, slot: slots.gasLimit}, + {op: 0x46, slot: slots.chainID}, + {op: 0x47, slot: slots.selfBalance}, + {op: 0x48, slot: slots.baseFee}, + {op: 0x4a, slot: slots.blobBaseFee}, + } { + code = append(code, op.op) + code = appendStoreTop(code, op.slot) + } + return append(code, 0x00) +} + +func erc20TransferRuntime(fromSlot, toSlot common.Hash, from, to common.Address, amount byte) []byte { + code := appendPush32(nil, fromSlot) + code = append(code, 0x54) + code = appendPush1(code, amount) + code = append(code, 0x90, 0x03) + code = appendPush32(code, fromSlot) + code = append(code, 0x55) + + code = appendPush32(code, toSlot) + code = append(code, 0x54) + code = appendPush1(code, amount) + code = append(code, 0x01) + code = appendPush32(code, toSlot) + code = append(code, 0x55) + + code = appendPush1(code, amount) + code = appendPush1(code, 0) + code = append(code, 0x52) + code = appendPush20(code, to) + code = appendPush20(code, from) + code = appendPush32(code, erc20TransferTopic()) + code = appendPush1(code, common.HashLength) + code = appendPush1(code, 0) + return append(code, 0xa3, 0x00) +} + +func createAndDestroyChildRuntime(beneficiary common.Address, endowment byte) []byte { + childInit := initCode(selfDestructCode(beneficiary)) + if len(childInit) > math.MaxUint8 { + panic("child init code too large for test runtime") + } + code := []byte{ + 0x60, byte(len(childInit)), + 0x60, 0x00, + 0x60, 0x00, + 0x39, + 0x60, byte(len(childInit)), + 0x60, 0x00, + 0x60, endowment, + 0xf0, + 0x60, 0x00, + 0x60, 0x00, + 0x60, 0x00, + 0x60, 0x00, + 0x60, 0x00, + 0x85, + 0x61, 0xff, 0xff, + 0xf1, + 0x00, + } + code[3] = byte(len(code)) + return append(code, childInit...) +} + +func erc20TransferTopic() common.Hash { + return crypto.Keccak256Hash([]byte("Transfer(address,address,uint256)")) +} + +func appendPush1(code []byte, value byte) []byte { + return append(code, 0x60, value) +} + +func appendPush2(code []byte, value uint16) []byte { + return append(code, 0x61, byte(value>>8), byte(value)) +} + +func appendPush20(code []byte, value common.Address) []byte { + code = append(code, 0x73) + return append(code, value.Bytes()...) +} + +func appendPush32(code []byte, value common.Hash) []byte { + code = append(code, 0x7f) + return append(code, value.Bytes()...) +} + +func appendPushUint64(code []byte, value uint64) []byte { + return appendPush32(code, common.BigToHash(new(big.Int).SetUint64(value))) +} + +func appendStoreTop(code []byte, slot common.Hash) []byte { + code = appendPush32(code, slot) + return append(code, 0x55) +}