From 204a411d773d983cd52e4c7444141ee4a6664272 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Mon, 15 Jun 2026 11:27:57 +0800 Subject: [PATCH 01/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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] }