diff --git a/giga/evmonly/README.md b/giga/evmonly/README.md new file mode 100644 index 0000000000..650f70d3db --- /dev/null +++ b/giga/evmonly/README.md @@ -0,0 +1,159 @@ +# EVM-only giga execution + +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`. + +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 + +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 `evmonly` 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 +- optional executor-internal OCC for non-conflicting transaction sets +- Ethereum receipt construction with logs, bloom, gas, tx hash, block metadata, + contract address, and effective gas price +- a map-backed `MemoryState` for tests and early integration +- 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. A nil chain config +defaults to geth's `params.AllDevChainProtocolChanges`, which is convenient for +tests and scaffold wiring; production callers should provide the chain's +explicit EVM config. + +## Executor interface + +The boundary is: + +```go +ExecuteBlock(context.Context, BlockRequest) (*BlockResult, error) +``` + +For callers that can pipeline stateless work across blocks, the concrete +executor also supports: + +```go +PrepareBlock(context.Context, BlockRequest) (PreparedBlock, error) +ExecutePreparedBlock(context.Context, PreparedBlock) (*BlockResult, error) +``` + +`PrepareBlock` decodes transaction RLP and recovers senders. This work does not +touch state, so block N+1 can be prepared while block N is still executing. +`ExecuteBlock` remains the convenience path and performs prepare then execute in +one call. `PreparedBlock` is trusted executor-produced data: callers should pass +the result of `PrepareBlock` unchanged, because `ExecutePreparedBlock` does not +recover senders again. + +The executor should be commit-neutral. It executes an ordered EVM block and +returns the state writes and receipts produced by that block. The caller owns +durable persistence, state commitment, block indexing, and receipt publication. +The concrete `Executor` accepts a `StateReader` backend through `WithState(...)`; +callers can persist the returned `ChangeSet` with a matching `StateWriter`. +Call `Close()` when an executor is no longer used so OCC worker goroutines are +stopped promptly. + +A non-nil `error` means block validation failed and the caller must not commit a +partial output. EVM call failures inside an otherwise valid transaction are +represented in `Receipts` and `Txs` with failed status. + +## 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. + +`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: + +- `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 +- `StorageClears`: addresses whose persisted storage must be fully cleared +- `Storage`: final storage slot updates or deletions + +The changeset should be deterministic and serializable by the runtime layer. +The executor should not require `sdk.Context` or Cosmos stores to build it. +State writers should apply `StorageClears` before per-slot `Storage` updates. + +`Receipts` are emitted in transaction order and should contain the Ethereum +receipt fields needed by RPC and indexing: status, cumulative gas used, bloom, +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. + +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 `evmonly` executor accepts a custom +precompile registry only as a fail-closed placeholder. Calls to registered +custom precompile addresses return `ErrCustomPrecompilesOpen`. + +## Current limitations + +- OCC is optional and conservative; conflicting or unsupported blocks fall back + to sequential execution. +- State input is key-addressable only. The executor lazily reads storage slots + by `(address, slot)` and does not require or expose range iteration. +- The map-backed `MemoryState` is for tests and early integration; production + should provide a durable native state backend. +- Historical `BLOCKHASH` lookups beyond the parent block are not wired yet. +- Block-level blob gas accounting and `MaxBlobGasPerBlock` enforcement are not + wired yet, so blob transactions are rejected fail-closed. diff --git a/giga/evmonly/config.go b/giga/evmonly/config.go new file mode 100644 index 0000000000..af44ad0492 --- /dev/null +++ b/giga/evmonly/config.go @@ -0,0 +1,41 @@ +package evmonly + +import ( + "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 { + DisableNonceCheck bool + DisableGasPriceCheck bool + MinGasPrice *big.Int + // ChainConfig defaults to params.AllDevChainProtocolChanges when nil. Test + // and scaffold callers can use the default, but production wiring should pass + // the chain's explicit config. + ChainConfig *params.ChainConfig + CustomPrecompiles precompiles.Registry + OCCWorkers int + // BlockResultPoolSize enables a bounded reusable output pool. Callers that + // enable it must call BlockResult.Release when they are done with returned + // results. Result sinks receive a retained result and must release it after + // they finish async persistence. + BlockResultPoolSize int +} + +func DefaultConfig() Config { + return Config{ + MinGasPrice: big.NewInt(1_000_000_000), + } +} + +func (c Config) WithDefaults() Config { + defaults := DefaultConfig() + if c.MinGasPrice == nil { + c.MinGasPrice = new(big.Int).Set(defaults.MinGasPrice) + } + return c +} diff --git a/giga/evmonly/executor.go b/giga/evmonly/executor.go new file mode 100644 index 0000000000..bab7ed2eb0 --- /dev/null +++ b/giga/evmonly/executor.go @@ -0,0 +1,414 @@ +package evmonly + +import ( + "context" + "fmt" + "math" + "math/big" + "runtime" + + "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/precompiles" +) + +// Executor runs raw EVM transactions against an EVM-native state backend. +type Executor struct { + cfg Config + state StateReader + resultSink ResultSink + occPool *occWorkerPool + resultPool *blockResultPool +} + +type Option func(*Executor) + +func WithState(state StateReader) Option { + return func(e *Executor) { + if state != nil { + e.state = state + } + } +} + +func WithResultSink(sink ResultSink) Option { + return func(e *Executor) { + e.resultSink = sink + } +} + +// NewExecutor constructs an EVM-only executor. Call Close when the executor is +// no longer used; with OCCWorkers > 1, the executor owns worker goroutines that +// otherwise live until the GC finalizer runs. +func NewExecutor(cfg Config, opts ...Option) *Executor { + e := &Executor{ + cfg: cfg.WithDefaults(), + state: NewMemoryState(), + resultPool: newBlockResultPool(cfg.BlockResultPoolSize), + } + if e.cfg.OCCWorkers > 1 { + e.occPool = newOCCWorkerPool(e.cfg.OCCWorkers) + runtime.SetFinalizer(e, (*Executor).Close) + } + for _, opt := range opts { + opt(e) + } + return e +} + +func (e *Executor) Close() { + if e == nil || e.occPool == nil { + return + } + runtime.SetFinalizer(e, nil) + e.occPool.Close() + e.occPool = nil +} + +func (e *Executor) Config() Config { + return e.cfg +} + +func (e *Executor) ExecuteBlock(ctx context.Context, req BlockRequest) (*BlockResult, error) { + prepared, err := e.PrepareBlock(ctx, req) + if err != nil { + return nil, err + } + return e.ExecutePreparedBlock(ctx, prepared) +} + +func (e *Executor) PrepareBlock(ctx context.Context, req BlockRequest) (PreparedBlock, error) { + chainConfig := e.chainConfig(req.Context) + if err := validateBlockContext(chainConfig, req.Context); err != nil { + return PreparedBlock{}, err + } + signer := ethtypes.MakeSigner(chainConfig, new(big.Int).SetUint64(req.Context.Number), req.Context.Time) + parsed, err := parseBlockTxs(ctx, req.Txs, signer) + if err != nil { + return PreparedBlock{}, err + } + return PreparedBlock{ + Context: req.Context, + Txs: parsed, + }, nil +} + +func (e *Executor) ExecutePreparedBlock(ctx context.Context, req PreparedBlock) (*BlockResult, error) { + if err := validateBlockContext(e.chainConfig(req.Context), req.Context); err != nil { + return nil, err + } + var result *BlockResult + var err error + if len(req.Txs) == 0 { + result, err = e.acquireBlockResult(ctx, 0) + } else if e.useOCC(len(req.Txs)) { + result, err = e.executeBlockOCC(ctx, req) + } else { + result, err = e.executeBlockSequential(ctx, req) + } + if err != nil { + return nil, err + } + if err := e.sinkBlockResult(ctx, req.Context.Number, result); err != nil { + result.Release() + return nil, err + } + return result, nil +} + +func (e *Executor) acquireBlockResult(ctx context.Context, txCapacity int) (*BlockResult, error) { + if e.resultPool == nil { + result := &BlockResult{} + result.prepareForBlock(txCapacity) + return result, nil + } + return e.resultPool.acquire(ctx, txCapacity) +} + +func (e *Executor) sinkBlockResult(ctx context.Context, height uint64, result *BlockResult) error { + if e.resultSink == nil || result == nil { + return nil + } + release := result.retain() + if err := e.resultSink.StoreBlockResult(ctx, height, result, release); err != nil { + release() + return fmt.Errorf("store block result for block %d: %w", height, err) + } + return nil +} + +func (e *Executor) useOCC(txCount int) bool { + if e.cfg.OCCWorkers <= 1 || txCount <= 1 { + return false + } + if e.cfg.CustomPrecompiles == nil { + return true + } + return len(e.cfg.CustomPrecompiles.Addresses()) == 0 +} + +func (e *Executor) executeBlockSequential(ctx context.Context, req PreparedBlock) (*BlockResult, error) { + chainConfig := e.chainConfig(req.Context) + + 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 := cloneOptionalBig(req.Context.BaseFee) + + result, err := e.acquireBlockResult(ctx, len(req.Txs)) + if err != nil { + return nil, err + } + ok := false + defer func() { + if !ok { + result.Release() + } + }() + var txIndexUint uint + for txIndex, p := range req.Txs { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + txResult, receipt, err := e.executeTx(evm, stateDB, gasPool, req.Context, p, txIndex, txIndexUint, baseFee) + if err != nil { + return nil, fmt.Errorf("execute tx %d %s: %w", txIndex, p.Tx.Hash(), err) + } + 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 + txIndexUint++ + } + stateDB.clearSnapshots() + stateDB.Finalise(true) + stateDB.ChangeSetInto(&result.ChangeSet) + ok = true + return result, nil +} + +func (e *Executor) executeTx( + evm *vm.EVM, + stateDB *nativeStateDB, + gasPool *core.GasPool, + block BlockContext, + p PreparedTx, + txIndex int, + txIndexUint uint, + baseFee *big.Int, +) (TxResult, *ethtypes.Receipt, error) { + tx := p.Tx + if err := validateSupportedTx(tx); err != nil { + return TxResult{Hash: tx.Hash(), Sender: p.Sender, To: tx.To(), Err: err}, nil, err + } + if !e.cfg.DisableGasPriceCheck && e.cfg.MinGasPrice != nil { + // MinGasPrice is block-validity policy; unlike EVM call failures, it + // does not produce a receipt for an otherwise invalid block. + if effectiveGasPrice(tx, baseFee).Cmp(e.cfg.MinGasPrice) < 0 { + return TxResult{Hash: tx.Hash(), Sender: p.Sender, To: tx.To(), Err: errInsufficientGasPrice}, + nil, + errInsufficientGasPrice + } + } + + msg := transactionToPreparedMessage(p, baseFee) + msg.SkipNonceChecks = e.cfg.DisableNonceCheck + + stateDB.setTxContext(tx.Hash(), txIndex, txIndexUint) + logStart := len(stateDB.logs) + evm.SetTxContext(core.NewEVMTxContext(msg)) + execResult, err := core.ApplyMessage(evm, msg, gasPool) + if err != nil { + return TxResult{Hash: tx.Hash(), Sender: p.Sender, To: tx.To(), Err: err}, nil, err + } + stateDB.clearSnapshots() + stateDB.Finalise(true) + + 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 = txIndexUint + } + + 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: txIndexUint, + } + if tx.To() == nil { + receipt.ContractAddress = crypto.CreateAddress(p.Sender, tx.Nonce()) + } + if tx.Type() == ethtypes.BlobTxType { + receipt.BlobGasUsed = tx.BlobGas() + receipt.BlobGasPrice = cloneOptionalBig(block.BlobBaseFee) + } + receipt.Bloom = ethtypes.CreateBloom(receipt) + + txResult := TxResult{ + 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 transactionToPreparedMessage(p PreparedTx, baseFee *big.Int) *core.Message { + tx := p.Tx + msg := &core.Message{ + From: p.Sender, + Nonce: tx.Nonce(), + GasLimit: tx.Gas(), + GasPrice: new(big.Int).Set(tx.GasPrice()), + GasFeeCap: new(big.Int).Set(tx.GasFeeCap()), + GasTipCap: new(big.Int).Set(tx.GasTipCap()), + To: tx.To(), + Value: tx.Value(), + Data: tx.Data(), + AccessList: tx.AccessList(), + SetCodeAuthorizations: tx.SetCodeAuthorizations(), + SkipNonceChecks: false, + SkipFromEOACheck: false, + BlobHashes: tx.BlobHashes(), + BlobGasFeeCap: tx.BlobGasFeeCap(), + } + if baseFee != nil { + msg.GasPrice = msg.GasPrice.Add(msg.GasTipCap, baseFee) + if msg.GasPrice.Cmp(msg.GasFeeCap) > 0 { + msg.GasPrice = msg.GasFeeCap + } + } + return msg +} + +func buildBlockContext(ctx BlockContext) vm.BlockContext { + prevRandao := ctx.PrevRandao + baseFee := cloneOptionalBig(ctx.BaseFee) + blobBaseFee := cloneOptionalBig(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 { + if ctx.Number > 0 && n == ctx.Number-1 { + return ctx.ParentHash + } + 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 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 validateSupportedTx(tx *ethtypes.Transaction) error { + if tx.Type() == ethtypes.BlobTxType { + return errUnsupportedBlobTx + } + return nil +} + +func validateBlockContext(chainConfig *params.ChainConfig, ctx BlockContext) error { + if chainConfig != nil && chainConfig.IsLondon(new(big.Int).SetUint64(ctx.Number)) && ctx.BaseFee == nil { + return errMissingBaseFee + } + return nil +} + +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() +} + +var ( + errInsufficientGasPrice = fmt.Errorf("insufficient gas price") + errMissingBaseFee = fmt.Errorf("missing base fee for post-London block") + errUnsupportedBlobTx = fmt.Errorf("blob transactions require block-level blob gas accounting") +) diff --git a/giga/evmonly/executor_parity_test.go b/giga/evmonly/executor_parity_test.go new file mode 100644 index 0000000000..ffc2919397 --- /dev/null +++ b/giga/evmonly/executor_parity_test.go @@ -0,0 +1,1279 @@ +package evmonly + +import ( + "context" + "crypto/ecdsa" + "errors" + "fmt" + "math" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/rawdb" + gethstate "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/tracing" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/triedb" + "github.com/holiman/uint256" + "github.com/stretchr/testify/require" +) + +type gethReferenceResult struct { + state *gethstate.StateDB + txs []TxResult + receipts ethtypes.Receipts + gasUsed uint64 +} + +func TestExecutorNativeTransferFeeAccountingMatchesGeth(t *testing.T) { + chainID := big.NewInt(713715) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + recipient := testAddress(0xd1) + initialBalance := big.NewInt(1_000_000_000) + value := big.NewInt(1234) + baseFee := big.NewInt(7) + tipCap := big.NewInt(3) + feeCap := big.NewInt(20) + + state := NewMemoryState() + state.SetBalance(sender, initialBalance) + + rawTx := signDynamicFeeTxWithFees(t, key, chainID, 0, &recipient, value, nil, tipCap, feeCap, 100_000) + ctx := blockContext(chainID) + ctx.BaseFee = baseFee + ctx.Coinbase = testAddress(0xd2) + cfg := Config{MinGasPrice: big.NewInt(0)} + + gethResult, err := executeGethReferenceBlock(t, state, cfg, ctx, [][]byte{rawTx}) + require.NoError(t, err) + execResult, err := NewExecutor(cfg, WithState(state)).ExecuteBlock(context.Background(), BlockRequest{ + Context: ctx, + Txs: [][]byte{rawTx}, + }) + require.NoError(t, err) + state.ApplyChangeSet(execResult.ChangeSet) + + requireExecutionParity(t, execResult, gethResult) + requireAddressParity(t, state, gethResult.state, sender) + requireAddressParity(t, state, gethResult.state, recipient) + requireAddressParity(t, state, gethResult.state, ctx.Coinbase) + + gasUsed := execResult.Txs[0].GasUsed + effectivePrice := new(big.Int).Add(baseFee, tipCap) + require.Equal(t, effectivePrice, execResult.Txs[0].EffectiveGasPrice) + totalFee := new(big.Int).Mul(new(big.Int).SetUint64(gasUsed), effectivePrice) + expectedSender := new(big.Int).Sub(new(big.Int).Sub(new(big.Int).Set(initialBalance), value), totalFee) + require.Equal(t, expectedSender, state.GetBalance(sender)) + require.Equal(t, value, state.GetBalance(recipient)) + require.Equal(t, totalFee, state.GetBalance(ctx.Coinbase)) +} + +func TestExecutorNativeTransferEdgeCasesMatchGeth(t *testing.T) { + chainID := big.NewInt(713715) + initialBalance := big.NewInt(1_000_000_000_000) + + tests := []struct { + name string + value *big.Int + configureCtx func(*BlockContext, common.Address) + sign func(t *testing.T, key *ecdsa.PrivateKey, ctx BlockContext, recipient common.Address) []byte + wantEffective *big.Int + assert func(t *testing.T, state *MemoryState, sender, recipient, coinbase common.Address, gasUsed uint64) + }{ + { + name: "zero value transfer touches nonce and fees only", + value: big.NewInt(0), + sign: func(t *testing.T, key *ecdsa.PrivateKey, _ BlockContext, recipient common.Address) []byte { + t.Helper() + return signLegacyTxWithGasPrice(t, key, chainID, 0, &recipient, big.NewInt(0), nil, 100_000, big.NewInt(1)) + }, + wantEffective: big.NewInt(1), + assert: func(t *testing.T, state *MemoryState, sender, recipient, coinbase common.Address, gasUsed uint64) { + t.Helper() + fee := new(big.Int).SetUint64(gasUsed) + require.Equal(t, new(big.Int).Sub(initialBalance, fee), state.GetBalance(sender)) + require.Equal(t, big.NewInt(0), state.GetBalance(recipient)) + require.Equal(t, fee, state.GetBalance(coinbase)) + }, + }, + { + name: "dynamic fee effective price is capped by fee cap", + value: big.NewInt(17), + configureCtx: func(ctx *BlockContext, _ common.Address) { + ctx.BaseFee = big.NewInt(7) + }, + sign: func(t *testing.T, key *ecdsa.PrivateKey, _ BlockContext, recipient common.Address) []byte { + t.Helper() + return signDynamicFeeTxWithFees(t, key, chainID, 0, &recipient, big.NewInt(17), nil, big.NewInt(10), big.NewInt(12), 100_000) + }, + wantEffective: big.NewInt(12), + assert: func(t *testing.T, state *MemoryState, sender, recipient, coinbase common.Address, gasUsed uint64) { + t.Helper() + fee := new(big.Int).Mul(new(big.Int).SetUint64(gasUsed), big.NewInt(12)) + require.Equal(t, new(big.Int).Sub(new(big.Int).Sub(initialBalance, big.NewInt(17)), fee), state.GetBalance(sender)) + require.Equal(t, big.NewInt(17), state.GetBalance(recipient)) + require.Equal(t, fee, state.GetBalance(coinbase)) + }, + }, + { + name: "coinbase sender receives its own fee reward", + value: big.NewInt(23), + configureCtx: func(ctx *BlockContext, sender common.Address) { + ctx.Coinbase = sender + }, + sign: func(t *testing.T, key *ecdsa.PrivateKey, _ BlockContext, recipient common.Address) []byte { + t.Helper() + return signLegacyTxWithGasPrice(t, key, chainID, 0, &recipient, big.NewInt(23), nil, 100_000, big.NewInt(5)) + }, + wantEffective: big.NewInt(5), + assert: func(t *testing.T, state *MemoryState, sender, recipient, _ common.Address, _ uint64) { + t.Helper() + require.Equal(t, new(big.Int).Sub(initialBalance, big.NewInt(23)), state.GetBalance(sender)) + require.Equal(t, big.NewInt(23), state.GetBalance(recipient)) + }, + }, + } + + for i, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + recipient := common.BigToAddress(big.NewInt(int64(0xe0 + i))) + state := NewMemoryState() + state.SetBalance(sender, initialBalance) + ctx := blockContext(chainID) + if tc.configureCtx != nil { + tc.configureCtx(&ctx, sender) + } + rawTx := tc.sign(t, key, ctx, recipient) + cfg := Config{MinGasPrice: big.NewInt(0)} + + gethResult, err := executeGethReferenceBlock(t, state, cfg, ctx, [][]byte{rawTx}) + require.NoError(t, err) + execResult, err := NewExecutor(cfg, WithState(state)).ExecuteBlock(context.Background(), BlockRequest{ + Context: ctx, + Txs: [][]byte{rawTx}, + }) + require.NoError(t, err) + state.ApplyChangeSet(execResult.ChangeSet) + + requireExecutionParity(t, execResult, gethResult) + requireAddressParity(t, state, gethResult.state, sender) + requireAddressParity(t, state, gethResult.state, recipient) + requireAddressParity(t, state, gethResult.state, ctx.Coinbase) + require.Equal(t, tc.wantEffective, execResult.Txs[0].EffectiveGasPrice) + require.Equal(t, uint64(1), state.GetNonce(sender)) + tc.assert(t, state, sender, recipient, ctx.Coinbase, execResult.Txs[0].GasUsed) + }) + } +} + +func TestExecutorERC20StyleTransferMatchesGeth(t *testing.T) { + chainID := big.NewInt(713715) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + token := testAddress(0xd3) + from := testAddress(0xd4) + to := testAddress(0xd5) + fromSlot := common.BytesToHash(from.Bytes()) + toSlot := common.BytesToHash(to.Bytes()) + amount := byte(7) + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000_000_000)) + state.SetCode(token, erc20TransferRuntime(fromSlot, toSlot, from, to, amount)) + state.SetState(token, fromSlot, common.BigToHash(big.NewInt(1000))) + + rawTx := signLegacyTxWithGasPrice(t, key, chainID, 0, &token, big.NewInt(0), nil, 200_000, big.NewInt(1)) + cfg := Config{MinGasPrice: big.NewInt(0)} + ctx := blockContext(chainID) + + gethResult, err := executeGethReferenceBlock(t, state, cfg, ctx, [][]byte{rawTx}) + require.NoError(t, err) + execResult, err := NewExecutor(cfg, WithState(state)).ExecuteBlock(context.Background(), BlockRequest{ + Context: ctx, + Txs: [][]byte{rawTx}, + }) + require.NoError(t, err) + state.ApplyChangeSet(execResult.ChangeSet) + + requireExecutionParity(t, execResult, gethResult) + requireAddressParity(t, state, gethResult.state, token, fromSlot, toSlot) + require.Equal(t, common.BigToHash(big.NewInt(993)), state.GetState(token, fromSlot)) + require.Equal(t, common.BigToHash(big.NewInt(7)), state.GetState(token, toSlot)) + + require.Len(t, execResult.Receipts[0].Logs, 1) + log := execResult.Receipts[0].Logs[0] + require.Equal(t, token, log.Address) + require.Equal(t, []common.Hash{ + erc20TransferTopic(), + common.BytesToHash(from.Bytes()), + common.BytesToHash(to.Bytes()), + }, log.Topics) + require.Equal(t, common.LeftPadBytes([]byte{amount}, common.HashLength), log.Data) + require.Equal(t, ethtypes.ReceiptStatusSuccessful, execResult.Receipts[0].Status) +} + +func TestExecutorSelfDestructCreatedInSameTxMatchesGeth(t *testing.T) { + chainID := big.NewInt(713715) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + factory := testAddress(0xd6) + beneficiary := testAddress(0xd7) + child := crypto.CreateAddress(factory, 0) + endowment := byte(17) + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000_000_000)) + state.SetCode(factory, createAndDestroyChildRuntime(beneficiary, endowment)) + + rawTx := signLegacyTxWithGasPrice(t, key, chainID, 0, &factory, big.NewInt(int64(endowment)), nil, 500_000, big.NewInt(1)) + cfg := Config{MinGasPrice: big.NewInt(0)} + ctx := blockContext(chainID) + + gethResult, err := executeGethReferenceBlock(t, state, cfg, ctx, [][]byte{rawTx}) + require.NoError(t, err) + execResult, err := NewExecutor(cfg, WithState(state)).ExecuteBlock(context.Background(), BlockRequest{ + Context: ctx, + Txs: [][]byte{rawTx}, + }) + require.NoError(t, err) + state.ApplyChangeSet(execResult.ChangeSet) + + requireExecutionParity(t, execResult, gethResult) + requireAddressParity(t, state, gethResult.state, factory) + requireAddressParity(t, state, gethResult.state, child) + requireAddressParity(t, state, gethResult.state, beneficiary) + require.Empty(t, state.GetCode(child)) + require.Equal(t, uint64(0), state.GetNonce(child)) + require.Equal(t, big.NewInt(0), state.GetBalance(child)) + require.Equal(t, big.NewInt(int64(endowment)), state.GetBalance(beneficiary)) +} + +func TestExecutorPragueSelfDestructEdgeCasesMatchGeth(t *testing.T) { + chainID := big.NewInt(713715) + slot := testHash(0x47) + value := testHash(0x48) + contractBalance := big.NewInt(99) + + tests := []struct { + name string + beneficiary func(common.Address) common.Address + wantContractBalance *big.Int + wantBeneficiaryCredit *big.Int + }{ + { + name: "preexisting contract sends balance but keeps code and storage", + beneficiary: func(common.Address) common.Address { + return testAddress(0xdb) + }, + wantContractBalance: big.NewInt(0), + wantBeneficiaryCredit: contractBalance, + }, + { + name: "preexisting contract self beneficiary keeps balance code and storage", + beneficiary: func(contract common.Address) common.Address { + return contract + }, + wantContractBalance: contractBalance, + wantBeneficiaryCredit: contractBalance, + }, + } + + for i, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + contract := common.BigToAddress(big.NewInt(int64(0xf0 + i))) + beneficiary := tc.beneficiary(contract) + runtime := selfDestructCode(beneficiary) + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000_000_000)) + state.SetBalance(contract, contractBalance) + state.SetNonce(contract, 1) + state.SetCode(contract, runtime) + state.SetState(contract, slot, value) + + rawTx := signLegacyTxWithGasPrice(t, key, chainID, 0, &contract, big.NewInt(0), nil, 100_000, big.NewInt(1)) + cfg := Config{MinGasPrice: big.NewInt(0)} + ctx := blockContext(chainID) + + gethResult, err := executeGethReferenceBlock(t, state, cfg, ctx, [][]byte{rawTx}) + require.NoError(t, err) + execResult, err := NewExecutor(cfg, WithState(state)).ExecuteBlock(context.Background(), BlockRequest{ + Context: ctx, + Txs: [][]byte{rawTx}, + }) + require.NoError(t, err) + state.ApplyChangeSet(execResult.ChangeSet) + + requireExecutionParity(t, execResult, gethResult) + requireAddressParity(t, state, gethResult.state, sender) + requireAddressParity(t, state, gethResult.state, contract, slot) + requireAddressParity(t, state, gethResult.state, beneficiary) + require.Equal(t, runtime, state.GetCode(contract)) + require.Equal(t, uint64(1), state.GetNonce(contract)) + require.Equal(t, value, state.GetState(contract, slot)) + require.Equal(t, tc.wantContractBalance, state.GetBalance(contract)) + if beneficiary != contract { + require.Equal(t, tc.wantBeneficiaryCredit, state.GetBalance(beneficiary)) + } + }) + } +} + +func TestExecutorAccessListGasAccountingMatchesGeth(t *testing.T) { + chainID := big.NewInt(713715) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + contract := testAddress(0xd8) + slot := testHash(0x44) + cfg := Config{MinGasPrice: big.NewInt(0)} + ctx := blockContext(chainID) + + run := func(t *testing.T, accessList ethtypes.AccessList) (uint64, *BlockResult) { + t.Helper() + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000_000_000)) + state.SetCode(contract, sloadRuntime(slot)) + state.SetState(contract, slot, testHash(0x45)) + rawTx := signAccessListTx(t, key, chainID, 0, &contract, big.NewInt(0), nil, 120_000, big.NewInt(1), accessList) + + gethResult, err := executeGethReferenceBlock(t, state, cfg, ctx, [][]byte{rawTx}) + require.NoError(t, err) + execResult, err := NewExecutor(cfg, WithState(state)).ExecuteBlock(context.Background(), BlockRequest{ + Context: ctx, + Txs: [][]byte{rawTx}, + }) + require.NoError(t, err) + requireExecutionParity(t, execResult, gethResult) + return execResult.Txs[0].GasUsed, execResult + } + + coldGas, _ := run(t, nil) + tests := []struct { + name string + accessList ethtypes.AccessList + expectedDelta uint64 + }{ + { + name: "contract slot warmed", + accessList: ethtypes.AccessList{{ + Address: contract, + StorageKeys: []common.Hash{slot}, + }}, + expectedDelta: params.TxAccessListAddressGas + + params.TxAccessListStorageKeyGas - + (params.ColdSloadCostEIP2929 - params.WarmStorageReadCostEIP2929), + }, + { + name: "address only still pays intrinsic cost because destination is already warm", + accessList: ethtypes.AccessList{{ + Address: contract, + }}, + expectedDelta: params.TxAccessListAddressGas, + }, + { + name: "duplicate storage keys are charged twice but warm once", + accessList: ethtypes.AccessList{{ + Address: contract, + StorageKeys: []common.Hash{slot, slot}, + }}, + expectedDelta: params.TxAccessListAddressGas + + 2*params.TxAccessListStorageKeyGas - + (params.ColdSloadCostEIP2929 - params.WarmStorageReadCostEIP2929), + }, + { + name: "unrelated entry only adds intrinsic access list gas", + accessList: ethtypes.AccessList{{ + Address: testAddress(0xdc), + StorageKeys: []common.Hash{testHash(0x49)}, + }}, + expectedDelta: params.TxAccessListAddressGas + params.TxAccessListStorageKeyGas, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + gasUsed, execResult := run(t, tc.accessList) + + require.Equal(t, coldGas+tc.expectedDelta, gasUsed) + require.Equal(t, uint8(ethtypes.AccessListTxType), execResult.Receipts[0].Type) + }) + } +} + +func TestExecutorLogOpcodeCorpusMatchesGeth(t *testing.T) { + chainID := big.NewInt(713715) + data := testHash(0x60) + cfg := Config{MinGasPrice: big.NewInt(0)} + ctx := blockContext(chainID) + + for topicCount := 0; topicCount <= 4; topicCount++ { + t.Run(fmt.Sprintf("LOG%d", topicCount), func(t *testing.T) { + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + contract := common.BigToAddress(big.NewInt(int64(0x100 + topicCount))) + topics := make([]common.Hash, topicCount) + for i := range topics { + topics[i] = common.BigToHash(big.NewInt(int64(0x700 + i))) + } + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000_000_000)) + state.SetCode(contract, logRuntime(data, topics)) + rawTx := signLegacyTxWithGasPrice(t, key, chainID, 0, &contract, big.NewInt(0), nil, 200_000, big.NewInt(1)) + + gethResult, err := executeGethReferenceBlock(t, state, cfg, ctx, [][]byte{rawTx}) + require.NoError(t, err) + execResult, err := NewExecutor(cfg, WithState(state)).ExecuteBlock(context.Background(), BlockRequest{ + Context: ctx, + Txs: [][]byte{rawTx}, + }) + require.NoError(t, err) + state.ApplyChangeSet(execResult.ChangeSet) + + requireExecutionParity(t, execResult, gethResult) + requireAddressParity(t, state, gethResult.state, sender) + requireAddressParity(t, state, gethResult.state, contract) + require.Len(t, execResult.Receipts[0].Logs, 1) + log := execResult.Receipts[0].Logs[0] + require.Equal(t, contract, log.Address) + require.Equal(t, topics, log.Topics) + require.Equal(t, data.Bytes(), log.Data) + require.Equal(t, ethtypes.CreateBloom(execResult.Receipts[0]), execResult.Receipts[0].Bloom) + }) + } +} + +func TestExecutorCallOpcodeCorpusMatchesGeth(t *testing.T) { + chainID := big.NewInt(713715) + targetSlot := testHash(0x61) + successSlot := testHash(0x62) + targetValue := testHash(0x63) + cfg := Config{MinGasPrice: big.NewInt(0)} + ctx := blockContext(chainID) + + tests := []struct { + name string + op byte + wantSuccess common.Hash + wantCallerTarget common.Hash + wantTargetTarget common.Hash + wantTargetUnchanged bool + }{ + { + name: "CALL writes callee storage", + op: 0xf1, + wantSuccess: common.BigToHash(big.NewInt(1)), + wantTargetTarget: targetValue, + }, + { + name: "CALLCODE writes caller storage", + op: 0xf2, + wantSuccess: common.BigToHash(big.NewInt(1)), + wantCallerTarget: targetValue, + }, + { + name: "DELEGATECALL writes caller storage", + op: 0xf4, + wantSuccess: common.BigToHash(big.NewInt(1)), + wantCallerTarget: targetValue, + }, + { + name: "STATICCALL reports failure and blocks callee storage writes", + op: 0xfa, + wantSuccess: common.Hash{}, + wantTargetUnchanged: true, + }, + } + + for i, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + caller := common.BigToAddress(big.NewInt(int64(0x120 + i))) + target := common.BigToAddress(big.NewInt(int64(0x130 + i))) + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000_000_000)) + state.SetCode(caller, callOpcodeRuntime(tc.op, target, successSlot)) + state.SetCode(target, storeCode(targetSlot, targetValue)) + rawTx := signLegacyTxWithGasPrice(t, key, chainID, 0, &caller, big.NewInt(0), nil, 500_000, big.NewInt(1)) + + gethResult, err := executeGethReferenceBlock(t, state, cfg, ctx, [][]byte{rawTx}) + require.NoError(t, err) + execResult, err := NewExecutor(cfg, WithState(state)).ExecuteBlock(context.Background(), BlockRequest{ + Context: ctx, + Txs: [][]byte{rawTx}, + }) + require.NoError(t, err) + state.ApplyChangeSet(execResult.ChangeSet) + + requireExecutionParity(t, execResult, gethResult) + requireAddressParity(t, state, gethResult.state, sender) + requireAddressParity(t, state, gethResult.state, caller, targetSlot, successSlot) + requireAddressParity(t, state, gethResult.state, target, targetSlot) + require.Equal(t, tc.wantSuccess, state.GetState(caller, successSlot)) + require.Equal(t, tc.wantCallerTarget, state.GetState(caller, targetSlot)) + require.Equal(t, tc.wantTargetTarget, state.GetState(target, targetSlot)) + if tc.wantTargetUnchanged { + require.Equal(t, common.Hash{}, state.GetState(target, targetSlot)) + } + }) + } +} + +func TestExecutorEnvironmentOpcodeCorpusMatchesGeth(t *testing.T) { + chainID := big.NewInt(713715) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + contract := testAddress(0xdf) + contractBalance := big.NewInt(123) + slots := environmentSlots() + ctx := blockContext(chainID) + ctx.Number = 99 + ctx.Time = 12345 + ctx.GasLimit = 9_000_000 + ctx.BaseFee = big.NewInt(11) + ctx.BlobBaseFee = big.NewInt(13) + ctx.Coinbase = testAddress(0xe1) + ctx.ParentHash = testHash(0x64) + ctx.PrevRandao = testHash(0x65) + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000_000_000)) + state.SetBalance(contract, contractBalance) + state.SetCode(contract, environmentRuntime(ctx.Number-1, slots)) + rawTx := signLegacyTxWithGasPrice(t, key, chainID, 0, &contract, big.NewInt(0), nil, 500_000, big.NewInt(20)) + cfg := Config{MinGasPrice: big.NewInt(0)} + + gethResult, err := executeGethReferenceBlock(t, state, cfg, ctx, [][]byte{rawTx}) + require.NoError(t, err) + execResult, err := NewExecutor(cfg, WithState(state)).ExecuteBlock(context.Background(), BlockRequest{ + Context: ctx, + Txs: [][]byte{rawTx}, + }) + require.NoError(t, err) + state.ApplyChangeSet(execResult.ChangeSet) + + requireExecutionParity(t, execResult, gethResult) + requireAddressParity(t, state, gethResult.state, sender) + requireAddressParity(t, state, gethResult.state, contract, slots.all()...) + require.Equal(t, ctx.ParentHash, state.GetState(contract, slots.blockHash)) + require.Equal(t, common.BytesToHash(ctx.Coinbase.Bytes()), state.GetState(contract, slots.coinbase)) + require.Equal(t, common.BigToHash(new(big.Int).SetUint64(ctx.Time)), state.GetState(contract, slots.timestamp)) + require.Equal(t, common.BigToHash(new(big.Int).SetUint64(ctx.Number)), state.GetState(contract, slots.number)) + require.Equal(t, ctx.PrevRandao, state.GetState(contract, slots.prevRandao)) + require.Equal(t, common.BigToHash(new(big.Int).SetUint64(ctx.GasLimit)), state.GetState(contract, slots.gasLimit)) + require.Equal(t, common.BigToHash(chainID), state.GetState(contract, slots.chainID)) + require.Equal(t, common.BigToHash(contractBalance), state.GetState(contract, slots.selfBalance)) + require.Equal(t, common.BigToHash(ctx.BaseFee), state.GetState(contract, slots.baseFee)) + require.Equal(t, common.BigToHash(ctx.BlobBaseFee), state.GetState(contract, slots.blobBaseFee)) +} + +func TestExecutorVMFailureReceiptsAndFeesMatchGeth(t *testing.T) { + chainID := big.NewInt(713715) + contract := testAddress(0xdd) + coinbase := testAddress(0xde) + storageSlot := testHash(0x50) + + tests := []struct { + name string + code []byte + gasLimit uint64 + wantErr error + checkState func(t *testing.T, state *MemoryState) + }{ + { + name: "revert refunds unused gas and keeps receipt failure", + code: revertRuntime(), + gasLimit: 100_000, + wantErr: vm.ErrExecutionReverted, + }, + { + name: "invalid opcode consumes the transaction gas limit", + code: []byte{0xfe}, + gasLimit: 100_000, + checkState: func(t *testing.T, state *MemoryState) { + t.Helper() + require.Equal(t, big.NewInt(100_000), state.GetBalance(coinbase)) + }, + }, + { + name: "out of gas rolls back storage", + code: storeCode(storageSlot, testHash(0x51)), + gasLimit: 22_000, + wantErr: vm.ErrOutOfGas, + checkState: func(t *testing.T, state *MemoryState) { + t.Helper() + require.Equal(t, common.Hash{}, state.GetState(contract, storageSlot)) + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + initialBalance := big.NewInt(1_000_000_000) + state := NewMemoryState() + state.SetBalance(sender, initialBalance) + state.SetCode(contract, tc.code) + rawTx := signLegacyTxWithGasPrice(t, key, chainID, 0, &contract, big.NewInt(0), nil, tc.gasLimit, big.NewInt(1)) + cfg := Config{MinGasPrice: big.NewInt(0)} + ctx := blockContext(chainID) + ctx.Coinbase = coinbase + + gethResult, err := executeGethReferenceBlock(t, state, cfg, ctx, [][]byte{rawTx}) + require.NoError(t, err) + execResult, err := NewExecutor(cfg, WithState(state)).ExecuteBlock(context.Background(), BlockRequest{ + Context: ctx, + Txs: [][]byte{rawTx}, + }) + require.NoError(t, err) + state.ApplyChangeSet(execResult.ChangeSet) + + requireExecutionParity(t, execResult, gethResult) + requireAddressParity(t, state, gethResult.state, sender) + requireAddressParity(t, state, gethResult.state, contract, storageSlot) + requireAddressParity(t, state, gethResult.state, coinbase) + require.Equal(t, ethtypes.ReceiptStatusFailed, execResult.Receipts[0].Status) + require.Equal(t, ethtypes.ReceiptStatusFailed, execResult.Txs[0].Status) + if tc.wantErr != nil { + require.ErrorIs(t, execResult.Txs[0].Err, tc.wantErr) + } else { + require.Error(t, execResult.Txs[0].Err) + } + require.Equal(t, uint64(1), state.GetNonce(sender)) + fee := new(big.Int).SetUint64(execResult.Txs[0].GasUsed) + require.Equal(t, new(big.Int).Sub(initialBalance, fee), state.GetBalance(sender)) + require.Equal(t, fee, state.GetBalance(coinbase)) + require.Empty(t, execResult.Receipts[0].Logs) + if tc.checkState != nil { + tc.checkState(t, state) + } + }) + } +} + +func TestExecutorPreVMFailuresMatchGeth(t *testing.T) { + chainID := big.NewInt(713715) + recipient := testAddress(0xd9) + ctx := blockContext(chainID) + + tests := []struct { + name string + cfg Config + setup func(t *testing.T) (*MemoryState, []byte) + want error + }{ + { + name: "nonce too high", + setup: func(t *testing.T) (*MemoryState, []byte) { + t.Helper() + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000_000_000)) + return state, signLegacyTxWithGasPrice(t, key, chainID, 1, &recipient, big.NewInt(1), nil, 100_000, big.NewInt(1)) + }, + want: core.ErrNonceTooHigh, + }, + { + name: "nonce too low", + setup: func(t *testing.T) (*MemoryState, []byte) { + t.Helper() + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000_000_000)) + state.SetNonce(sender, 1) + return state, signLegacyTxWithGasPrice(t, key, chainID, 0, &recipient, big.NewInt(1), nil, 100_000, big.NewInt(1)) + }, + want: core.ErrNonceTooLow, + }, + { + name: "insufficient funds", + setup: func(t *testing.T) (*MemoryState, []byte) { + t.Helper() + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1)) + return state, signLegacyTxWithGasPrice(t, key, chainID, 0, &recipient, big.NewInt(1), nil, 100_000, big.NewInt(1)) + }, + want: core.ErrInsufficientFunds, + }, + { + name: "intrinsic gas too low", + setup: func(t *testing.T) (*MemoryState, []byte) { + t.Helper() + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000_000_000)) + return state, signLegacyTxWithGasPrice(t, key, chainID, 0, &recipient, big.NewInt(1), nil, 20_000, big.NewInt(1)) + }, + want: core.ErrIntrinsicGas, + }, + { + name: "fee cap below base fee", + cfg: Config{DisableGasPriceCheck: true}, + setup: func(t *testing.T) (*MemoryState, []byte) { + t.Helper() + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000_000_000)) + return state, signDynamicFeeTxWithFees(t, key, chainID, 0, &recipient, big.NewInt(1), nil, big.NewInt(1), big.NewInt(1), 100_000) + }, + want: core.ErrFeeCapTooLow, + }, + { + name: "tip cap above fee cap", + cfg: Config{DisableGasPriceCheck: true}, + setup: func(t *testing.T) (*MemoryState, []byte) { + t.Helper() + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000_000_000)) + return state, signDynamicFeeTxWithFees(t, key, chainID, 0, &recipient, big.NewInt(1), nil, big.NewInt(3), big.NewInt(2), 100_000) + }, + want: core.ErrTipAboveFeeCap, + }, + { + name: "sender has contract code", + setup: func(t *testing.T) (*MemoryState, []byte) { + t.Helper() + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000_000_000)) + state.SetCode(sender, []byte{0x00}) + return state, signLegacyTxWithGasPrice(t, key, chainID, 0, &recipient, big.NewInt(1), nil, 100_000, big.NewInt(1)) + }, + want: core.ErrSenderNoEOA, + }, + { + name: "sender nonce already max uint64", + setup: func(t *testing.T) (*MemoryState, []byte) { + t.Helper() + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000_000_000)) + state.SetNonce(sender, math.MaxUint64) + return state, signLegacyTxWithGasPrice(t, key, chainID, math.MaxUint64, &recipient, big.NewInt(1), nil, 100_000, big.NewInt(1)) + }, + want: core.ErrNonceMax, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + state, rawTx := tc.setup(t) + testCtx := ctx + if errors.Is(tc.want, core.ErrFeeCapTooLow) { + testCtx.BaseFee = big.NewInt(2) + } + cfg := tc.cfg + if cfg.MinGasPrice == nil { + cfg.MinGasPrice = big.NewInt(0) + } + + gethResult, gethErr := executeGethReferenceBlock(t, state, cfg, testCtx, [][]byte{rawTx}) + execResult, execErr := NewExecutor(cfg, WithState(state)).ExecuteBlock(context.Background(), BlockRequest{ + Context: testCtx, + Txs: [][]byte{rawTx}, + }) + + require.ErrorIs(t, gethErr, tc.want) + require.ErrorIs(t, execErr, tc.want) + require.Nil(t, gethResult) + require.Nil(t, execResult) + require.Equal(t, big.NewInt(0), state.GetBalance(recipient)) + }) + } +} + +func TestExecutorPrepareBlockRejectsBlobTransactions(t *testing.T) { + chainID := big.NewInt(713715) + key, err := crypto.GenerateKey() + require.NoError(t, err) + recipient := testAddress(0xda) + rawTx := signBlobTxWithFees( + t, + key, + chainID, + 0, + recipient, + big.NewInt(1), + nil, + big.NewInt(1), + big.NewInt(3), + big.NewInt(3), + 100_000, + []common.Hash{testHash(0x46)}, + ) + ctx := blockContext(chainID) + ctx.BlobBaseFee = big.NewInt(1) + + prepared, err := NewExecutor(Config{MinGasPrice: big.NewInt(0)}).PrepareBlock(context.Background(), BlockRequest{ + Context: ctx, + Txs: [][]byte{rawTx}, + }) + + require.ErrorIs(t, err, errUnsupportedBlobTx) + require.Empty(t, prepared.Txs) +} + +func TestExecutorOCCDeterministicAcrossRuns(t *testing.T) { + chainID := big.NewInt(713715) + txCount := 24 + rawTxs := make([][]byte, 0, txCount) + senders := make([]common.Address, 0, txCount) + recipients := make([]common.Address, 0, txCount) + + for i := range txCount { + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + recipient := common.BigToAddress(big.NewInt(int64(40_000 + i))) + senders = append(senders, sender) + recipients = append(recipients, recipient) + rawTxs = append(rawTxs, signLegacyTxWithGasPrice(t, key, chainID, 0, &recipient, big.NewInt(int64(i+1)), nil, 100_000, big.NewInt(1))) + } + + newState := func() *MemoryState { + state := NewMemoryState() + for _, sender := range senders { + state.SetBalance(sender, big.NewInt(1_000_000_000)) + } + return state + } + + var baseline *BlockResult + var baselineState *MemoryState + cfg := Config{MinGasPrice: big.NewInt(0), OCCWorkers: 4} + req := BlockRequest{Context: blockContext(chainID), Txs: rawTxs} + for iteration := 0; iteration < 8; iteration++ { + state := newState() + executor := NewExecutor(cfg, WithState(state)) + result, err := executor.ExecuteBlock(context.Background(), req) + executor.Close() + require.NoError(t, err) + require.True(t, result.OCCStats.Attempted) + require.False(t, result.OCCStats.Fallback) + require.Zero(t, result.OCCStats.ConflictCount) + state.ApplyChangeSet(result.ChangeSet) + + if iteration == 0 { + baseline = result + baselineState = state + continue + } + require.Equal(t, baseline.GasUsed, result.GasUsed) + require.Equal(t, baseline.Txs, result.Txs) + require.Equal(t, baseline.Receipts, result.Receipts) + require.Equal(t, baseline.ChangeSet, result.ChangeSet) + require.Equal(t, baseline.OCCStats, result.OCCStats) + for i := range txCount { + require.Equal(t, baselineState.GetBalance(senders[i]), state.GetBalance(senders[i])) + require.Equal(t, baselineState.GetBalance(recipients[i]), state.GetBalance(recipients[i])) + } + require.Equal(t, baselineState.GetBalance(req.Context.Coinbase), state.GetBalance(req.Context.Coinbase)) + } +} + +func executeGethReferenceBlock(t *testing.T, initial *MemoryState, cfg Config, ctx BlockContext, rawTxs [][]byte) (*gethReferenceResult, error) { + t.Helper() + chainConfig := chainConfigForTest(cfg, ctx) + if err := validateBlockContext(chainConfig, ctx); err != nil { + return nil, err + } + stateDB := newGethStateFromMemory(t, initial) + evm := vm.NewEVM(buildBlockContext(ctx), stateDB, chainConfig, vm.Config{}, nil) + gasLimit := ctx.GasLimit + if gasLimit == 0 { + gasLimit = math.MaxUint64 + } + gasPool := new(core.GasPool).AddGas(gasLimit) + baseFee := cloneOptionalBig(ctx.BaseFee) + signer := ethtypes.MakeSigner(chainConfig, new(big.Int).SetUint64(ctx.Number), ctx.Time) + result := &gethReferenceResult{} + + for txIndex, rawTx := range rawTxs { + var tx ethtypes.Transaction + if err := tx.UnmarshalBinary(rawTx); err != nil { + return nil, err + } + if err := validateSupportedTx(&tx); err != nil { + return nil, err + } + sender, err := ethtypes.Sender(signer, &tx) + if err != nil { + return nil, err + } + msg, err := core.TransactionToMessage(&tx, signer, baseFee) + if err != nil { + return nil, err + } + stateDB.SetTxContext(tx.Hash(), txIndex) + evm.SetTxContext(core.NewEVMTxContext(msg)) + execResult, err := core.ApplyMessage(evm, msg, gasPool) + if err != nil { + return nil, err + } + stateDB.Finalise(true) + + txIndexUint := uint(txIndex) + txLogs := stateDB.GetLogs(tx.Hash(), ctx.Number, ctx.BlockHash) + status := ethtypes.ReceiptStatusSuccessful + if execResult.Failed() { + status = ethtypes.ReceiptStatusFailed + } + receipt := ðtypes.Receipt{ + Type: tx.Type(), + Status: status, + Logs: txLogs, + TxHash: tx.Hash(), + GasUsed: execResult.UsedGas, + EffectiveGasPrice: effectiveGasPrice(&tx, baseFee), + BlockHash: ctx.BlockHash, + BlockNumber: new(big.Int).SetUint64(ctx.Number), + TransactionIndex: txIndexUint, + } + if tx.To() == nil { + receipt.ContractAddress = crypto.CreateAddress(sender, tx.Nonce()) + } + receipt.Bloom = ethtypes.CreateBloom(receipt) + + result.gasUsed += execResult.UsedGas + receipt.CumulativeGasUsed = result.gasUsed + result.txs = append(result.txs, TxResult{ + Hash: tx.Hash(), + Sender: sender, + To: tx.To(), + ContractAddress: receipt.ContractAddress, + Status: status, + GasUsed: execResult.UsedGas, + CumulativeGasUsed: result.gasUsed, + EffectiveGasPrice: new(big.Int).Set(receipt.EffectiveGasPrice), + Logs: txLogs, + Err: execResult.Err, + }) + result.receipts = append(result.receipts, receipt) + } + result.state = stateDB + return result, nil +} + +func newGethStateFromMemory(t *testing.T, initial *MemoryState) *gethstate.StateDB { + t.Helper() + db := gethstate.NewDatabase(triedb.NewDatabase(rawdb.NewMemoryDatabase(), nil), nil) + seed, err := gethstate.New(ethtypes.EmptyRootHash, db) + require.NoError(t, err) + if initial != nil { + initial.mu.RLock() + for addr, acct := range initial.accounts { + if acct.Balance != nil { + seed.SetBalance(addr, uint256.MustFromBig(acct.Balance), tracing.BalanceChangeUnspecified) + } + if acct.Nonce != 0 { + seed.SetNonce(addr, acct.Nonce, tracing.NonceChangeUnspecified) + } + if len(acct.Code) != 0 { + seed.SetCode(addr, acct.Code) + } + for key, value := range acct.Storage { + seed.SetState(addr, key, value) + } + } + initial.mu.RUnlock() + } + root, err := seed.Commit(0, true, false) + require.NoError(t, err) + stateDB, err := gethstate.New(root, db) + require.NoError(t, err) + return stateDB +} + +func chainConfigForTest(cfg Config, ctx BlockContext) *params.ChainConfig { + executor := &Executor{cfg: cfg.WithDefaults()} + return executor.chainConfig(ctx) +} + +func requireExecutionParity(t *testing.T, execResult *BlockResult, gethResult *gethReferenceResult) { + t.Helper() + require.Equal(t, gethResult.gasUsed, execResult.GasUsed) + require.Len(t, execResult.Txs, len(gethResult.txs)) + require.Len(t, execResult.Receipts, len(gethResult.receipts)) + for i := range gethResult.txs { + require.Equal(t, gethResult.txs[i], execResult.Txs[i]) + require.Equal(t, gethResult.receipts[i].Type, execResult.Receipts[i].Type) + require.Equal(t, gethResult.receipts[i].Status, execResult.Receipts[i].Status) + require.Equal(t, gethResult.receipts[i].CumulativeGasUsed, execResult.Receipts[i].CumulativeGasUsed) + require.Equal(t, gethResult.receipts[i].Bloom, execResult.Receipts[i].Bloom) + require.Equal(t, gethResult.receipts[i].Logs, execResult.Receipts[i].Logs) + require.Equal(t, gethResult.receipts[i].TxHash, execResult.Receipts[i].TxHash) + require.Equal(t, gethResult.receipts[i].ContractAddress, execResult.Receipts[i].ContractAddress) + require.Equal(t, gethResult.receipts[i].GasUsed, execResult.Receipts[i].GasUsed) + require.Equal(t, gethResult.receipts[i].EffectiveGasPrice, execResult.Receipts[i].EffectiveGasPrice) + require.Equal(t, gethResult.receipts[i].BlockHash, execResult.Receipts[i].BlockHash) + require.Equal(t, gethResult.receipts[i].BlockNumber, execResult.Receipts[i].BlockNumber) + require.Equal(t, gethResult.receipts[i].TransactionIndex, execResult.Receipts[i].TransactionIndex) + } +} + +func requireAddressParity(t *testing.T, state *MemoryState, gethState *gethstate.StateDB, addr common.Address, slots ...common.Hash) { + t.Helper() + require.Zero(t, gethState.GetBalance(addr).ToBig().Cmp(state.GetBalance(addr))) + require.Equal(t, gethState.GetNonce(addr), state.GetNonce(addr)) + require.Equal(t, gethState.GetCode(addr), state.GetCode(addr)) + for _, slot := range slots { + require.Equal(t, gethState.GetState(addr, slot), state.GetState(addr, slot)) + } +} + +func signAccessListTx( + t *testing.T, + key *ecdsa.PrivateKey, + chainID *big.Int, + nonce uint64, + to *common.Address, + value *big.Int, + data []byte, + gas uint64, + gasPrice *big.Int, + accessList ethtypes.AccessList, +) []byte { + t.Helper() + tx := ethtypes.NewTx(ðtypes.AccessListTx{ + ChainID: chainID, + Nonce: nonce, + GasPrice: new(big.Int).Set(gasPrice), + Gas: gas, + To: to, + Value: value, + Data: data, + AccessList: accessList, + }) + signed, err := ethtypes.SignTx(tx, ethtypes.LatestSignerForChainID(chainID), key) + require.NoError(t, err) + raw, err := signed.MarshalBinary() + require.NoError(t, err) + return raw +} + +func sloadRuntime(slot common.Hash) []byte { + code := appendPush32(nil, slot) + return append(code, 0x54, 0x00) +} + +func revertRuntime() []byte { + return []byte{0x60, 0x00, 0x60, 0x00, 0xfd} +} + +func logRuntime(data common.Hash, topics []common.Hash) []byte { + code := appendPush32(nil, data) + code = appendPush1(code, 0) + code = append(code, 0x52) + for i := len(topics) - 1; i >= 0; i-- { + code = appendPush32(code, topics[i]) + } + code = appendPush1(code, common.HashLength) + code = appendPush1(code, 0) + return append(code, 0xa0+byte(len(topics)), 0x00) +} + +func callOpcodeRuntime(op byte, target common.Address, successSlot common.Hash) []byte { + code := appendPush1(nil, 0) + code = appendPush1(code, 0) + code = appendPush1(code, 0) + code = appendPush1(code, 0) + if op == 0xf1 || op == 0xf2 { + code = appendPush1(code, 0) + } + code = appendPush20(code, target) + code = appendPush2(code, 0xffff) + code = append(code, op) + code = appendPush32(code, successSlot) + return append(code, 0x55, 0x00) +} + +type envOpcodeSlots struct { + blockHash common.Hash + coinbase common.Hash + timestamp common.Hash + number common.Hash + prevRandao common.Hash + gasLimit common.Hash + chainID common.Hash + selfBalance common.Hash + baseFee common.Hash + blobBaseFee common.Hash +} + +func environmentSlots() envOpcodeSlots { + return envOpcodeSlots{ + blockHash: testHash(0x70), + coinbase: testHash(0x71), + timestamp: testHash(0x72), + number: testHash(0x73), + prevRandao: testHash(0x74), + gasLimit: testHash(0x75), + chainID: testHash(0x76), + selfBalance: testHash(0x77), + baseFee: testHash(0x78), + blobBaseFee: testHash(0x79), + } +} + +func (s envOpcodeSlots) all() []common.Hash { + return []common.Hash{ + s.blockHash, + s.coinbase, + s.timestamp, + s.number, + s.prevRandao, + s.gasLimit, + s.chainID, + s.selfBalance, + s.baseFee, + s.blobBaseFee, + } +} + +func environmentRuntime(blockNumberForHash uint64, slots envOpcodeSlots) []byte { + code := appendPushUint64(nil, blockNumberForHash) + code = append(code, 0x40) + code = appendStoreTop(code, slots.blockHash) + for _, op := range []struct { + op byte + slot common.Hash + }{ + {op: 0x41, slot: slots.coinbase}, + {op: 0x42, slot: slots.timestamp}, + {op: 0x43, slot: slots.number}, + {op: 0x44, slot: slots.prevRandao}, + {op: 0x45, slot: slots.gasLimit}, + {op: 0x46, slot: slots.chainID}, + {op: 0x47, slot: slots.selfBalance}, + {op: 0x48, slot: slots.baseFee}, + {op: 0x4a, slot: slots.blobBaseFee}, + } { + code = append(code, op.op) + code = appendStoreTop(code, op.slot) + } + return append(code, 0x00) +} + +func erc20TransferRuntime(fromSlot, toSlot common.Hash, from, to common.Address, amount byte) []byte { + code := appendPush32(nil, fromSlot) + code = append(code, 0x54) + code = appendPush1(code, amount) + code = append(code, 0x90, 0x03) + code = appendPush32(code, fromSlot) + code = append(code, 0x55) + + code = appendPush32(code, toSlot) + code = append(code, 0x54) + code = appendPush1(code, amount) + code = append(code, 0x01) + code = appendPush32(code, toSlot) + code = append(code, 0x55) + + code = appendPush1(code, amount) + code = appendPush1(code, 0) + code = append(code, 0x52) + code = appendPush20(code, to) + code = appendPush20(code, from) + code = appendPush32(code, erc20TransferTopic()) + code = appendPush1(code, common.HashLength) + code = appendPush1(code, 0) + return append(code, 0xa3, 0x00) +} + +func createAndDestroyChildRuntime(beneficiary common.Address, endowment byte) []byte { + childInit := initCode(selfDestructCode(beneficiary)) + if len(childInit) > math.MaxUint8 { + panic("child init code too large for test runtime") + } + code := []byte{ + 0x60, byte(len(childInit)), + 0x60, 0x00, + 0x60, 0x00, + 0x39, + 0x60, byte(len(childInit)), + 0x60, 0x00, + 0x60, endowment, + 0xf0, + 0x60, 0x00, + 0x60, 0x00, + 0x60, 0x00, + 0x60, 0x00, + 0x60, 0x00, + 0x85, + 0x61, 0xff, 0xff, + 0xf1, + 0x00, + } + code[3] = byte(len(code)) + return append(code, childInit...) +} + +func erc20TransferTopic() common.Hash { + return crypto.Keccak256Hash([]byte("Transfer(address,address,uint256)")) +} + +func appendPush1(code []byte, value byte) []byte { + return append(code, 0x60, value) +} + +func appendPush2(code []byte, value uint16) []byte { + return append(code, 0x61, byte(value>>8), byte(value)) +} + +func appendPush20(code []byte, value common.Address) []byte { + code = append(code, 0x73) + return append(code, value.Bytes()...) +} + +func appendPush32(code []byte, value common.Hash) []byte { + code = append(code, 0x7f) + return append(code, value.Bytes()...) +} + +func appendPushUint64(code []byte, value uint64) []byte { + return appendPush32(code, common.BigToHash(new(big.Int).SetUint64(value))) +} + +func appendStoreTop(code []byte, slot common.Hash) []byte { + code = appendPush32(code, slot) + return append(code, 0x55) +} diff --git a/giga/evmonly/executor_test.go b/giga/evmonly/executor_test.go new file mode 100644 index 0000000000..d9087d55ed --- /dev/null +++ b/giga/evmonly/executor_test.go @@ -0,0 +1,1480 @@ +package evmonly + +import ( + "context" + "crypto/ecdsa" + "errors" + "math/big" + "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/holiman/uint256" + "github.com/stretchr/testify/require" + + "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles" +) + +const testGasPriceWei = 1_000_000_000 + +type recordingResultSink struct { + heights []uint64 + results []*BlockResult + releases []func() +} + +func (s *recordingResultSink) StoreBlockResult(_ context.Context, height uint64, result *BlockResult, release func()) error { + s.heights = append(s.heights, height) + s.results = append(s.results, result) + s.releases = append(s.releases, release) + return nil +} + +func TestExecutorEmptyBlock(t *testing.T) { + executor := NewExecutor(Config{}) + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: blockContext(big.NewInt(713715)), + }) + + require.NoError(t, err) + require.NotNil(t, result) +} + +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 := 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(), 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 TestExecutorInvokesResultSink(t *testing.T) { + chainID := big.NewInt(713715) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + recipient := common.HexToAddress("0x00000000000000000000000000000000000000a7") + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(200_000_000_000_000)) + sink := &recordingResultSink{} + + rawTx := signLegacyTx(t, key, chainID, 0, &recipient, big.NewInt(7), nil) + executor := NewExecutor(Config{}, WithState(state), WithResultSink(sink)) + ctx := blockContext(chainID) + ctx.Number = 77 + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: ctx, + Txs: [][]byte{rawTx}, + }) + + require.NoError(t, err) + require.Len(t, sink.results, 1) + require.Len(t, sink.releases, 1) + require.Equal(t, []uint64{ctx.Number}, sink.heights) + require.Same(t, result, sink.results[0]) + sink.releases[0]() +} + +func TestExecutorPooledResultRelease(t *testing.T) { + chainID := big.NewInt(713715) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + recipient := common.HexToAddress("0x00000000000000000000000000000000000000a8") + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(200_000_000_000_000)) + sink := &recordingResultSink{} + executor := NewExecutor(Config{BlockResultPoolSize: 1}, WithState(state), WithResultSink(sink)) + rawTx := signLegacyTx(t, key, chainID, 0, &recipient, big.NewInt(7), nil) + req := BlockRequest{Context: blockContext(chainID), Txs: [][]byte{rawTx}} + + first, err := executor.ExecuteBlock(context.Background(), req) + require.NoError(t, err) + require.Same(t, first, sink.results[0]) + require.NotNil(t, sink.releases[0]) + sink.releases[0]() + first.Release() + + second, err := executor.ExecuteBlock(context.Background(), req) + require.NoError(t, err) + require.Same(t, first, second) + sink.releases[1]() + second.Release() +} + +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 TestExecutorRejectsBlobTxUntilBlockAccountingIsWired(t *testing.T) { + chainID := big.NewInt(713715) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + recipient := testAddress(0xb5) + blobBaseFee := big.NewInt(3) + blobHash := common.Hash{0x01} + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000_000_000_000)) + rawTx := signBlobTxWithFees( + t, + key, + chainID, + 0, + recipient, + big.NewInt(1), + nil, + big.NewInt(1), + big.NewInt(3), + big.NewInt(3), + 100_000, + []common.Hash{blobHash}, + ) + ctx := blockContext(chainID) + ctx.BaseFee = big.NewInt(2) + ctx.BlobBaseFee = blobBaseFee + + executor := NewExecutor(Config{MinGasPrice: big.NewInt(0)}, WithState(state)) + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: ctx, + Txs: [][]byte{rawTx}, + }) + + require.ErrorIs(t, err, errUnsupportedBlobTx) + require.Nil(t, result) + require.Equal(t, big.NewInt(0), state.GetBalance(recipient)) + + tx, sender, err := parseTx(rawTx, ethtypes.LatestSignerForChainID(chainID)) + require.NoError(t, err) + result, err = executor.ExecutePreparedBlock(context.Background(), PreparedBlock{ + Context: ctx, + Txs: []PreparedTx{{ + Tx: tx, + Sender: sender, + }}, + }) + + require.ErrorIs(t, err, errUnsupportedBlobTx) + require.Nil(t, result) + require.Equal(t, big.NewInt(0), state.GetBalance(recipient)) +} + +func TestExecutorRequiresBaseFeeAfterLondon(t *testing.T) { + chainID := big.NewInt(713715) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + recipient := testAddress(0xb4) + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(200_000_000_000_000)) + rawTx := signLegacyTx(t, key, chainID, 0, &recipient, big.NewInt(1), nil) + ctx := blockContext(chainID) + ctx.BaseFee = nil + + result, err := NewExecutor(Config{}, WithState(state)).ExecuteBlock(context.Background(), BlockRequest{ + Context: ctx, + Txs: [][]byte{rawTx}, + }) + + require.ErrorIs(t, err, errMissingBaseFee) + require.Nil(t, result) + require.Equal(t, big.NewInt(0), state.GetBalance(recipient)) +} + +func TestExecutorOCCNonConflictingTransfersMatchSequential(t *testing.T) { + chainID := big.NewInt(713715) + txCount := 12 + rawTxs := make([][]byte, 0, txCount) + senders := make([]common.Address, 0, txCount) + recipients := make([]common.Address, 0, txCount) + seqState := NewMemoryState() + occState := NewMemoryState() + + for i := 0; i < txCount; i++ { + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + recipient := common.BigToAddress(big.NewInt(int64(10_000 + i))) + senders = append(senders, sender) + recipients = append(recipients, recipient) + seqState.SetBalance(sender, big.NewInt(1_000_000)) + occState.SetBalance(sender, big.NewInt(1_000_000)) + rawTxs = append(rawTxs, signLegacyTxWithGasPrice(t, key, chainID, 0, &recipient, big.NewInt(7), nil, 100_000, big.NewInt(0))) + } + + cfg := Config{MinGasPrice: big.NewInt(0)} + seqExecutor := NewExecutor(cfg, WithState(seqState)) + occExecutor := NewExecutor(Config{MinGasPrice: big.NewInt(0), OCCWorkers: 4}, WithState(occState)) + req := BlockRequest{Context: blockContext(chainID), Txs: rawTxs} + + seqResult, err := seqExecutor.ExecuteBlock(context.Background(), req) + require.NoError(t, err) + occResult, err := occExecutor.ExecuteBlock(context.Background(), req) + require.NoError(t, err) + + require.Equal(t, seqResult.GasUsed, occResult.GasUsed) + require.Len(t, occResult.Txs, txCount) + require.Len(t, occResult.Receipts, txCount) + require.True(t, occResult.OCCStats.Attempted) + require.False(t, occResult.OCCStats.Fallback) + require.Zero(t, occResult.OCCStats.ConflictCount) + for i := range txCount { + require.Equal(t, seqResult.Txs[i].Hash, occResult.Txs[i].Hash) + require.Equal(t, seqResult.Txs[i].Status, occResult.Txs[i].Status) + require.Equal(t, seqResult.Receipts[i].CumulativeGasUsed, occResult.Receipts[i].CumulativeGasUsed) + } + + seqState.ApplyChangeSet(seqResult.ChangeSet) + occState.ApplyChangeSet(occResult.ChangeSet) + for i := range txCount { + require.Equal(t, seqState.GetBalance(senders[i]), occState.GetBalance(senders[i])) + require.Equal(t, seqState.GetNonce(senders[i]), occState.GetNonce(senders[i])) + require.Equal(t, seqState.GetBalance(recipients[i]), occState.GetBalance(recipients[i])) + } +} + +func TestExecutorOCCConflictingTransfersMatchSequential(t *testing.T) { + chainID := big.NewInt(713715) + txCount := 8 + recipient := testAddress(0xdd) + rawTxs := make([][]byte, 0, txCount) + seqState := NewMemoryState() + occState := NewMemoryState() + + for i := 0; i < txCount; i++ { + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + seqState.SetBalance(sender, big.NewInt(1_000_000)) + occState.SetBalance(sender, big.NewInt(1_000_000)) + rawTxs = append(rawTxs, signLegacyTxWithGasPrice(t, key, chainID, 0, &recipient, big.NewInt(3), nil, 100_000, big.NewInt(0))) + } + + req := BlockRequest{Context: blockContext(chainID), Txs: rawTxs} + seqResult, err := NewExecutor(Config{MinGasPrice: big.NewInt(0)}, WithState(seqState)).ExecuteBlock(context.Background(), req) + require.NoError(t, err) + occResult, err := NewExecutor(Config{MinGasPrice: big.NewInt(0), OCCWorkers: 4}, WithState(occState)).ExecuteBlock(context.Background(), req) + require.NoError(t, err) + + seqState.ApplyChangeSet(seqResult.ChangeSet) + occState.ApplyChangeSet(occResult.ChangeSet) + require.Equal(t, seqResult.GasUsed, occResult.GasUsed) + require.True(t, occResult.OCCStats.Attempted) + require.True(t, occResult.OCCStats.Fallback) + require.Equal(t, "conflict", occResult.OCCStats.FallbackReason) + require.Greater(t, occResult.OCCStats.ConflictCount, uint64(0)) + require.NotEmpty(t, occResult.OCCStats.ConflictSamples) + foundRecipientBalanceConflict := false + for _, conflict := range occResult.OCCStats.ConflictSamples { + if conflict.Kind == "balance" && conflict.Address == recipient { + foundRecipientBalanceConflict = true + require.Greater(t, conflict.Count, uint64(0)) + } + } + require.True(t, foundRecipientBalanceConflict) + require.Equal(t, seqState.GetBalance(recipient), occState.GetBalance(recipient)) + require.Equal(t, big.NewInt(int64(txCount*3)), occState.GetBalance(recipient)) +} + +func TestExecutorOCCFeePayingTransfersDoNotConflictOnCoinbase(t *testing.T) { + chainID := big.NewInt(713715) + txCount := 4 + rawTxs := make([][]byte, 0, txCount) + senders := make([]common.Address, 0, txCount) + recipients := make([]common.Address, 0, txCount) + seqState := NewMemoryState() + occState := NewMemoryState() + + for i := 0; i < txCount; i++ { + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + recipient := common.BigToAddress(big.NewInt(int64(30_000 + i))) + senders = append(senders, sender) + recipients = append(recipients, recipient) + seqState.SetBalance(sender, big.NewInt(1_000_000_000)) + occState.SetBalance(sender, big.NewInt(1_000_000_000)) + rawTxs = append(rawTxs, signLegacyTxWithGasPrice(t, key, chainID, 0, &recipient, big.NewInt(7), nil, 100_000, big.NewInt(1))) + } + + cfg := Config{MinGasPrice: big.NewInt(0)} + req := BlockRequest{Context: blockContext(chainID), Txs: rawTxs} + seqResult, err := NewExecutor(cfg, WithState(seqState)).ExecuteBlock(context.Background(), req) + require.NoError(t, err) + occResult, err := NewExecutor(Config{MinGasPrice: big.NewInt(0), OCCWorkers: 4}, WithState(occState)).ExecuteBlock(context.Background(), req) + require.NoError(t, err) + + require.True(t, occResult.OCCStats.Attempted) + require.False(t, occResult.OCCStats.Fallback) + require.Equal(t, seqResult.GasUsed, occResult.GasUsed) + + seqState.ApplyChangeSet(seqResult.ChangeSet) + occState.ApplyChangeSet(occResult.ChangeSet) + require.Equal(t, seqState.GetBalance(req.Context.Coinbase), occState.GetBalance(req.Context.Coinbase)) + for i := range txCount { + require.Equal(t, seqState.GetBalance(senders[i]), occState.GetBalance(senders[i])) + require.Equal(t, seqState.GetBalance(recipients[i]), occState.GetBalance(recipients[i])) + } +} + +func TestExecutorOCCFallsBackWhenLaterTxReadsFeeCreditedCoinbase(t *testing.T) { + chainID := big.NewInt(713715) + feePayerKey, err := crypto.GenerateKey() + require.NoError(t, err) + readerKey, err := crypto.GenerateKey() + require.NoError(t, err) + feePayer := crypto.PubkeyToAddress(feePayerKey.PublicKey) + reader := crypto.PubkeyToAddress(readerKey.PublicKey) + recipient := testAddress(0xbe) + contract := testAddress(0xcf) + storageKey := testHash(0x0b) + + seqState := NewMemoryState() + occState := NewMemoryState() + for _, state := range []*MemoryState{seqState, occState} { + state.SetBalance(feePayer, big.NewInt(1_000_000_000)) + state.SetBalance(reader, big.NewInt(1_000_000_000)) + state.SetCode(contract, balanceStoreCode(blockContext(chainID).Coinbase, storageKey)) + } + + feeTx := signLegacyTxWithGasPrice(t, feePayerKey, chainID, 0, &recipient, big.NewInt(1), nil, 100_000, big.NewInt(1)) + readCoinbaseTx := signLegacyTxWithGasPrice(t, readerKey, chainID, 0, &contract, big.NewInt(0), nil, 100_000, big.NewInt(0)) + req := BlockRequest{Context: blockContext(chainID), Txs: [][]byte{feeTx, readCoinbaseTx}} + seqResult, err := NewExecutor(Config{MinGasPrice: big.NewInt(0)}, WithState(seqState)).ExecuteBlock(context.Background(), req) + require.NoError(t, err) + occResult, err := NewExecutor(Config{MinGasPrice: big.NewInt(0), OCCWorkers: 2}, WithState(occState)).ExecuteBlock(context.Background(), req) + require.NoError(t, err) + + require.True(t, occResult.OCCStats.Attempted) + require.True(t, occResult.OCCStats.Fallback) + require.Equal(t, occFallbackReasonConflict, occResult.OCCStats.FallbackReason) + foundCoinbaseBalanceRead := false + for _, conflict := range occResult.OCCStats.ConflictSamples { + if conflict.Access == "read" && conflict.Kind == "balance" && conflict.Address == req.Context.Coinbase { + foundCoinbaseBalanceRead = true + } + } + require.True(t, foundCoinbaseBalanceRead) + + seqState.ApplyChangeSet(seqResult.ChangeSet) + occState.ApplyChangeSet(occResult.ChangeSet) + require.Equal(t, seqState.GetState(contract, storageKey), occState.GetState(contract, storageKey)) + require.Equal(t, common.BigToHash(big.NewInt(21_000)), occState.GetState(contract, storageKey)) +} + +func TestExecutorOCCMergesCoinbaseSenderFeeWithoutDoubleCount(t *testing.T) { + chainID := big.NewInt(713715) + coinbaseKey, err := crypto.GenerateKey() + require.NoError(t, err) + otherKey, err := crypto.GenerateKey() + require.NoError(t, err) + coinbase := crypto.PubkeyToAddress(coinbaseKey.PublicKey) + other := crypto.PubkeyToAddress(otherKey.PublicKey) + coinbaseRecipient := testAddress(0xba) + otherRecipient := testAddress(0xbb) + initialCoinbaseBalance := big.NewInt(1_000_000_000) + + seqState := NewMemoryState() + occState := NewMemoryState() + for _, state := range []*MemoryState{seqState, occState} { + state.SetBalance(coinbase, initialCoinbaseBalance) + state.SetBalance(other, big.NewInt(1_000_000_000)) + } + + coinbaseTx := signLegacyTxWithGasPrice(t, coinbaseKey, chainID, 0, &coinbaseRecipient, big.NewInt(0), nil, 100_000, big.NewInt(1)) + otherTx := signLegacyTxWithGasPrice(t, otherKey, chainID, 0, &otherRecipient, big.NewInt(0), nil, 100_000, big.NewInt(1)) + ctx := blockContext(chainID) + ctx.Coinbase = coinbase + req := BlockRequest{Context: ctx, Txs: [][]byte{coinbaseTx, otherTx}} + seqResult, err := NewExecutor(Config{MinGasPrice: big.NewInt(0)}, WithState(seqState)).ExecuteBlock(context.Background(), req) + require.NoError(t, err) + occResult, err := NewExecutor(Config{MinGasPrice: big.NewInt(0), OCCWorkers: 2}, WithState(occState)).ExecuteBlock(context.Background(), req) + require.NoError(t, err) + + require.True(t, occResult.OCCStats.Attempted) + require.False(t, occResult.OCCStats.Fallback) + + seqState.ApplyChangeSet(seqResult.ChangeSet) + occState.ApplyChangeSet(occResult.ChangeSet) + require.Equal(t, seqState.GetBalance(coinbase), occState.GetBalance(coinbase)) + require.Equal(t, new(big.Int).Add(initialCoinbaseBalance, big.NewInt(21_000)), occState.GetBalance(coinbase)) +} + +func TestExecutorOCCSpeculativeErrorFallsBackToSequential(t *testing.T) { + chainID := big.NewInt(713715) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + firstRecipient := testAddress(0xb6) + secondRecipient := testAddress(0xb7) + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000)) + firstTx := signLegacyTxWithGasPrice(t, key, chainID, 0, &firstRecipient, big.NewInt(1), nil, 100_000, big.NewInt(0)) + secondTx := signLegacyTxWithGasPrice(t, key, chainID, 1, &secondRecipient, big.NewInt(1), nil, 100_000, big.NewInt(0)) + executor := NewExecutor(Config{MinGasPrice: big.NewInt(0), OCCWorkers: 2}, WithState(state)) + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: blockContext(chainID), + Txs: [][]byte{firstTx, secondTx}, + }) + + require.NoError(t, err) + require.True(t, result.OCCStats.Attempted) + require.True(t, result.OCCStats.Fallback) + require.Equal(t, occFallbackReasonSpeculativeError, result.OCCStats.FallbackReason) + + state.ApplyChangeSet(result.ChangeSet) + require.Equal(t, uint64(2), state.GetNonce(sender)) + require.Equal(t, big.NewInt(1), state.GetBalance(firstRecipient)) + require.Equal(t, big.NewInt(1), state.GetBalance(secondRecipient)) +} + +func TestExecutorOCCFallsBackWhenDeclaredGasExceedsBlockLimit(t *testing.T) { + chainID := big.NewInt(713715) + rawTxs := make([][]byte, 0, 2) + state := NewMemoryState() + + for i := 0; i < 2; i++ { + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + recipient := common.BigToAddress(big.NewInt(int64(20_000 + i))) + state.SetBalance(sender, big.NewInt(1_000_000)) + rawTxs = append(rawTxs, signLegacyTxWithGasPrice(t, key, chainID, 0, &recipient, big.NewInt(1), nil, 90_000, big.NewInt(0))) + } + + ctx := blockContext(chainID) + ctx.GasLimit = 100_000 + executor := NewExecutor(Config{MinGasPrice: big.NewInt(0), OCCWorkers: 2}, WithState(state)) + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: ctx, + Txs: rawTxs, + }) + + require.Error(t, err) + require.True(t, errors.Is(err, core.ErrGasLimitReached)) + require.Nil(t, result) +} + +func TestExecutorReceiptAndLogMetadata(t *testing.T) { + chainID := big.NewInt(713715) + key, err := crypto.GenerateKey() + 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("nonce too high", 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("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) + 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)) + }) + + 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) { + 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, 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() + 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 TestStateDBSelfDestructEmitsStorageClear(t *testing.T) { + contract := testAddress(0xc4) + loadedKey := testHash(0x01) + unreadKey := testHash(0x02) + loadedValue := testHash(0x11) + unreadValue := testHash(0x22) + + state := NewMemoryState() + state.SetCode(contract, []byte{0x60, 0x00, 0x00}) + state.SetState(contract, loadedKey, loadedValue) + state.SetState(contract, unreadKey, unreadValue) + stateDB := newNativeStateDB(state) + + require.Equal(t, loadedValue, stateDB.GetState(contract, loadedKey)) + stateDB.SelfDestruct(contract) + stateDB.Finalise(true) + + changes := stateDB.ChangeSet() + require.Contains(t, changes.StorageClears, contract) + state.ApplyChangeSet(changes) + require.Empty(t, state.GetCode(contract)) + require.Equal(t, common.Hash{}, state.GetState(contract, loadedKey)) + require.Equal(t, common.Hash{}, state.GetState(contract, unreadKey)) +} + +func TestStateDBCreateAccountPreservesStorageClear(t *testing.T) { + contract := testAddress(0xc6) + unreadKey := testHash(0x02) + unreadValue := testHash(0x22) + + state := NewMemoryState() + state.SetCode(contract, []byte{0x60, 0x00, 0x00}) + state.SetState(contract, unreadKey, unreadValue) + stateDB := newNativeStateDB(state) + + stateDB.SelfDestruct(contract) + stateDB.Finalise(true) + stateDB.CreateAccount(contract) + stateDB.Finalise(true) + + changes := stateDB.ChangeSet() + require.Contains(t, changes.StorageClears, contract) + state.ApplyChangeSet(changes) + require.Equal(t, common.Hash{}, state.GetState(contract, unreadKey)) +} + +func TestStateDBStorageClearThenSameValueWriteIsEmitted(t *testing.T) { + contract := testAddress(0xc5) + key := testHash(0x01) + value := testHash(0x22) + + state := NewMemoryState() + state.SetState(contract, key, value) + stateDB := newNativeStateDB(state) + + require.Equal(t, value, stateDB.GetState(contract, key)) + stateDB.SelfDestruct(contract) + stateDB.Finalise(true) + stateDB.SetState(contract, key, value) + + changes := stateDB.ChangeSet() + require.Contains(t, changes.StorageClears, contract) + require.Len(t, changes.Storage, 1) + require.Equal(t, key, changes.Storage[0].Key) + require.Equal(t, value, changes.Storage[0].Value) + require.False(t, changes.Storage[0].Delete) + + state.ApplyChangeSet(changes) + require.Equal(t, value, state.GetState(contract, key)) +} + +func TestStateDBGetCommittedStateAdvancesAtFinalise(t *testing.T) { + addr := testAddress(0xc7) + key := testHash(0x01) + first := testHash(0x11) + second := testHash(0x22) + third := testHash(0x33) + + state := NewMemoryState() + state.SetState(addr, key, first) + stateDB := newNativeStateDB(state) + + require.Equal(t, first, stateDB.GetCommittedState(addr, key)) + require.Equal(t, first, stateDB.SetState(addr, key, second)) + require.Equal(t, first, stateDB.GetCommittedState(addr, key)) + stateDB.Finalise(true) + require.Equal(t, second, stateDB.GetCommittedState(addr, key)) + require.Equal(t, second, stateDB.SetState(addr, key, third)) + require.Equal(t, second, stateDB.GetCommittedState(addr, key)) +} + +func TestStateDBGetStateAfterStorageClearDoesNotReloadPersistedSlot(t *testing.T) { + contract := testAddress(0xc8) + key := testHash(0x01) + value := testHash(0x22) + + state := NewMemoryState() + state.SetState(contract, key, value) + stateDB := newNativeStateDB(state) + + require.Equal(t, value, stateDB.GetState(contract, key)) + stateDB.SelfDestruct(contract) + stateDB.Finalise(true) + + require.Equal(t, common.Hash{}, stateDB.GetCommittedState(contract, key)) + require.Equal(t, common.Hash{}, stateDB.GetState(contract, key)) +} + +func TestPrepareClearsTransientStorage(t *testing.T) { + stateDB := newNativeStateDB(NewMemoryState()) + addr := common.HexToAddress("0x00000000000000000000000000000000000000a3") + 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 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 TestStateDBCopyPreservesSnapshotCommutativeBalanceDeltas(t *testing.T) { + coinbase := testAddress(0xc9) + stateDB := newNativeStateDB(NewMemoryState()) + stateDB.addCommutativeBalance(coinbase, uint256.NewInt(5)) + snapshot := stateDB.Snapshot() + stateDB.addCommutativeBalance(coinbase, uint256.NewInt(7)) + + copied := stateDB.Copy().(*nativeStateDB) + copied.RevertToSnapshot(snapshot) + + require.Equal(t, map[common.Address]*big.Int{ + coinbase: big.NewInt(5), + }, copied.commutativeBalanceDeltasBig()) +} + +func TestStateDBFirstStorageReadPreservesBase(t *testing.T) { + addr := testAddress(0xa9) + key := testHash(0x01) + 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 TestStateDBZeroStorageWriteStaysDirty(t *testing.T) { + addr := testAddress(0xaa) + key := testHash(0x01) + value := testHash(0x02) + + state := NewMemoryState() + state.SetState(addr, key, value) + stateDB := newNativeStateDB(state) + + require.Equal(t, value, stateDB.GetState(addr, key)) + require.Equal(t, value, stateDB.SetState(addr, key, common.Hash{})) + require.Equal(t, common.Hash{}, stateDB.GetState(addr, key)) + + changes := stateDB.ChangeSet() + require.Len(t, changes.Storage, 1) + require.Equal(t, addr, changes.Storage[0].Address) + require.Equal(t, key, changes.Storage[0].Key) + require.True(t, changes.Storage[0].Delete) + + state.ApplyChangeSet(changes) + require.Equal(t, common.Hash{}, state.GetState(addr, key)) +} + +func TestStateDBGetCodeHashDistinguishesExistingCodelessAccounts(t *testing.T) { + missing := testAddress(0xab) + eoa := testAddress(0xac) + contract := testAddress(0xad) + code := []byte{0x60, 0x00, 0x00} + + state := NewMemoryState() + state.SetBalance(eoa, big.NewInt(1)) + state.SetCode(contract, code) + stateDB := newNativeStateDB(state) + + require.Equal(t, common.Hash{}, stateDB.GetCodeHash(missing)) + require.Equal(t, ethtypes.EmptyCodeHash, stateDB.GetCodeHash(eoa)) + require.Equal(t, crypto.Keccak256Hash(code), stateDB.GetCodeHash(contract)) +} + +func TestStateDBGetCodeHashTracksCodelessAccountExistenceReads(t *testing.T) { + eoa := testAddress(0xbd) + state := NewMemoryState() + state.SetBalance(eoa, big.NewInt(1)) + stateDB := newNativeStateDB(state) + stateDB.enableAccessTracking() + + require.Equal(t, ethtypes.EmptyCodeHash, stateDB.GetCodeHash(eoa)) + readSet, _ := stateDB.accessSets() + require.Contains(t, readSet, stateAccessKey{kind: stateAccessCode, address: eoa}) + require.Contains(t, readSet, stateAccessKey{kind: stateAccessBalance, address: eoa}) + require.Contains(t, readSet, stateAccessKey{kind: stateAccessNonce, address: eoa}) + + validation := validateOCCResults([]occTxExecution{ + { + gasLimit: 1, + writeSet: map[stateAccessKey]struct{}{ + {kind: stateAccessBalance, address: eoa}: {}, + }, + }, + { + gasLimit: 1, + readSet: readSet, + }, + }, 10) + require.False(t, validation.valid) + require.Equal(t, occFallbackReasonConflict, validation.fallbackReason) +} + +func TestFinaliseClearsRefund(t *testing.T) { + stateDB := newNativeStateDB(NewMemoryState()) + stateDB.AddRefund(12) + + stateDB.Finalise(true) + + require.Zero(t, stateDB.GetRefund()) +} + +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 := 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{ + CustomPrecompiles: staticPrecompileRegistry{addr: customAddr}, + }, 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.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 { + 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() + 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: new(big.Int).Set(gasPrice), + Gas: gas, + 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 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 + 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() + 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: new(big.Int).Set(gasTipCap), + GasFeeCap: new(big.Int).Set(gasFeeCap), + Gas: gas, + 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 signBlobTxWithFees( + t *testing.T, + key *ecdsa.PrivateKey, + chainID *big.Int, + nonce uint64, + to common.Address, + value *big.Int, + data []byte, + gasTipCap *big.Int, + gasFeeCap *big.Int, + blobFeeCap *big.Int, + gas uint64, + blobHashes []common.Hash, +) []byte { + t.Helper() + tx := ethtypes.NewTx(ðtypes.BlobTx{ + ChainID: uint256.MustFromBig(chainID), + Nonce: nonce, + GasTipCap: uint256.MustFromBig(gasTipCap), + GasFeeCap: uint256.MustFromBig(gasFeeCap), + Gas: gas, + To: to, + Value: uint256.MustFromBig(value), + Data: data, + BlobFeeCap: uint256.MustFromBig(blobFeeCap), + BlobHashes: append([]common.Hash(nil), blobHashes...), + }) + signed, err := ethtypes.SignTx(tx, ethtypes.LatestSignerForChainID(chainID), key) + require.NoError(t, err) + raw, err := signed.MarshalBinary() + require.NoError(t, err) + return raw +} + +func blockContext(chainID *big.Int) BlockContext { + return BlockContext{ + Number: 1, + Time: 1, + GasLimit: 30_000_000, + ChainID: chainID, + BaseFee: big.NewInt(0), + Coinbase: common.HexToAddress("0x00000000000000000000000000000000000000cb"), + } +} + +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) +} + +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 balanceStoreCode(addr common.Address, key common.Hash) []byte { + code := append([]byte{0x73}, addr.Bytes()...) + code = append(code, 0x31, 0x7f) + code = append(code, key.Bytes()...) + return append(code, 0x55, 0x00) +} + +func initCode(runtime []byte) []byte { + if len(runtime) > 255 { + panic("test runtime too large") + } + 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 +} + +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/occ.go b/giga/evmonly/occ.go new file mode 100644 index 0000000000..7f04ba2f98 --- /dev/null +++ b/giga/evmonly/occ.go @@ -0,0 +1,527 @@ +package evmonly + +import ( + "bytes" + "context" + "fmt" + "math" + "math/big" + "sort" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/params" +) + +type occTxExecution struct { + txResult TxResult + receipt *ethtypes.Receipt + changeSet StateChangeSet + readSet map[stateAccessKey]struct{} + writeSet map[stateAccessKey]struct{} + gasUsed uint64 + gasLimit uint64 + commutativeBalanceDeltas map[common.Address]*big.Int +} + +type occTxRange struct { + start int + end int + startUint uint +} + +func (e *Executor) executeBlockOCC(ctx context.Context, req PreparedBlock) (*BlockResult, error) { + chainConfig := e.chainConfig(req.Context) + blockCtx := buildBlockContext(req.Context) + baseFee := cloneOptionalBig(req.Context.BaseFee) + gasLimit := req.Context.GasLimit + if gasLimit == 0 { + gasLimit = math.MaxUint64 + } + + workers := e.cfg.OCCWorkers + txCount := len(req.Txs) + if workers > txCount { + workers = txCount + } + results := make([]occTxExecution, txCount) + chunkSize := occChunkSize(txCount, workers) + pool := e.occPool + if pool == nil { + pool = newOCCWorkerPool(workers) + defer pool.Close() + } + if err := pool.Run(ctx, occRanges(txCount, chunkSize), func(workerCtx context.Context, txRange occTxRange) error { + for idx, idxUint := txRange.start, txRange.startUint; idx < txRange.end; idx, idxUint = idx+1, idxUint+1 { + result, err := e.executeTxSpeculative(workerCtx, req, idx, idxUint, chainConfig, blockCtx, baseFee, gasLimit) + if err != nil { + return err + } + results[idx] = result + } + return nil + }); err != nil { + if ctxErr := ctx.Err(); ctxErr != nil { + return nil, ctxErr + } + result, seqErr := e.executeBlockSequential(ctx, req) + if seqErr != nil { + return nil, seqErr + } + result.OCCStats = OCCStats{ + Attempted: true, + Fallback: true, + FallbackReason: occFallbackReasonSpeculativeError, + } + return result, nil + } + validation := validateOCCResults(results, gasLimit) + if !validation.valid { + result, err := e.executeBlockSequential(ctx, req) + if err != nil { + return nil, err + } + result.OCCStats = validation.stats(true) + return result, nil + } + result, err := e.mergeOCCResults(ctx, results) + if err != nil { + return nil, err + } + result.OCCStats = validation.stats(false) + return result, nil +} + +func occRanges(txCount int, chunkSize int) []occTxRange { + if chunkSize <= 0 { + chunkSize = 1 + } + ranges := make([]occTxRange, 0, (txCount+chunkSize-1)/chunkSize) + startUint := uint(0) + for start := 0; start < txCount; { + end := start + chunkSize + if end > txCount { + end = txCount + } + ranges = append(ranges, occTxRange{start: start, end: end, startUint: startUint}) + for start < end { + start++ + startUint++ + } + } + return ranges +} + +func occChunkSize(txCount int, workers int) int { + if txCount <= 0 || workers <= 0 { + return 1 + } + targetChunks := workers * 8 + chunkSize := (txCount + targetChunks - 1) / targetChunks + if chunkSize < 16 { + return 16 + } + if chunkSize > 256 { + return 256 + } + return chunkSize +} + +func (e *Executor) executeTxSpeculative( + ctx context.Context, + req PreparedBlock, + txIndex int, + txIndexUint uint, + chainConfig *params.ChainConfig, + blockCtx vm.BlockContext, + baseFee *big.Int, + gasLimit uint64, +) (occTxExecution, error) { + p := req.Txs[txIndex] + stateDB := newNativeStateDB(e.state) + stateDB.enableAccessTracking() + evm := vm.NewEVM(blockCtx, stateDB, chainConfig, vm.Config{}, nil) + stateDB.SetEVM(evm) + gasPool := new(core.GasPool).AddGas(gasLimit) + txResult, receipt, err := e.executeTx( + evm, + stateDB, + gasPool, + req.Context, + p, + txIndex, + txIndexUint, + baseFee, + ) + if err != nil { + return occTxExecution{}, fmt.Errorf("execute tx %d %s: %w", txIndex, p.Tx.Hash(), err) + } + readSet, writeSet := stateDB.accessSets() + var changeSet StateChangeSet + stateDB.ChangeSetInto(&changeSet) + return occTxExecution{ + txResult: txResult, + receipt: receipt, + changeSet: changeSet, + readSet: readSet, + writeSet: writeSet, + gasUsed: txResult.GasUsed, + gasLimit: p.Tx.Gas(), + commutativeBalanceDeltas: stateDB.commutativeBalanceDeltasBig(), + }, nil +} + +type occValidationResult struct { + valid bool + fallbackReason string + conflictCount uint64 + conflicts map[occConflictAggregationKey]uint64 +} + +type occConflictAggregationKey struct { + access string + kind stateAccessKind + address common.Address + slot common.Hash +} + +const ( + occFallbackReasonConflict = "conflict" + occFallbackReasonGasLimit = "gas_limit" + occFallbackReasonGasOverflow = "gas_overflow" + occFallbackReasonSpeculativeError = "speculative_error" +) + +func validateOCCResults(results []occTxExecution, gasLimit uint64) occValidationResult { + writes := newStateAccessIndex() + var totalGasLimit uint64 + var totalGasUsed uint64 + validation := occValidationResult{valid: true} + for _, result := range results { + if len(result.changeSet.StorageClears) > 0 { + validation.valid = false + validation.fallbackReason = occFallbackReasonConflict + return validation + } + if result.gasLimit > math.MaxUint64-totalGasLimit || result.gasUsed > math.MaxUint64-totalGasUsed { + validation.valid = false + validation.fallbackReason = occFallbackReasonGasOverflow + return validation + } + totalGasLimit += result.gasLimit + totalGasUsed += result.gasUsed + if totalGasLimit > gasLimit { + validation.valid = false + validation.fallbackReason = occFallbackReasonGasLimit + return validation + } + validation.addConflicts("read", writes, result.readSet) + validation.addConflicts("write", writes, result.writeSet) + writes.addAll(result.writeSet) + writes.addCommutativeBalanceDeltas(result.commutativeBalanceDeltas) + } + if validation.conflictCount > 0 { + validation.valid = false + validation.fallbackReason = occFallbackReasonConflict + } + return validation +} + +func (r *occValidationResult) addConflicts(access string, writes *stateAccessIndex, set map[stateAccessKey]struct{}) { + for key := range set { + if !writes.conflictsWith(key) { + continue + } + if r.conflicts == nil { + r.conflicts = map[occConflictAggregationKey]uint64{} + } + r.conflictCount++ + r.conflicts[occConflictAggregationKey{ + access: access, + kind: key.kind, + address: key.address, + slot: key.slot, + }]++ + } +} + +func (r occValidationResult) stats(fallback bool) OCCStats { + stats := OCCStats{ + Attempted: true, + Fallback: fallback, + FallbackReason: r.fallbackReason, + ConflictCount: r.conflictCount, + } + if len(r.conflicts) == 0 { + return stats + } + keys := make([]occConflictAggregationKey, 0, len(r.conflicts)) + for key := range r.conflicts { + keys = append(keys, key) + } + sort.Slice(keys, func(i, j int) bool { + left, right := keys[i], keys[j] + if left.access != right.access { + return left.access < right.access + } + if left.kind != right.kind { + return left.kind < right.kind + } + if cmp := bytes.Compare(left.address[:], right.address[:]); cmp != 0 { + return cmp < 0 + } + return bytes.Compare(left.slot[:], right.slot[:]) < 0 + }) + for _, key := range keys { + stats.ConflictSamples = append(stats.ConflictSamples, OCCConflictCount{ + Access: key.access, + Kind: key.kind.String(), + Address: key.address, + Slot: key.slot, + Count: r.conflicts[key], + }) + } + return stats +} + +func (k stateAccessKind) String() string { + switch k { + case stateAccessAccount: + return "account" + case stateAccessBalance: + return "balance" + case stateAccessNonce: + return "nonce" + case stateAccessCode: + return "code" + case stateAccessStorage: + return "storage" + default: + return "unknown" + } +} + +func (e *Executor) mergeOCCResults(ctx context.Context, results []occTxExecution) (*BlockResult, error) { + blockResult, err := e.acquireBlockResult(ctx, len(results)) + if err != nil { + return nil, err + } + blockResult.prepareIndexedResults(len(results)) + var logIndex uint + for i, result := range results { + blockResult.GasUsed += result.gasUsed + result.txResult.CumulativeGasUsed = blockResult.GasUsed + result.receipt.CumulativeGasUsed = blockResult.GasUsed + for _, log := range result.receipt.Logs { + log.Index = logIndex + logIndex++ + } + blockResult.Txs[i] = result.txResult + blockResult.Receipts[i] = result.receipt + } + mergeChangeSetsInto(results, &blockResult.ChangeSet) + return blockResult, nil +} + +type stateAccessIndex struct { + exact map[stateAccessKey]struct{} + account map[common.Address]struct{} + touched map[common.Address]struct{} + commutativeBalance map[common.Address]struct{} +} + +func newStateAccessIndex() *stateAccessIndex { + return &stateAccessIndex{ + exact: map[stateAccessKey]struct{}{}, + account: map[common.Address]struct{}{}, + touched: map[common.Address]struct{}{}, + commutativeBalance: map[common.Address]struct{}{}, + } +} + +func (i *stateAccessIndex) conflictsWith(key stateAccessKey) bool { + if _, ok := i.exact[key]; ok { + return true + } + if _, ok := i.account[key.address]; ok { + return true + } + if key.kind == stateAccessAccount { + if _, ok := i.touched[key.address]; ok { + return true + } + } + if key.kind == stateAccessStorage { + return false + } + _, ok := i.commutativeBalance[key.address] + return ok +} + +func (i *stateAccessIndex) addAll(set map[stateAccessKey]struct{}) { + for key := range set { + i.exact[key] = struct{}{} + // Exist/Empty account reads depend on account metadata, not storage slots. + if key.kind != stateAccessStorage { + i.touched[key.address] = struct{}{} + } + if key.kind == stateAccessAccount { + i.account[key.address] = struct{}{} + } + } +} + +func (i *stateAccessIndex) addCommutativeBalanceDeltas(deltas map[common.Address]*big.Int) { + for addr, delta := range deltas { + if delta == nil || delta.Sign() == 0 { + continue + } + i.commutativeBalance[addr] = struct{}{} + } +} + +type storageChangeKey struct { + address common.Address + key common.Hash +} + +func mergeChangeSetsInto(results []occTxExecution, merged *StateChangeSet) { + merged.resetForReuse() + balances := map[common.Address]*big.Int{} + balanceBases := map[common.Address]*big.Int{} + balanceDeltas := map[common.Address]*big.Int{} + nonces := map[common.Address]uint64{} + code := map[common.Address]CodeChange{} + storageClears := map[common.Address]struct{}{} + storage := map[storageChangeKey]StorageChange{} + + for _, result := range results { + for _, change := range result.changeSet.Balances { + delta := result.commutativeBalanceDeltas[change.Address] + if delta == nil { + balances[change.Address] = cloneBig(change.Balance) + continue + } + base := new(big.Int).Sub(cloneBig(change.Balance), delta) + balanceBases[change.Address] = base + if _, normalWrite := result.writeSet[stateAccessKey{kind: stateAccessBalance, address: change.Address}]; normalWrite { + balances[change.Address] = cloneBig(base) + } + } + for addr, delta := range result.commutativeBalanceDeltas { + if balanceDeltas[addr] == nil { + balanceDeltas[addr] = new(big.Int) + } + balanceDeltas[addr].Add(balanceDeltas[addr], delta) + } + for _, change := range result.changeSet.Nonces { + nonces[change.Address] = change.Nonce + } + for _, change := range result.changeSet.Code { + code[change.Address] = CodeChange{ + Address: change.Address, + Code: cloneBytes(change.Code), + Delete: change.Delete, + } + } + for _, addr := range result.changeSet.StorageClears { + storageClears[addr] = struct{}{} + for key := range storage { + if key.address == addr { + delete(storage, key) + } + } + } + for _, change := range result.changeSet.Storage { + storage[storageChangeKey{address: change.Address, key: change.Key}] = change + } + } + for addr, delta := range balanceDeltas { + base := balances[addr] + if base == nil { + base = balanceBases[addr] + } + if base == nil { + base = new(big.Int) + } else { + base = cloneBig(base) + } + balances[addr] = base.Add(base, delta) + } + + balanceAddrs := sortedAddressesFromBigMap(balances) + for _, addr := range balanceAddrs { + merged.Balances = append(merged.Balances, BalanceChange{Address: addr, Balance: cloneBig(balances[addr])}) + } + nonceAddrs := sortedAddressesFromUint64Map(nonces) + for _, addr := range nonceAddrs { + merged.Nonces = append(merged.Nonces, NonceChange{Address: addr, Nonce: nonces[addr]}) + } + codeAddrs := sortedAddressesFromCodeMap(code) + for _, addr := range codeAddrs { + change := code[addr] + change.Code = cloneBytes(change.Code) + merged.Code = append(merged.Code, change) + } + storageClearAddrs := sortedAddressesFromSet(storageClears) + merged.StorageClears = append(merged.StorageClears, storageClearAddrs...) + storageKeys := make([]storageChangeKey, 0, len(storage)) + for key := range storage { + storageKeys = append(storageKeys, key) + } + sort.Slice(storageKeys, func(i, j int) bool { + if cmp := bytes.Compare(storageKeys[i].address[:], storageKeys[j].address[:]); cmp != 0 { + return cmp < 0 + } + return bytes.Compare(storageKeys[i].key[:], storageKeys[j].key[:]) < 0 + }) + for _, key := range storageKeys { + merged.Storage = append(merged.Storage, storage[key]) + } +} + +func sortedAddressesFromBigMap(values map[common.Address]*big.Int) []common.Address { + addrs := make([]common.Address, 0, len(values)) + for addr := range values { + addrs = append(addrs, addr) + } + sort.Slice(addrs, func(i, j int) bool { + return bytes.Compare(addrs[i][:], addrs[j][:]) < 0 + }) + return addrs +} + +func sortedAddressesFromUint64Map(values map[common.Address]uint64) []common.Address { + addrs := make([]common.Address, 0, len(values)) + for addr := range values { + addrs = append(addrs, addr) + } + sort.Slice(addrs, func(i, j int) bool { + return bytes.Compare(addrs[i][:], addrs[j][:]) < 0 + }) + return addrs +} + +func sortedAddressesFromCodeMap(values map[common.Address]CodeChange) []common.Address { + addrs := make([]common.Address, 0, len(values)) + for addr := range values { + addrs = append(addrs, addr) + } + sort.Slice(addrs, func(i, j int) bool { + return bytes.Compare(addrs[i][:], addrs[j][:]) < 0 + }) + return addrs +} + +func sortedAddressesFromSet(values map[common.Address]struct{}) []common.Address { + addrs := make([]common.Address, 0, len(values)) + for addr := range values { + addrs = append(addrs, addr) + } + sort.Slice(addrs, func(i, j int) bool { + return bytes.Compare(addrs[i][:], addrs[j][:]) < 0 + }) + return addrs +} diff --git a/giga/evmonly/occ_pool.go b/giga/evmonly/occ_pool.go new file mode 100644 index 0000000000..0678968a96 --- /dev/null +++ b/giga/evmonly/occ_pool.go @@ -0,0 +1,119 @@ +package evmonly + +import ( + "context" + "fmt" + "sync" +) + +type occWorkerPool struct { + jobs chan occPoolJob + stop chan struct{} + closed chan struct{} + once sync.Once +} + +type occPoolJob struct { + ctx context.Context + txRange occTxRange + run func(context.Context, occTxRange) error + + done *sync.WaitGroup + cancel context.CancelFunc + errOnce *sync.Once + err *error +} + +func newOCCWorkerPool(workers int) *occWorkerPool { + p := &occWorkerPool{ + jobs: make(chan occPoolJob, workers*2), + stop: make(chan struct{}), + closed: make(chan struct{}), + } + var workerWG sync.WaitGroup + workerWG.Add(workers) + for range workers { + go func() { + defer workerWG.Done() + p.runWorker() + }() + } + go func() { + workerWG.Wait() + close(p.closed) + }() + return p +} + +func (p *occWorkerPool) runWorker() { + for { + select { + case <-p.stop: + return + case job := <-p.jobs: + p.runJob(job) + } + } +} + +func (p *occWorkerPool) runJob(job occPoolJob) { + defer job.done.Done() + if err := job.ctx.Err(); err != nil { + return + } + if err := job.run(job.ctx, job.txRange); err != nil { + job.errOnce.Do(func() { + *job.err = err + job.cancel() + }) + } +} + +func (p *occWorkerPool) Run(ctx context.Context, ranges []occTxRange, run func(context.Context, occTxRange) error) error { + jobCtx, cancel := context.WithCancel(ctx) + defer cancel() + + var done sync.WaitGroup + var err error + var errOnce sync.Once +dispatch: + for _, txRange := range ranges { + done.Add(1) + job := occPoolJob{ + ctx: jobCtx, + txRange: txRange, + run: run, + done: &done, + cancel: cancel, + errOnce: &errOnce, + err: &err, + } + select { + case p.jobs <- job: + case <-jobCtx.Done(): + done.Done() + break dispatch + case <-p.stop: + done.Done() + return fmt.Errorf("OCC worker pool is closed") + } + } + done.Wait() + if err != nil { + return err + } + if ctxErr := ctx.Err(); ctxErr != nil { + return ctxErr + } + return nil +} + +func (p *occWorkerPool) Close() { + if p == nil { + return + } + p.once.Do(func() { + close(p.stop) + <-p.closed + }) +} diff --git a/giga/evmonly/parser.go b/giga/evmonly/parser.go new file mode 100644 index 0000000000..16a6581f5d --- /dev/null +++ b/giga/evmonly/parser.go @@ -0,0 +1,41 @@ +package evmonly + +import ( + "context" + "fmt" + + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" +) + +func parseBlockTxs(ctx context.Context, txs [][]byte, signer ethtypes.Signer) ([]PreparedTx, error) { + parsed := make([]PreparedTx, len(txs)) + for i, raw := range txs { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + tx, sender, err := parseTx(raw, signer) + if err != nil { + return nil, fmt.Errorf("parse tx %d: %w", i, err) + } + if err := validateSupportedTx(tx); err != nil { + return nil, fmt.Errorf("parse tx %d: %w", i, err) + } + parsed[i] = PreparedTx{Tx: tx, Sender: sender} + } + return parsed, nil +} + +func parseTx(raw []byte, signer ethtypes.Signer) (*ethtypes.Transaction, common.Address, error) { + var tx ethtypes.Transaction + if err := tx.UnmarshalBinary(raw); 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/precompiles/context.go b/giga/evmonly/precompiles/context.go new file mode 100644 index 0000000000..828937d6b1 --- /dev/null +++ b/giga/evmonly/precompiles/context.go @@ -0,0 +1,68 @@ +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) + Addresses() []common.Address +} + +// 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/result_pool.go b/giga/evmonly/result_pool.go new file mode 100644 index 0000000000..feb7424b4a --- /dev/null +++ b/giga/evmonly/result_pool.go @@ -0,0 +1,127 @@ +package evmonly + +import ( + "context" + "sync" + "sync/atomic" + + ethtypes "github.com/ethereum/go-ethereum/core/types" +) + +type blockResultPool struct { + free chan *BlockResult +} + +type blockResultLease struct { + pool *blockResultPool + result *BlockResult + refs atomic.Int32 +} + +func newBlockResultPool(size int) *blockResultPool { + if size <= 0 { + return nil + } + p := &blockResultPool{free: make(chan *BlockResult, size)} + for range size { + p.free <- &BlockResult{} + } + return p +} + +func (p *blockResultPool) acquire(ctx context.Context, txCapacity int) (*BlockResult, error) { + if p == nil { + result := &BlockResult{} + result.prepareForBlock(txCapacity) + return result, nil + } + select { + case result := <-p.free: + result.prepareForBlock(txCapacity) + lease := &blockResultLease{pool: p, result: result} + lease.refs.Store(1) + result.lease = lease + return result, nil + case <-ctx.Done(): + return nil, ctx.Err() + } +} + +func (l *blockResultLease) retain() func() { + if l == nil { + return func() {} + } + l.refs.Add(1) + var once sync.Once + return func() { + once.Do(l.release) + } +} + +func (l *blockResultLease) release() { + if l == nil { + return + } + if l.refs.Add(-1) != 0 { + return + } + result := l.result + result.resetForPool() + l.pool.free <- result +} + +func (r *BlockResult) retain() func() { + if r == nil || r.lease == nil { + return func() {} + } + return r.lease.retain() +} + +func (r *BlockResult) prepareForBlock(txCapacity int) { + r.resetForPool() + if cap(r.Txs) < txCapacity { + r.Txs = make([]TxResult, 0, txCapacity) + } + if cap(r.Receipts) < txCapacity { + r.Receipts = make(ethtypes.Receipts, 0, txCapacity) + } +} + +func (r *BlockResult) prepareIndexedResults(txCount int) { + if cap(r.Txs) < txCount { + r.Txs = make([]TxResult, txCount) + } else { + r.Txs = r.Txs[:txCount] + clear(r.Txs) + } + if cap(r.Receipts) < txCount { + r.Receipts = make(ethtypes.Receipts, txCount) + } else { + r.Receipts = r.Receipts[:txCount] + clear(r.Receipts) + } +} + +func (r *BlockResult) resetForPool() { + r.ChangeSet.resetForReuse() + clear(r.Txs) + r.Txs = r.Txs[:0] + clear(r.Receipts) + r.Receipts = r.Receipts[:0] + r.GasUsed = 0 + r.OCCStats = OCCStats{} + r.lease = nil +} + +func (cs *StateChangeSet) resetForReuse() { + clear(cs.Balances) + cs.Balances = cs.Balances[:0] + clear(cs.Nonces) + cs.Nonces = cs.Nonces[:0] + clear(cs.Code) + cs.Code = cs.Code[:0] + clear(cs.StorageClears) + cs.StorageClears = cs.StorageClears[:0] + clear(cs.Storage) + cs.Storage = cs.Storage[:0] +} diff --git a/giga/evmonly/state.go b/giga/evmonly/state.go new file mode 100644 index 0000000000..88b415675b --- /dev/null +++ b/giga/evmonly/state.go @@ -0,0 +1,182 @@ +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 _, addr := range cs.StorageClears { + acct := s.getOrCreateAccountLocked(addr) + acct.Storage = map[common.Hash]common.Hash{} + } + for _, change := range cs.Storage { + acct := s.getOrCreateAccountLocked(change.Address) + if acct.Storage == nil { + 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 cloneOptionalBig(v *big.Int) *big.Int { + if v == nil { + return nil + } + return new(big.Int).Set(v) +} + +func cloneBytes(v []byte) []byte { + if len(v) == 0 { + return nil + } + return append([]byte(nil), v...) +} diff --git a/giga/evmonly/state_db.go b/giga/evmonly/state_db.go new file mode 100644 index 0000000000..8f5a6134de --- /dev/null +++ b/giga/evmonly/state_db.go @@ -0,0 +1,1072 @@ +package evmonly + +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" +) + +var errInsufficientBalance = errors.New("insufficient balance") + +type nativeStateDB struct { + source 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 + finaliseAddrs map[common.Address]struct{} + committedStorage map[common.Address]map[common.Hash]storageValue + txStorageWrites map[common.Address]map[common.Hash]struct{} + txStorageClears map[common.Address]struct{} + commutativeBalanceDeltas map[common.Address]*uint256.Int + journal []nativeJournalEntry + snapshots []nativeSnapshot + readSet map[stateAccessKey]struct{} + writeSet map[stateAccessKey]struct{} + + txHash common.Hash + txIndex int + txIndexUint uint + err error + evm *vm.EVM +} + +type nativeAccount struct { + Balance *uint256.Int + Nonce uint64 + Code []byte + Storage map[common.Hash]storageValue + StorageCleared bool + SelfDestructed bool + Created bool +} + +type storageValue struct { + value common.Hash +} + +type nativeSnapshot struct { + journalLen int + refund uint64 + logsLen int + accessList accessList + transientStates map[common.Address]map[common.Hash]common.Hash + finaliseAddrs map[common.Address]struct{} + txStorageWrites map[common.Address]map[common.Hash]struct{} + txStorageClears map[common.Address]struct{} + commutativeBalanceDeltas map[common.Address]*uint256.Int + preimages map[common.Hash][]byte + journaledAddrs map[common.Address]struct{} + err error +} + +type nativeJournalKind uint8 + +const ( + nativeJournalAccount nativeJournalKind = iota +) + +type nativeJournalEntry struct { + kind nativeJournalKind + address common.Address + account *nativeAccount +} + +type accessList struct { + addresses map[common.Address]struct{} + slots map[common.Address]map[common.Hash]struct{} +} + +type stateAccessKind uint8 + +const ( + stateAccessAccount stateAccessKind = iota + stateAccessBalance + stateAccessNonce + stateAccessCode + stateAccessStorage +) + +type stateAccessKey struct { + kind stateAccessKind + address common.Address + slot common.Hash +} + +func newNativeStateDB(source StateReader) *nativeStateDB { + if source == nil { + source = NewMemoryState() + } + return &nativeStateDB{ + source: source, + accounts: map[common.Address]*nativeAccount{}, + base: map[common.Address]*nativeAccount{}, + preimages: map[common.Hash][]byte{}, + accessList: newAccessList(), + transientStates: map[common.Address]map[common.Hash]common.Hash{}, + finaliseAddrs: map[common.Address]struct{}{}, + committedStorage: map[common.Address]map[common.Hash]storageValue{}, + txStorageWrites: map[common.Address]map[common.Hash]struct{}{}, + txStorageClears: map[common.Address]struct{}{}, + commutativeBalanceDeltas: map[common.Address]*uint256.Int{}, + } +} + +func (s *nativeStateDB) ChangeSet() StateChangeSet { + var changes StateChangeSet + s.ChangeSetInto(&changes) + return changes +} + +func (s *nativeStateDB) ChangeSetInto(changes *StateChangeSet) { + changes.resetForReuse() + addresses := make([]common.Address, 0, len(s.accounts)) + for addr := range s.accounts { + addresses = append(addresses, addr) + } + sort.Slice(addresses, func(i, j int) bool { + return bytes.Compare(addresses[i][:], addresses[j][:]) < 0 + }) + + for _, addr := range addresses { + acct := s.accounts[addr] + base := s.baseAccount(addr) + + _, hasCommutativeDelta := s.commutativeBalanceDeltas[addr] + if !acct.Balance.Eq(base.Balance) || hasCommutativeDelta { + changes.Balances = append(changes.Balances, BalanceChange{ + Address: addr, + Balance: acct.Balance.ToBig(), + }) + } + if acct.Nonce != base.Nonce { + changes.Nonces = append(changes.Nonces, NonceChange{ + Address: addr, + Nonce: acct.Nonce, + }) + } + if !bytes.Equal(acct.Code, base.Code) { + changes.Code = append(changes.Code, CodeChange{ + Address: addr, + Code: cloneBytes(acct.Code), + Delete: len(acct.Code) == 0, + }) + } + storageKeys := storageKeyUnion(base.Storage, acct.Storage) + for _, key := range storageKeys { + oldValue := storageHash(base.Storage, key) + newValue := storageHash(acct.Storage, key) + if acct.StorageCleared { + oldValue = common.Hash{} + } + if oldValue == newValue { + continue + } + changes.Storage = append(changes.Storage, StorageChange{ + Address: addr, + Key: key, + Value: newValue, + Delete: newValue == (common.Hash{}), + }) + } + if acct.StorageCleared { + changes.StorageClears = append(changes.StorageClears, addr) + } + } +} + +func (s *nativeStateDB) CreateAccount(addr common.Address) { + acct := s.account(addr) + s.recordAccount(addr) + s.markWrite(stateAccessKey{kind: stateAccessAccount, address: addr}) + balance := acct.Balance.Clone() + storageCleared := acct.StorageCleared + selfDestructed := acct.SelfDestructed + *acct = nativeAccount{ + Balance: balance, + Storage: map[common.Hash]storageValue{}, + StorageCleared: storageCleared, + SelfDestructed: selfDestructed, + Created: true, + } + s.markForFinalise(addr) +} + +func (s *nativeStateDB) CreateContract(addr common.Address) { + acct := s.account(addr) + s.recordAccount(addr) + s.markWrite(stateAccessKey{kind: stateAccessAccount, address: addr}) + acct.Created = true + s.markForFinalise(addr) +} + +func (s *nativeStateDB) SubBalance(addr common.Address, amount *uint256.Int, _ tracing.BalanceChangeReason) uint256.Int { + prev := *s.GetBalance(addr) + if amount == nil || amount.IsZero() { + return prev + } + acct := s.account(addr) + if acct.Balance.Cmp(amount) < 0 { + s.err = errInsufficientBalance + return prev + } + s.recordAccount(addr) + s.markWrite(stateAccessKey{kind: stateAccessBalance, address: addr}) + acct.Balance.Sub(acct.Balance, amount) + return prev +} + +func (s *nativeStateDB) AddBalance(addr common.Address, amount *uint256.Int, reason tracing.BalanceChangeReason) uint256.Int { + if reason == tracing.BalanceIncreaseRewardTransactionFee { + return s.addCommutativeBalance(addr, amount) + } + prev := *s.GetBalance(addr) + if amount == nil || amount.IsZero() { + return prev + } + acct := s.account(addr) + s.recordAccount(addr) + s.markWrite(stateAccessKey{kind: stateAccessBalance, address: addr}) + acct.Balance.Add(acct.Balance, amount) + return prev +} + +func (s *nativeStateDB) addCommutativeBalance(addr common.Address, amount *uint256.Int) uint256.Int { + acct := s.account(addr) + prev := *acct.Balance.Clone() + if amount == nil || amount.IsZero() { + return prev + } + s.recordAccount(addr) + acct.Balance.Add(acct.Balance, amount) + if s.commutativeBalanceDeltas == nil { + s.commutativeBalanceDeltas = map[common.Address]*uint256.Int{} + } + delta, ok := s.commutativeBalanceDeltas[addr] + if !ok { + delta = uint256.NewInt(0) + s.commutativeBalanceDeltas[addr] = delta + } + delta.Add(delta, amount) + return prev +} + +func (s *nativeStateDB) commutativeBalanceDeltasBig() map[common.Address]*big.Int { + if len(s.commutativeBalanceDeltas) == 0 { + return nil + } + deltas := make(map[common.Address]*big.Int, len(s.commutativeBalanceDeltas)) + for addr, delta := range s.commutativeBalanceDeltas { + deltas[addr] = delta.ToBig() + } + return deltas +} + +func (s *nativeStateDB) GetBalance(addr common.Address) *uint256.Int { + s.markRead(stateAccessKey{kind: stateAccessBalance, address: addr}) + return s.account(addr).Balance.Clone() +} + +func (s *nativeStateDB) SetBalance(addr common.Address, balance *uint256.Int, _ tracing.BalanceChangeReason) { + acct := s.account(addr) + s.recordAccount(addr) + s.markWrite(stateAccessKey{kind: stateAccessBalance, address: addr}) + if balance == nil { + acct.Balance = uint256.NewInt(0) + return + } + acct.Balance = balance.Clone() +} + +func (s *nativeStateDB) GetNonce(addr common.Address) uint64 { + s.markRead(stateAccessKey{kind: stateAccessNonce, address: addr}) + return s.account(addr).Nonce +} + +func (s *nativeStateDB) SetNonce(addr common.Address, nonce uint64, _ tracing.NonceChangeReason) { + acct := s.account(addr) + s.recordAccount(addr) + s.markWrite(stateAccessKey{kind: stateAccessNonce, address: addr}) + acct.Nonce = nonce +} + +func (s *nativeStateDB) GetCodeHash(addr common.Address) common.Hash { + s.markRead(stateAccessKey{kind: stateAccessCode, address: addr}) + acct := s.account(addr) + if len(acct.Code) > 0 { + return crypto.Keccak256Hash(acct.Code) + } + s.markRead(stateAccessKey{kind: stateAccessBalance, address: addr}) + s.markRead(stateAccessKey{kind: stateAccessNonce, address: addr}) + if acct.Nonce == 0 && acct.Balance.IsZero() { + return common.Hash{} + } + return ethtypes.EmptyCodeHash +} + +func (s *nativeStateDB) GetCode(addr common.Address) []byte { + s.markRead(stateAccessKey{kind: stateAccessCode, address: addr}) + return cloneBytes(s.account(addr).Code) +} + +func (s *nativeStateDB) SetCode(addr common.Address, code []byte) []byte { + acct := s.account(addr) + prev := cloneBytes(acct.Code) + s.recordAccount(addr) + s.markWrite(stateAccessKey{kind: stateAccessCode, address: addr}) + acct.Code = cloneBytes(code) + return prev +} + +func (s *nativeStateDB) GetCodeSize(addr common.Address) int { + s.markRead(stateAccessKey{kind: stateAccessCode, address: addr}) + return len(s.account(addr).Code) +} + +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.markRead(stateAccessKey{kind: stateAccessStorage, address: addr, slot: key}) + return s.committedState(addr, key) +} + +func (s *nativeStateDB) GetState(addr common.Address, key common.Hash) common.Hash { + s.markRead(stateAccessKey{kind: stateAccessStorage, address: addr, slot: key}) + s.ensureStorage(addr, key) + return storageHash(s.account(addr).Storage, key) +} + +func (s *nativeStateDB) SetState(addr common.Address, key common.Hash, value common.Hash) common.Hash { + s.ensureStorage(addr, key) + acct := s.account(addr) + prev := storageHash(acct.Storage, key) + s.recordAccount(addr) + s.markWrite(stateAccessKey{kind: stateAccessStorage, address: addr, slot: key}) + acct.Storage[key] = storageValue{value: value} + s.markTxStorageWrite(addr, key) + return prev +} + +func (s *nativeStateDB) SetStorage(addr common.Address, states map[common.Hash]common.Hash) { + acct := s.account(addr) + s.recordAccount(addr) + s.markWrite(stateAccessKey{kind: stateAccessAccount, address: addr}) + s.markTxStorageClear(addr) + acct.Storage = map[common.Hash]storageValue{} + acct.StorageCleared = true + for key, value := range states { + acct.Storage[key] = storageValue{value: value} + s.markTxStorageWrite(addr, key) + } +} + +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() + s.recordAccount(addr) + s.markWrite(stateAccessKey{kind: stateAccessAccount, address: addr}) + acct.Balance.Clear() + acct.SelfDestructed = true + s.markForFinalise(addr) + return prev +} + +func (s *nativeStateDB) SelfDestruct6780(addr common.Address) (uint256.Int, bool) { + acct := s.account(addr) + if !acct.Created { + return *acct.Balance.Clone(), 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 { + s.markRead(stateAccessKey{kind: stateAccessAccount, address: addr}) + acct := s.account(addr) + return acct.SelfDestructed || acct.Nonce != 0 || !acct.Balance.IsZero() || len(acct.Code) != 0 +} + +func (s *nativeStateDB) Empty(addr common.Address) bool { + s.markRead(stateAccessKey{kind: stateAccessAccount, address: addr}) + acct := s.account(addr) + return acct.Nonce == 0 && acct.Balance.IsZero() && len(acct.Code) == 0 +} + +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.reset() + clearNestedHashMaps(s.transientStates) + 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{ + journalLen: len(s.journal), + refund: s.refund, + logsLen: len(s.logs), + accessList: cloneAccessList(s.accessList), + transientStates: cloneTransientStates(s.transientStates), + finaliseAddrs: cloneAddressSet(s.finaliseAddrs), + txStorageWrites: cloneStorageWriteSet(s.txStorageWrites), + txStorageClears: cloneAddressSet(s.txStorageClears), + commutativeBalanceDeltas: cloneUint256Map(s.commutativeBalanceDeltas), + preimages: clonePreimages(s.preimages), + journaledAddrs: map[common.Address]struct{}{}, + 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] + for i := len(s.journal) - 1; i >= snapshot.journalLen; i-- { + s.journal[i].revert(s) + } + s.journal = s.journal[:snapshot.journalLen] + s.refund = snapshot.refund + s.logs = s.logs[:snapshot.logsLen] + s.accessList = cloneAccessList(snapshot.accessList) + s.transientStates = cloneTransientStates(snapshot.transientStates) + s.finaliseAddrs = cloneAddressSet(snapshot.finaliseAddrs) + s.txStorageWrites = cloneStorageWriteSet(snapshot.txStorageWrites) + s.txStorageClears = cloneAddressSet(snapshot.txStorageClears) + s.commutativeBalanceDeltas = cloneUint256Map(snapshot.commutativeBalanceDeltas) + s.preimages = clonePreimages(snapshot.preimages) + s.err = snapshot.err + s.snapshots = s.snapshots[:id] +} + +func (s *nativeStateDB) AddLog(log *ethtypes.Log) { + log.TxHash = s.txHash + log.TxIndex = s.txIndexUint + 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 addr := range s.finaliseAddrs { + acct := s.account(addr) + if acct.SelfDestructed { + s.recordAccount(addr) + acct.Code = nil + acct.Storage = map[common.Hash]storageValue{} + acct.StorageCleared = true + acct.Nonce = 0 + acct.SelfDestructed = false + s.markTxStorageClear(addr) + } + if acct.Created { + s.recordAccount(addr) + acct.Created = false + } + } + s.finaliseTxStorage() + clear(s.finaliseAddrs) + s.refund = 0 +} + +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) 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, + accounts: cloneAccounts(s.accounts), + base: cloneAccounts(s.base), + refund: s.refund, + logs: append([]*ethtypes.Log(nil), s.logs...), + preimages: clonePreimages(s.preimages), + accessList: cloneAccessList(s.accessList), + transientStates: cloneTransientStates(s.transientStates), + finaliseAddrs: cloneAddressSet(s.finaliseAddrs), + committedStorage: cloneStorageValueMaps(s.committedStorage), + txStorageWrites: cloneStorageWriteSet(s.txStorageWrites), + txStorageClears: cloneAddressSet(s.txStorageClears), + commutativeBalanceDeltas: cloneUint256Map(s.commutativeBalanceDeltas), + journal: cloneJournal(s.journal), + snapshots: cloneSnapshots(s.snapshots), + readSet: cloneAccessSet(s.readSet), + writeSet: cloneAccessSet(s.writeSet), + txHash: s.txHash, + txIndex: s.txIndex, + txIndexUint: s.txIndexUint, + err: s.err, + evm: s.evm, + } + return cp +} + +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) enableAccessTracking() { + if s.readSet == nil { + s.readSet = map[stateAccessKey]struct{}{} + } else { + clear(s.readSet) + } + if s.writeSet == nil { + s.writeSet = map[stateAccessKey]struct{}{} + } else { + clear(s.writeSet) + } +} + +func (s *nativeStateDB) accessSets() (map[stateAccessKey]struct{}, map[stateAccessKey]struct{}) { + return cloneAccessSet(s.readSet), cloneAccessSet(s.writeSet) +} + +func (s *nativeStateDB) markRead(key stateAccessKey) { + if s.readSet != nil { + s.readSet[key] = struct{}{} + } +} + +func (s *nativeStateDB) markWrite(key stateAccessKey) { + if s.writeSet != nil { + s.writeSet[key] = struct{}{} + } +} + +func (s *nativeStateDB) recordAccount(addr common.Address) { + if len(s.snapshots) == 0 { + return + } + snapshot := &s.snapshots[len(s.snapshots)-1] + if _, ok := snapshot.journaledAddrs[addr]; ok { + return + } + snapshot.journaledAddrs[addr] = struct{}{} + s.journal = append(s.journal, nativeJournalEntry{ + kind: nativeJournalAccount, + address: addr, + account: s.account(addr).clone(), + }) +} + +func (e nativeJournalEntry) revert(s *nativeStateDB) { + switch e.kind { + case nativeJournalAccount: + s.accounts[e.address] = e.account.clone() + default: + panic("unknown native state journal entry") + } +} + +func (s *nativeStateDB) markForFinalise(addr common.Address) { + s.finaliseAddrs[addr] = struct{}{} +} + +func (s *nativeStateDB) clearSnapshots() { + clear(s.journal) + s.journal = s.journal[:0] + clear(s.snapshots) + s.snapshots = s.snapshots[:0] +} + +func (s *nativeStateDB) account(addr common.Address) *nativeAccount { + if acct, ok := s.accounts[addr]; ok { + return acct + } + 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] +} + +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) + acct := s.account(addr) + if _, ok := acct.Storage[key]; ok { + return + } + if slots, ok := s.committedStorage[addr]; ok { + if value, committed := slots[key]; committed { + acct.Storage[key] = value + return + } + } + if acct.StorageCleared { + return + } + if _, ok := base.Storage[key]; !ok { + if value := s.source.GetState(addr, key); value != (common.Hash{}) { + base.Storage[key] = storageValue{value: value} + } + } + if value := storageHash(base.Storage, key); value != (common.Hash{}) { + acct.Storage[key] = storageValue{value: value} + } +} + +func (s *nativeStateDB) committedState(addr common.Address, key common.Hash) common.Hash { + if slots, ok := s.committedStorage[addr]; ok { + if value, committed := slots[key]; committed { + return value.value + } + } + if acct, ok := s.accounts[addr]; ok && acct.StorageCleared { + return common.Hash{} + } + base := s.baseAccount(addr) + if _, ok := base.Storage[key]; !ok { + if value := s.source.GetState(addr, key); value != (common.Hash{}) { + base.Storage[key] = storageValue{value: value} + } + } + return storageHash(base.Storage, key) +} + +func (s *nativeStateDB) markTxStorageWrite(addr common.Address, key common.Hash) { + if s.txStorageWrites == nil { + s.txStorageWrites = map[common.Address]map[common.Hash]struct{}{} + } + slots, ok := s.txStorageWrites[addr] + if !ok { + slots = map[common.Hash]struct{}{} + s.txStorageWrites[addr] = slots + } + slots[key] = struct{}{} +} + +func (s *nativeStateDB) markTxStorageClear(addr common.Address) { + if s.txStorageClears == nil { + s.txStorageClears = map[common.Address]struct{}{} + } + s.txStorageClears[addr] = struct{}{} +} + +func (s *nativeStateDB) finaliseTxStorage() { + if s.committedStorage == nil { + s.committedStorage = map[common.Address]map[common.Hash]storageValue{} + } + for addr := range s.txStorageClears { + s.committedStorage[addr] = map[common.Hash]storageValue{} + } + for addr, keys := range s.txStorageWrites { + if len(keys) == 0 { + continue + } + slots, ok := s.committedStorage[addr] + if !ok { + slots = map[common.Hash]storageValue{} + s.committedStorage[addr] = slots + } + acct := s.account(addr) + for key := range keys { + value, ok := acct.Storage[key] + if !ok { + delete(slots, key) + continue + } + slots[key] = value + } + } + clearNestedStorageWriteSets(s.txStorageWrites) + clear(s.txStorageClears) +} + +func (s *nativeStateDB) loadAccount(addr common.Address) *nativeAccount { + acct := &nativeAccount{ + Balance: uint256FromBig(s.source.GetBalance(addr)), + Nonce: s.source.GetNonce(addr), + Code: cloneBytes(s.source.GetCode(addr)), + Storage: map[common.Hash]storageValue{}, + } + return acct +} + +func (a *nativeAccount) clone() *nativeAccount { + if a == nil { + return &nativeAccount{Balance: uint256.NewInt(0), Storage: map[common.Hash]storageValue{}} + } + cp := &nativeAccount{ + Balance: uint256.NewInt(0), + Nonce: a.Nonce, + Code: cloneBytes(a.Code), + Storage: map[common.Hash]storageValue{}, + StorageCleared: a.StorageCleared, + 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 (al *accessList) reset() { + if al.addresses == nil { + al.addresses = map[common.Address]struct{}{} + } else { + clear(al.addresses) + } + if al.slots == nil { + al.slots = map[common.Address]map[common.Hash]struct{}{} + return + } + for _, slots := range al.slots { + clear(slots) + } + clear(al.slots) +} + +func cloneAccessList(al accessList) accessList { + cp := newAccessList() + for addr := range al.addresses { + 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 cloneStorageValueMaps(states map[common.Address]map[common.Hash]storageValue) map[common.Address]map[common.Hash]storageValue { + cp := make(map[common.Address]map[common.Hash]storageValue, len(states)) + for addr, slots := range states { + cp[addr] = map[common.Hash]storageValue{} + for key, value := range slots { + cp[addr][key] = value + } + } + return cp +} + +func cloneStorageWriteSet(states map[common.Address]map[common.Hash]struct{}) map[common.Address]map[common.Hash]struct{} { + cp := make(map[common.Address]map[common.Hash]struct{}, len(states)) + for addr, slots := range states { + cp[addr] = map[common.Hash]struct{}{} + for key := range slots { + cp[addr][key] = struct{}{} + } + } + return cp +} + +func cloneAddressSet(addrs map[common.Address]struct{}) map[common.Address]struct{} { + cp := make(map[common.Address]struct{}, len(addrs)) + for addr := range addrs { + cp[addr] = struct{}{} + } + return cp +} + +func cloneUint256Map(values map[common.Address]*uint256.Int) map[common.Address]*uint256.Int { + if values == nil { + return nil + } + cp := make(map[common.Address]*uint256.Int, len(values)) + for addr, value := range values { + if value == nil { + cp[addr] = uint256.NewInt(0) + } else { + cp[addr] = value.Clone() + } + } + return cp +} + +func cloneAccessSet(set map[stateAccessKey]struct{}) map[stateAccessKey]struct{} { + if set == nil { + return nil + } + cp := make(map[stateAccessKey]struct{}, len(set)) + for key := range set { + cp[key] = struct{}{} + } + return cp +} + +func clonePreimages(preimages map[common.Hash][]byte) map[common.Hash][]byte { + cp := make(map[common.Hash][]byte, len(preimages)) + for hash, preimage := range preimages { + cp[hash] = cloneBytes(preimage) + } + return cp +} + +func clearNestedHashMaps(values map[common.Address]map[common.Hash]common.Hash) { + for _, slots := range values { + clear(slots) + } + clear(values) +} + +func clearNestedStorageWriteSets(values map[common.Address]map[common.Hash]struct{}) { + for _, slots := range values { + clear(slots) + } + clear(values) +} + +func cloneJournal(journal []nativeJournalEntry) []nativeJournalEntry { + cp := make([]nativeJournalEntry, len(journal)) + for i, entry := range journal { + cp[i] = nativeJournalEntry{ + kind: entry.kind, + address: entry.address, + account: entry.account.clone(), + } + } + return cp +} + +func cloneSnapshots(snapshots []nativeSnapshot) []nativeSnapshot { + cp := make([]nativeSnapshot, len(snapshots)) + for i, snapshot := range snapshots { + cp[i] = nativeSnapshot{ + journalLen: snapshot.journalLen, + refund: snapshot.refund, + logsLen: snapshot.logsLen, + accessList: cloneAccessList(snapshot.accessList), + transientStates: cloneTransientStates(snapshot.transientStates), + finaliseAddrs: cloneAddressSet(snapshot.finaliseAddrs), + txStorageWrites: cloneStorageWriteSet(snapshot.txStorageWrites), + txStorageClears: cloneAddressSet(snapshot.txStorageClears), + commutativeBalanceDeltas: cloneUint256Map(snapshot.commutativeBalanceDeltas), + preimages: clonePreimages(snapshot.preimages), + journaledAddrs: cloneAddressSet(snapshot.journaledAddrs), + err: snapshot.err, + } + } + return cp +} + +func storageHash(values map[common.Hash]storageValue, key common.Hash) common.Hash { + return values[key].value +} + +func storageKeyUnion(a, b map[common.Hash]storageValue) []common.Hash { + seen := map[common.Hash]struct{}{} + for key := range a { + seen[key] = struct{}{} + } + 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 +} diff --git a/giga/evmonly/types.go b/giga/evmonly/types.go new file mode 100644 index 0000000000..b59acfe290 --- /dev/null +++ b/giga/evmonly/types.go @@ -0,0 +1,158 @@ +package evmonly + +import ( + "context" + "math/big" + + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" +) + +// BlockExecutor is the Cosmos-free block execution boundary for the EVM-only path. +type BlockExecutor interface { + ExecuteBlock(context.Context, BlockRequest) (*BlockResult, error) +} + +// PreparedBlockExecutor exposes the split transaction preparation and block +// execution phases. Preparation is stateless, so callers may pipeline it ahead +// of ordered block execution. +type PreparedBlockExecutor interface { + BlockExecutor + PrepareBlock(context.Context, BlockRequest) (PreparedBlock, error) + ExecutePreparedBlock(context.Context, PreparedBlock) (*BlockResult, error) +} + +// ResultSink persists executor-produced block outputs. The sink can retain the +// complete BlockResult without forcing the executor to copy changesets or +// receipts before handing them to an async sink. +// The sink must invoke release exactly once after it no longer references +// result. If StoreBlockResult returns an error, the executor releases that sink +// reference. +type ResultSink interface { + StoreBlockResult(ctx context.Context, height uint64, result *BlockResult, release func()) error +} + +// BlockRequest contains all consensus/runtime inputs needed to execute a block. +// Txs must be raw Ethereum transaction RLP bytes. +type BlockRequest struct { + Context BlockContext + Txs [][]byte +} + +// PreparedBlock contains decoded transactions with recovered senders. It is a +// trusted executor-produced value: ExecutePreparedBlock assumes callers pass the +// result of PrepareBlock unchanged and does not recover senders again. +type PreparedBlock struct { + Context BlockContext + Txs []PreparedTx +} + +// PreparedTx is the stateless per-transaction work needed before EVM execution. +// Sender is trusted data recovered by PrepareBlock. +type PreparedTx struct { + Tx *ethtypes.Transaction + Sender common.Address +} + +// 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 + OCCStats OCCStats + + lease *blockResultLease +} + +// Release returns a pooled BlockResult to its executor-owned pool. It is a +// no-op for results that were not allocated from a pool. +func (r *BlockResult) Release() { + if r == nil || r.lease == nil { + return + } + lease := r.lease + r.lease = nil + lease.release() +} + +// OCCStats reports optimistic concurrency control behavior for a block. +type OCCStats struct { + Attempted bool + Fallback bool + FallbackReason string + ConflictCount uint64 + ConflictSamples []OCCConflictCount +} + +// OCCConflictCount aggregates conflicts by the access key that forced OCC to +// fall back to sequential execution. +type OCCConflictCount struct { + Access string + Kind string + Address common.Address + Slot common.Hash + Count uint64 +} + +// StateChangeSet is the deterministic EVM-native state output for a block. +// Values are post-block values, not deltas. +type StateChangeSet struct { + Balances []BalanceChange + Nonces []NonceChange + Code []CodeChange + StorageClears []common.Address + 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 +} diff --git a/sei-cosmos/storev2/rootmulti/hash_consistency_test.go b/sei-cosmos/storev2/rootmulti/hash_consistency_test.go index 7ab5b95064..e01485c0f0 100644 --- a/sei-cosmos/storev2/rootmulti/hash_consistency_test.go +++ b/sei-cosmos/storev2/rootmulti/hash_consistency_test.go @@ -17,6 +17,7 @@ func TestCommitAndHistoricalQueryHashConsistency(t *testing.T) { ssConfig := seidbconfig.StateStoreConfig{} store := NewStore(t.TempDir(), scConfig, ssConfig, nil) + defer func() { require.NoError(t, store.Close()) }() keys := []string{"acc", "bank", "distribution", "staking", "ibc", "upgrade"} storeKeys := make(map[string]*types.KVStoreKey) @@ -102,6 +103,7 @@ func TestCommitAndHistoricalQueryWithDoubleFlush(t *testing.T) { ssConfig := seidbconfig.StateStoreConfig{} store := NewStore(t.TempDir(), scConfig, ssConfig, nil) + defer func() { require.NoError(t, store.Close()) }() keys := []string{"acc", "bank", "distribution", "staking", "ibc", "upgrade"} storeKeys := make(map[string]*types.KVStoreKey)