From 204a411d773d983cd52e4c7444141ee4a6664272 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Mon, 15 Jun 2026 11:27:57 +0800 Subject: [PATCH 01/27] scaffold evm-only giga executor path --- giga/evmonly/README.md | 86 ++++++++++++++++++++++++++ giga/evmonly/precompiles/context.go | 67 +++++++++++++++++++++ giga/evmonly/seiv3/config.go | 29 +++++++++ giga/evmonly/seiv3/executor.go | 38 ++++++++++++ giga/evmonly/seiv3/executor_test.go | 31 ++++++++++ giga/evmonly/seiv3/runtime.go | 7 +++ giga/evmonly/types.go | 93 +++++++++++++++++++++++++++++ 7 files changed, 351 insertions(+) create mode 100644 giga/evmonly/README.md create mode 100644 giga/evmonly/precompiles/context.go create mode 100644 giga/evmonly/seiv3/config.go create mode 100644 giga/evmonly/seiv3/executor.go create mode 100644 giga/evmonly/seiv3/executor_test.go create mode 100644 giga/evmonly/seiv3/runtime.go create mode 100644 giga/evmonly/types.go diff --git a/giga/evmonly/README.md b/giga/evmonly/README.md new file mode 100644 index 0000000000..231a6d9d65 --- /dev/null +++ b/giga/evmonly/README.md @@ -0,0 +1,86 @@ +# EVM-only execution scaffold + +This package sketches the EVM-only execution boundary for the final-form giga +path. It is intentionally separate from the current Cosmos-backed giga wiring in +`app/app.go`. + +The target execution model is based on the `sei-v3` executor: + +- raw transaction bytes are Ethereum RLP transactions, not Cosmos SDK txs +- state layout is EVM-native: balance, storage, code, and nonce are keyed by + EVM addresses and hashes +- block execution can overlap with parsing or persistence work for nearby blocks +- hot execution should not allocate `sdk.Context`, `sdk.Tx`, + `MsgEVMTransaction`, Cosmos messages, or Cosmos coins + +Custom precompiles are intentionally not implemented in this scaffold. The open +work is to port them behind an EVM-native context that is visible to the +executor's conflict tracking without reintroducing Cosmos keeper dependencies. + +## Executor interface + +The boundary is: + +```go +ExecuteBlock(context.Context, BlockRequest) (*BlockResult, error) +``` + +The executor should be commit-neutral. It executes an ordered EVM block and +returns the state writes and receipts produced by that block. The caller owns +durable persistence, state commitment, block indexing, and receipt publication. + +## Input block format + +`BlockRequest` is the expected input: + +- `Context` contains block-constant EVM fields: + - block number + - timestamp + - block gas limit + - chain ID + - base fee + - blob base fee, when enabled + - coinbase + - parent hash + - current block hash + - prevRandao +- `Txs` is the canonical, already-ordered transaction list for the block. +- Each tx is raw Ethereum transaction RLP bytes. +- There is no Cosmos SDK tx envelope, `MsgEVMTransaction`, ante wrapper, memo, + fee object, account address mapping object, or Cosmos gas meter in the input. + +The runtime or consensus layer is responsible for choosing the block order and +providing the block context. The executor is responsible for parsing tx RLP, +recovering senders, validating EVM nonce/fee/intrinsic-gas rules, executing EVM +state transitions, and producing deterministic outputs. + +## Output format + +`BlockResult` contains two primary outputs: + +- `ChangeSet`: the EVM-native state writes produced by the block. +- `Receipts`: Ethereum receipts for the executed transactions. + +`ChangeSet` is expressed as post-block values, not deltas: + +- `Balances`: final balance for each changed EVM address +- `Nonces`: final nonce for each changed EVM address +- `Code`: final bytecode updates or deletions +- `Storage`: final storage slot updates or deletions + +The changeset should be deterministic and serializable by the runtime layer. +The executor should not require `sdk.Context` or Cosmos stores to build it. + +`Receipts` are emitted in transaction order and should contain the Ethereum +receipt fields needed by RPC and indexing: status, cumulative gas used, bloom, +logs, tx hash, contract address, and effective gas price metadata where needed. + +`Txs` carries per-transaction execution metadata used to build or enrich +receipts and RPC responses. `GasUsed` is the total EVM gas consumed by the block. + +## Open precompile work + +Native custom precompiles still need a separate design. If they introduce state +outside balance, nonce, code, and storage, that state must either become part of +the EVM-native changeset or be represented through an explicit extension that is +visible to the OCC conflict tracker. diff --git a/giga/evmonly/precompiles/context.go b/giga/evmonly/precompiles/context.go new file mode 100644 index 0000000000..8ab405ad00 --- /dev/null +++ b/giga/evmonly/precompiles/context.go @@ -0,0 +1,67 @@ +package precompiles + +import ( + "errors" + "math/big" + + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" +) + +var ErrCustomPrecompilesOpen = errors.New("evm-only custom precompiles are not implemented") + +// Registry resolves native custom precompiles for the EVM-only path. +type Registry interface { + Get(common.Address) (Contract, bool) +} + +// Contract is the sdk.Context-free custom precompile interface. +type Contract interface { + RequiredGas(input []byte) uint64 + Run(*Context, []byte) ([]byte, error) +} + +// Context is the only execution context custom precompiles should receive in +// the EVM-only path. It deliberately excludes sdk.Context and Cosmos keepers. +type Context struct { + Caller common.Address + Address common.Address + ApparentValue *big.Int + ReadOnly bool + DelegateCall bool + GasRemaining uint64 + Block BlockContext + State State + Logs LogSink +} + +// BlockContext is the block data custom precompiles may read. +type BlockContext struct { + Number uint64 + Time uint64 + ChainID *big.Int + BaseFee *big.Int + BlobBaseFee *big.Int + Coinbase common.Address + PrevRandao common.Hash +} + +// State is the precompile-facing state API. Implementations must make these +// reads and writes visible to the executor's conflict tracking. +type State interface { + GetBalance(common.Address) *big.Int + AddBalance(common.Address, *big.Int) + SubBalance(common.Address, *big.Int) error + GetNonce(common.Address) uint64 + SetNonce(common.Address, uint64) + GetCode(common.Address) []byte + GetState(common.Address, common.Hash) common.Hash + SetState(common.Address, common.Hash, common.Hash) + GetCustom([]byte) ([]byte, bool) + SetCustom([]byte, []byte) +} + +// LogSink lets custom precompiles emit Ethereum logs without Cosmos events. +type LogSink interface { + AddLog(*ethtypes.Log) +} diff --git a/giga/evmonly/seiv3/config.go b/giga/evmonly/seiv3/config.go new file mode 100644 index 0000000000..034b8aba9c --- /dev/null +++ b/giga/evmonly/seiv3/config.go @@ -0,0 +1,29 @@ +package seiv3 + +import "math" + +// Config captures the sei-v3 executor knobs needed by the EVM-only path. +type Config struct { + OCCWorkers int + FlushBatchSize int + DisableNonceCheck bool + DisableGasPriceCheck bool +} + +func DefaultConfig() Config { + return Config{ + OCCWorkers: int(math.Min(12, float64(runtimeCPU()))), + FlushBatchSize: 100, + } +} + +func (c Config) WithDefaults() Config { + defaults := DefaultConfig() + if c.OCCWorkers == 0 { + c.OCCWorkers = defaults.OCCWorkers + } + if c.FlushBatchSize == 0 { + c.FlushBatchSize = defaults.FlushBatchSize + } + return c +} diff --git a/giga/evmonly/seiv3/executor.go b/giga/evmonly/seiv3/executor.go new file mode 100644 index 0000000000..cf0331f52a --- /dev/null +++ b/giga/evmonly/seiv3/executor.go @@ -0,0 +1,38 @@ +package seiv3 + +import ( + "context" + "fmt" + + "github.com/sei-protocol/sei-chain/giga/evmonly" +) + +// Executor is the placeholder for the sei-v3-derived EVM-only executor. +type Executor struct { + cfg Config +} + +func NewExecutor(cfg Config) *Executor { + return &Executor{cfg: cfg.WithDefaults()} +} + +func (e *Executor) Config() Config { + return e.cfg +} + +func (e *Executor) ExecuteBlock(ctx context.Context, req evmonly.BlockRequest) (*evmonly.BlockResult, error) { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + if len(req.Txs) == 0 { + return &evmonly.BlockResult{}, nil + } + + return nil, fmt.Errorf( + "%w: port sei-v3 parser, EVMC host context, OCC scheduler, EVM-native stores, and receipt pipeline", + evmonly.ErrNotImplemented, + ) +} diff --git a/giga/evmonly/seiv3/executor_test.go b/giga/evmonly/seiv3/executor_test.go new file mode 100644 index 0000000000..580321cb3c --- /dev/null +++ b/giga/evmonly/seiv3/executor_test.go @@ -0,0 +1,31 @@ +package seiv3 + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/sei-protocol/sei-chain/giga/evmonly" +) + +func TestExecutorEmptyBlock(t *testing.T) { + executor := NewExecutor(Config{OCCWorkers: 1}) + + result, err := executor.ExecuteBlock(context.Background(), evmonly.BlockRequest{}) + + require.NoError(t, err) + require.NotNil(t, result) +} + +func TestExecutorTxsRemainScaffoldOnly(t *testing.T) { + executor := NewExecutor(Config{OCCWorkers: 1}) + + _, err := executor.ExecuteBlock(context.Background(), evmonly.BlockRequest{ + Txs: [][]byte{{0x01}}, + }) + + require.Error(t, err) + require.True(t, errors.Is(err, evmonly.ErrNotImplemented)) +} diff --git a/giga/evmonly/seiv3/runtime.go b/giga/evmonly/seiv3/runtime.go new file mode 100644 index 0000000000..cd5d04da9b --- /dev/null +++ b/giga/evmonly/seiv3/runtime.go @@ -0,0 +1,7 @@ +package seiv3 + +import "runtime" + +func runtimeCPU() int { + return runtime.NumCPU() +} diff --git a/giga/evmonly/types.go b/giga/evmonly/types.go new file mode 100644 index 0000000000..c7c835b3ad --- /dev/null +++ b/giga/evmonly/types.go @@ -0,0 +1,93 @@ +package evmonly + +import ( + "context" + "errors" + "math/big" + + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" +) + +var ErrNotImplemented = errors.New("evm-only executor is not implemented") + +// Executor is the Cosmos-free block execution boundary for the EVM-only path. +type Executor interface { + ExecuteBlock(context.Context, BlockRequest) (*BlockResult, error) +} + +// BlockRequest contains all consensus/runtime inputs needed to execute a block. +// Txs must be raw Ethereum transaction RLP bytes. +type BlockRequest struct { + Context BlockContext + Txs [][]byte +} + +// BlockContext contains block-constant EVM execution data. +type BlockContext struct { + Number uint64 + Time uint64 + GasLimit uint64 + ChainID *big.Int + BaseFee *big.Int + BlobBaseFee *big.Int + Coinbase common.Address + ParentHash common.Hash + BlockHash common.Hash + PrevRandao common.Hash +} + +// BlockResult is the executor output consumed by the new runtime boundary. +type BlockResult struct { + ChangeSet StateChangeSet + Txs []TxResult + Receipts ethtypes.Receipts + GasUsed uint64 +} + +// StateChangeSet is the deterministic EVM-native state output for a block. +// Values are post-block values, not deltas. +type StateChangeSet struct { + Balances []BalanceChange + Nonces []NonceChange + Code []CodeChange + Storage []StorageChange +} + +type BalanceChange struct { + Address common.Address + Balance *big.Int +} + +type NonceChange struct { + Address common.Address + Nonce uint64 +} + +type CodeChange struct { + Address common.Address + Code []byte + Delete bool +} + +type StorageChange struct { + Address common.Address + Key common.Hash + Value common.Hash + Delete bool +} + +// TxResult is the minimum per-transaction output needed for receipts, RPC, and +// runtime result reporting. +type TxResult struct { + Hash common.Hash + Sender common.Address + To *common.Address + ContractAddress common.Address + Status uint64 + GasUsed uint64 + CumulativeGasUsed uint64 + EffectiveGasPrice *big.Int + Logs []*ethtypes.Log + Err error +} From 7314cc27e8a5b1ad5aacf5b297a0d36d686030a3 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Mon, 15 Jun 2026 13:38:30 +0800 Subject: [PATCH 02/27] port native evm-only giga executor --- giga/evmonly/README.md | 33 +- giga/evmonly/precompiles/context.go | 1 + giga/evmonly/seiv3/config.go | 16 +- giga/evmonly/seiv3/executor.go | 262 ++++++++++- giga/evmonly/seiv3/executor_test.go | 100 ++++- giga/evmonly/seiv3/parser.go | 44 ++ giga/evmonly/seiv3/state_db.go | 649 ++++++++++++++++++++++++++++ giga/evmonly/state.go | 171 ++++++++ 8 files changed, 1251 insertions(+), 25 deletions(-) create mode 100644 giga/evmonly/seiv3/parser.go create mode 100644 giga/evmonly/seiv3/state_db.go create mode 100644 giga/evmonly/state.go diff --git a/giga/evmonly/README.md b/giga/evmonly/README.md index 231a6d9d65..75aa00d30b 100644 --- a/giga/evmonly/README.md +++ b/giga/evmonly/README.md @@ -1,6 +1,6 @@ -# EVM-only execution scaffold +# EVM-only giga execution -This package sketches the EVM-only execution boundary for the final-form giga +This package contains the EVM-only execution boundary for the final-form giga path. It is intentionally separate from the current Cosmos-backed giga wiring in `app/app.go`. @@ -13,9 +13,11 @@ The target execution model is based on the `sei-v3` executor: - hot execution should not allocate `sdk.Context`, `sdk.Tx`, `MsgEVMTransaction`, Cosmos messages, or Cosmos coins -Custom precompiles are intentionally not implemented in this scaffold. The open -work is to port them behind an EVM-native context that is visible to the -executor's conflict tracking without reintroducing Cosmos keeper dependencies. +The current port executes raw RLP transactions with go-ethereum against an +EVM-native state backend, then returns a changeset plus Ethereum receipts. +Custom precompiles are still placeholders. The open work is to port them behind +an EVM-native context that is visible to the executor's conflict tracking +without reintroducing Cosmos keeper dependencies. ## Executor interface @@ -28,6 +30,9 @@ ExecuteBlock(context.Context, BlockRequest) (*BlockResult, error) The executor should be commit-neutral. It executes an ordered EVM block and returns the state writes and receipts produced by that block. The caller owns durable persistence, state commitment, block indexing, and receipt publication. +The `seiv3` implementation accepts a `StateReader` backend through +`WithState(...)`; callers can persist the returned `ChangeSet` with a matching +`StateWriter`. ## Input block format @@ -84,3 +89,21 @@ Native custom precompiles still need a separate design. If they introduce state outside balance, nonce, code, and storage, that state must either become part of the EVM-native changeset or be represented through an explicit extension that is visible to the OCC conflict tracker. + +The intended direction is to treat each custom precompile's migrated module +state as contract storage owned by that precompile address. With no range reads +and no side state, precompile reads and writes can then flow through ordinary +`(address, slot)` storage tracking. + +Until that design is implemented, the `seiv3` executor accepts a custom +precompile registry only as a fail-closed placeholder. Calls to registered +custom precompile addresses return `ErrCustomPrecompilesOpen`. + +## Current limitations + +- The current port is sequential. The EVM-native state boundary and changeset + shape are intended to be replaceable with the sei-v3 OCC scheduler/store. +- State input is key-addressable only. The executor lazily reads storage slots + by `(address, slot)` and does not require or expose range iteration. +- The map-backed `MemoryState` is for tests and early integration; production + should provide a durable native state backend. diff --git a/giga/evmonly/precompiles/context.go b/giga/evmonly/precompiles/context.go index 8ab405ad00..828937d6b1 100644 --- a/giga/evmonly/precompiles/context.go +++ b/giga/evmonly/precompiles/context.go @@ -13,6 +13,7 @@ var ErrCustomPrecompilesOpen = errors.New("evm-only custom precompiles are not i // Registry resolves native custom precompiles for the EVM-only path. type Registry interface { Get(common.Address) (Contract, bool) + Addresses() []common.Address } // Contract is the sdk.Context-free custom precompile interface. diff --git a/giga/evmonly/seiv3/config.go b/giga/evmonly/seiv3/config.go index 034b8aba9c..e19cafbd2b 100644 --- a/giga/evmonly/seiv3/config.go +++ b/giga/evmonly/seiv3/config.go @@ -1,6 +1,13 @@ package seiv3 -import "math" +import ( + "math" + "math/big" + + "github.com/ethereum/go-ethereum/params" + + "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles" +) // Config captures the sei-v3 executor knobs needed by the EVM-only path. type Config struct { @@ -8,12 +15,16 @@ type Config struct { FlushBatchSize int DisableNonceCheck bool DisableGasPriceCheck bool + MinGasPrice *big.Int + ChainConfig *params.ChainConfig + CustomPrecompiles precompiles.Registry } func DefaultConfig() Config { return Config{ OCCWorkers: int(math.Min(12, float64(runtimeCPU()))), FlushBatchSize: 100, + MinGasPrice: big.NewInt(1_000_000_000), } } @@ -25,5 +36,8 @@ func (c Config) WithDefaults() Config { if c.FlushBatchSize == 0 { c.FlushBatchSize = defaults.FlushBatchSize } + if c.MinGasPrice == nil { + c.MinGasPrice = new(big.Int).Set(defaults.MinGasPrice) + } return c } diff --git a/giga/evmonly/seiv3/executor.go b/giga/evmonly/seiv3/executor.go index cf0331f52a..52f2715e63 100644 --- a/giga/evmonly/seiv3/executor.go +++ b/giga/evmonly/seiv3/executor.go @@ -3,17 +3,45 @@ package seiv3 import ( "context" "fmt" + "math" + "math/big" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/tracing" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/params" "github.com/sei-protocol/sei-chain/giga/evmonly" + "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles" ) -// Executor is the placeholder for the sei-v3-derived EVM-only executor. +// Executor runs raw EVM transactions against an EVM-native state backend. type Executor struct { - cfg Config + cfg Config + state evmonly.StateReader } -func NewExecutor(cfg Config) *Executor { - return &Executor{cfg: cfg.WithDefaults()} +type Option func(*Executor) + +func WithState(state evmonly.StateReader) Option { + return func(e *Executor) { + if state != nil { + e.state = state + } + } +} + +func NewExecutor(cfg Config, opts ...Option) *Executor { + e := &Executor{ + cfg: cfg.WithDefaults(), + state: evmonly.NewMemoryState(), + } + for _, opt := range opts { + opt(e) + } + return e } func (e *Executor) Config() Config { @@ -21,18 +49,224 @@ func (e *Executor) Config() Config { } func (e *Executor) ExecuteBlock(ctx context.Context, req evmonly.BlockRequest) (*evmonly.BlockResult, error) { - select { - case <-ctx.Done(): - return nil, ctx.Err() - default: - } - if len(req.Txs) == 0 { return &evmonly.BlockResult{}, nil } - return nil, fmt.Errorf( - "%w: port sei-v3 parser, EVMC host context, OCC scheduler, EVM-native stores, and receipt pipeline", - evmonly.ErrNotImplemented, - ) + chainConfig := e.chainConfig(req.Context) + signer := ethtypes.MakeSigner(chainConfig, new(big.Int).SetUint64(req.Context.Number), req.Context.Time) + parsed, err := parseBlockTxs(ctx, req.Txs, signer) + if err != nil { + return nil, err + } + + stateDB := newNativeStateDB(e.state) + blockCtx := buildBlockContext(req.Context) + evm := vm.NewEVM(blockCtx, stateDB, chainConfig, vm.Config{}, customPrecompileMap(e.cfg.CustomPrecompiles)) + stateDB.SetEVM(evm) + + gasLimit := req.Context.GasLimit + if gasLimit == 0 { + gasLimit = math.MaxUint64 + } + gasPool := new(core.GasPool).AddGas(gasLimit) + baseFee := cloneBig(req.Context.BaseFee) + + result := &evmonly.BlockResult{ + Txs: make([]evmonly.TxResult, 0, len(parsed)), + Receipts: make(ethtypes.Receipts, 0, len(parsed)), + } + for txIndex, p := range parsed { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + txResult, receipt, err := e.executeTx(evm, stateDB, gasPool, req.Context, p, txIndex, baseFee) + if err != nil { + return nil, fmt.Errorf("execute tx %d %s: %w", txIndex, p.tx.Hash(), err) + } + txResult.CumulativeGasUsed = result.GasUsed + txResult.GasUsed + receipt.CumulativeGasUsed = txResult.CumulativeGasUsed + result.Txs = append(result.Txs, txResult) + result.Receipts = append(result.Receipts, receipt) + result.GasUsed += txResult.GasUsed + } + stateDB.Finalise(true) + result.ChangeSet = stateDB.ChangeSet() + return result, nil } + +func (e *Executor) executeTx( + evm *vm.EVM, + stateDB *nativeStateDB, + gasPool *core.GasPool, + block evmonly.BlockContext, + p parsedTx, + txIndex int, + baseFee *big.Int, +) (evmonly.TxResult, *ethtypes.Receipt, error) { + tx := p.tx + if e.cfg.CustomPrecompiles != nil && tx.To() != nil { + if _, ok := e.cfg.CustomPrecompiles.Get(*tx.To()); ok { + return evmonly.TxResult{Hash: tx.Hash(), Sender: p.sender, To: tx.To(), Err: precompiles.ErrCustomPrecompilesOpen}, + nil, + precompiles.ErrCustomPrecompilesOpen + } + } + if !e.cfg.DisableGasPriceCheck && e.cfg.MinGasPrice != nil { + if effectiveGasPrice(tx, baseFee).Cmp(e.cfg.MinGasPrice) < 0 { + return evmonly.TxResult{Hash: tx.Hash(), Sender: p.sender, To: tx.To(), Err: errInsufficientGasPrice}, + nil, + errInsufficientGasPrice + } + } + + msg, err := core.TransactionToMessage(tx, ethtypes.MakeSigner(e.chainConfig(block), new(big.Int).SetUint64(block.Number), block.Time), baseFee) + if err != nil { + return evmonly.TxResult{Hash: tx.Hash(), Sender: p.sender, To: tx.To(), Err: err}, nil, err + } + msg.SkipNonceChecks = e.cfg.DisableNonceCheck + + stateDB.SetTxContext(tx.Hash(), txIndex) + logStart := len(stateDB.logs) + evm.SetTxContext(core.NewEVMTxContext(msg)) + execResult, err := core.ApplyMessage(evm, msg, gasPool) + if err != nil { + return evmonly.TxResult{Hash: tx.Hash(), Sender: p.sender, To: tx.To(), Err: err}, nil, err + } + + txLogs := append([]*ethtypes.Log(nil), stateDB.logs[logStart:]...) + for _, log := range txLogs { + log.BlockNumber = block.Number + log.BlockHash = block.BlockHash + log.TxHash = tx.Hash() + log.TxIndex = uint(txIndex) + } + + status := ethtypes.ReceiptStatusSuccessful + if execResult.Failed() { + status = ethtypes.ReceiptStatusFailed + } + receipt := ðtypes.Receipt{ + Type: tx.Type(), + Status: status, + Logs: txLogs, + TxHash: tx.Hash(), + GasUsed: execResult.UsedGas, + EffectiveGasPrice: effectiveGasPrice(tx, baseFee), + BlockHash: block.BlockHash, + BlockNumber: new(big.Int).SetUint64(block.Number), + TransactionIndex: uint(txIndex), + } + if tx.To() == nil { + receipt.ContractAddress = crypto.CreateAddress(p.sender, tx.Nonce()) + } + receipt.Bloom = ethtypes.CreateBloom(receipt) + + txResult := evmonly.TxResult{ + Hash: tx.Hash(), + Sender: p.sender, + To: tx.To(), + ContractAddress: receipt.ContractAddress, + Status: status, + GasUsed: execResult.UsedGas, + EffectiveGasPrice: new(big.Int).Set(receipt.EffectiveGasPrice), + Logs: txLogs, + Err: execResult.Err, + } + return txResult, receipt, nil +} + +func buildBlockContext(ctx evmonly.BlockContext) vm.BlockContext { + prevRandao := ctx.PrevRandao + baseFee := cloneBig(ctx.BaseFee) + blobBaseFee := cloneBig(ctx.BlobBaseFee) + gasLimit := ctx.GasLimit + if gasLimit == 0 { + gasLimit = math.MaxUint64 + } + return vm.BlockContext{ + CanTransfer: core.CanTransfer, + Transfer: core.Transfer, + GetHash: func(n uint64) common.Hash { + switch { + case n == ctx.Number: + return ctx.BlockHash + case ctx.Number > 0 && n == ctx.Number-1: + return ctx.ParentHash + default: + return common.Hash{} + } + }, + Coinbase: ctx.Coinbase, + GasLimit: gasLimit, + BlockNumber: new(big.Int).SetUint64(ctx.Number), + Time: ctx.Time, + Difficulty: new(big.Int), + BaseFee: baseFee, + BlobBaseFee: blobBaseFee, + Random: &prevRandao, + } +} + +type unresolvedCustomPrecompile struct{} + +func (unresolvedCustomPrecompile) RequiredGas([]byte) uint64 { + return 0 +} + +func (unresolvedCustomPrecompile) Run(*vm.EVM, common.Address, common.Address, []byte, *big.Int, bool, bool, *tracing.Hooks) ([]byte, error) { + return nil, precompiles.ErrCustomPrecompilesOpen +} + +func customPrecompileMap(registry precompiles.Registry) map[common.Address]vm.PrecompiledContract { + if registry == nil { + return nil + } + addresses := registry.Addresses() + if len(addresses) == 0 { + return nil + } + contracts := make(map[common.Address]vm.PrecompiledContract, len(addresses)) + for _, addr := range addresses { + contracts[addr] = unresolvedCustomPrecompile{} + } + return contracts +} + +func (e *Executor) chainConfig(ctx evmonly.BlockContext) *params.ChainConfig { + var cfg params.ChainConfig + if e.cfg.ChainConfig != nil { + cfg = *e.cfg.ChainConfig + } else { + cfg = *params.AllDevChainProtocolChanges + } + if ctx.ChainID != nil { + cfg.ChainID = new(big.Int).Set(ctx.ChainID) + } else if cfg.ChainID != nil { + cfg.ChainID = new(big.Int).Set(cfg.ChainID) + } else { + cfg.ChainID = big.NewInt(1) + } + return &cfg +} + +func effectiveGasPrice(tx *ethtypes.Transaction, baseFee *big.Int) *big.Int { + if baseFee == nil { + return tx.GasPrice() + } + if tx.Type() == ethtypes.DynamicFeeTxType || tx.Type() == ethtypes.BlobTxType || tx.Type() == ethtypes.SetCodeTxType { + return new(big.Int).Add(baseFee, tx.EffectiveGasTipValue(baseFee)) + } + return tx.GasPrice() +} + +func cloneBig(v *big.Int) *big.Int { + if v == nil { + return new(big.Int) + } + return new(big.Int).Set(v) +} + +var errInsufficientGasPrice = fmt.Errorf("insufficient gas price") diff --git a/giga/evmonly/seiv3/executor_test.go b/giga/evmonly/seiv3/executor_test.go index 580321cb3c..6664103fae 100644 --- a/giga/evmonly/seiv3/executor_test.go +++ b/giga/evmonly/seiv3/executor_test.go @@ -2,12 +2,18 @@ package seiv3 import ( "context" + "crypto/ecdsa" "errors" + "math/big" "testing" + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" "github.com/stretchr/testify/require" "github.com/sei-protocol/sei-chain/giga/evmonly" + "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles" ) func TestExecutorEmptyBlock(t *testing.T) { @@ -19,13 +25,97 @@ func TestExecutorEmptyBlock(t *testing.T) { require.NotNil(t, result) } -func TestExecutorTxsRemainScaffoldOnly(t *testing.T) { - executor := NewExecutor(Config{OCCWorkers: 1}) +func TestExecutorTransferTx(t *testing.T) { + chainID := big.NewInt(713715) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + recipient := common.HexToAddress("0x00000000000000000000000000000000000000a1") + + state := evmonly.NewMemoryState() + state.SetBalance(sender, big.NewInt(200_000_000_000_000)) + + rawTx := signLegacyTx(t, key, chainID, 0, &recipient, big.NewInt(7), nil) + executor := NewExecutor(Config{OCCWorkers: 1}, WithState(state)) + + result, err := executor.ExecuteBlock(context.Background(), evmonly.BlockRequest{ + Context: blockContext(chainID), + Txs: [][]byte{rawTx}, + }) + + require.NoError(t, err) + require.Len(t, result.Txs, 1) + require.Len(t, result.Receipts, 1) + require.Equal(t, ethtypes.ReceiptStatusSuccessful, result.Txs[0].Status) + require.Equal(t, uint64(21_000), result.GasUsed) + require.NotEmpty(t, result.ChangeSet.Balances) + + state.ApplyChangeSet(result.ChangeSet) + require.Equal(t, big.NewInt(7), state.GetBalance(recipient)) + require.Equal(t, uint64(1), state.GetNonce(sender)) +} + +func TestExecutorCustomPrecompilePlaceholder(t *testing.T) { + chainID := big.NewInt(713715) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + customAddr := common.HexToAddress("0x0000000000000000000000000000000000001001") + + state := evmonly.NewMemoryState() + state.SetBalance(sender, big.NewInt(200_000_000_000_000)) + + rawTx := signLegacyTx(t, key, chainID, 0, &customAddr, big.NewInt(0), []byte{0x01}) + executor := NewExecutor(Config{ + OCCWorkers: 1, + CustomPrecompiles: staticPrecompileRegistry{addr: customAddr}, + }, WithState(state)) - _, err := executor.ExecuteBlock(context.Background(), evmonly.BlockRequest{ - Txs: [][]byte{{0x01}}, + _, err = executor.ExecuteBlock(context.Background(), evmonly.BlockRequest{ + Context: blockContext(chainID), + Txs: [][]byte{rawTx}, }) require.Error(t, err) - require.True(t, errors.Is(err, evmonly.ErrNotImplemented)) + require.True(t, errors.Is(err, precompiles.ErrCustomPrecompilesOpen)) +} + +func signLegacyTx(t *testing.T, key *ecdsa.PrivateKey, chainID *big.Int, nonce uint64, to *common.Address, value *big.Int, data []byte) []byte { + t.Helper() + tx := ethtypes.NewTx(ðtypes.LegacyTx{ + Nonce: nonce, + GasPrice: big.NewInt(1_000_000_000), + Gas: 100_000, + To: to, + Value: value, + Data: data, + }) + signed, err := ethtypes.SignTx(tx, ethtypes.LatestSignerForChainID(chainID), key) + require.NoError(t, err) + raw, err := signed.MarshalBinary() + require.NoError(t, err) + return raw +} + +func blockContext(chainID *big.Int) evmonly.BlockContext { + return evmonly.BlockContext{ + Number: 1, + Time: 1, + GasLimit: 30_000_000, + ChainID: chainID, + BaseFee: big.NewInt(0), + Coinbase: common.HexToAddress("0x00000000000000000000000000000000000000cb"), + } +} + +type staticPrecompileRegistry struct { + addr common.Address +} + +func (r staticPrecompileRegistry) Get(addr common.Address) (precompiles.Contract, bool) { + return nil, addr == r.addr +} + +func (r staticPrecompileRegistry) Addresses() []common.Address { + return []common.Address{r.addr} } diff --git a/giga/evmonly/seiv3/parser.go b/giga/evmonly/seiv3/parser.go new file mode 100644 index 0000000000..76a4da311a --- /dev/null +++ b/giga/evmonly/seiv3/parser.go @@ -0,0 +1,44 @@ +package seiv3 + +import ( + "context" + "fmt" + + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/rlp" +) + +type parsedTx struct { + tx *ethtypes.Transaction + sender common.Address +} + +func parseBlockTxs(ctx context.Context, txs [][]byte, signer ethtypes.Signer) ([]parsedTx, error) { + parsed := make([]parsedTx, len(txs)) + for i, raw := range txs { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + tx, sender, err := parseTx(raw, signer) + if err != nil { + return nil, fmt.Errorf("parse tx %d: %w", i, err) + } + parsed[i] = parsedTx{tx: tx, sender: sender} + } + return parsed, nil +} + +func parseTx(raw []byte, signer ethtypes.Signer) (*ethtypes.Transaction, common.Address, error) { + var tx ethtypes.Transaction + if err := rlp.DecodeBytes(raw, &tx); err != nil { + return nil, common.Address{}, err + } + sender, err := ethtypes.Sender(signer, &tx) + if err != nil { + return nil, common.Address{}, err + } + return &tx, sender, nil +} diff --git a/giga/evmonly/seiv3/state_db.go b/giga/evmonly/seiv3/state_db.go new file mode 100644 index 0000000000..a641ea7f10 --- /dev/null +++ b/giga/evmonly/seiv3/state_db.go @@ -0,0 +1,649 @@ +package seiv3 + +import ( + "bytes" + "errors" + "math/big" + "sort" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/stateless" + "github.com/ethereum/go-ethereum/core/tracing" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/params" + ethutils "github.com/ethereum/go-ethereum/trie/utils" + "github.com/holiman/uint256" + + "github.com/sei-protocol/sei-chain/giga/evmonly" +) + +var errInsufficientBalance = errors.New("insufficient balance") + +type nativeStateDB struct { + source evmonly.StateReader + + accounts map[common.Address]*nativeAccount + base map[common.Address]*nativeAccount + + refund uint64 + logs []*ethtypes.Log + preimages map[common.Hash][]byte + + accessList accessList + transientStates map[common.Address]map[common.Hash]common.Hash + snapshots []nativeSnapshot + + txHash common.Hash + txIndex int + err error + evm *vm.EVM +} + +type nativeAccount struct { + Balance *uint256.Int + Nonce uint64 + Code []byte + Storage map[common.Hash]common.Hash + SelfDestructed bool + Created bool +} + +type nativeSnapshot struct { + accounts map[common.Address]*nativeAccount + refund uint64 + logs []*ethtypes.Log + accessList accessList + transientStates map[common.Address]map[common.Hash]common.Hash + preimages map[common.Hash][]byte + err error +} + +type accessList struct { + addresses map[common.Address]struct{} + slots map[common.Address]map[common.Hash]struct{} +} + +func newNativeStateDB(source evmonly.StateReader) *nativeStateDB { + if source == nil { + source = evmonly.NewMemoryState() + } + return &nativeStateDB{ + source: source, + accounts: map[common.Address]*nativeAccount{}, + base: map[common.Address]*nativeAccount{}, + preimages: map[common.Hash][]byte{}, + accessList: newAccessList(), + transientStates: map[common.Address]map[common.Hash]common.Hash{}, + } +} + +func (s *nativeStateDB) ChangeSet() evmonly.StateChangeSet { + addresses := make([]common.Address, 0, len(s.accounts)) + for addr := range s.accounts { + addresses = append(addresses, addr) + } + sort.Slice(addresses, func(i, j int) bool { + return bytes.Compare(addresses[i][:], addresses[j][:]) < 0 + }) + + var changes evmonly.StateChangeSet + for _, addr := range addresses { + acct := s.accounts[addr] + base := s.baseAccount(addr) + + if !acct.Balance.Eq(base.Balance) { + changes.Balances = append(changes.Balances, evmonly.BalanceChange{ + Address: addr, + Balance: acct.Balance.ToBig(), + }) + } + if acct.Nonce != base.Nonce { + changes.Nonces = append(changes.Nonces, evmonly.NonceChange{ + Address: addr, + Nonce: acct.Nonce, + }) + } + if !bytes.Equal(acct.Code, base.Code) { + changes.Code = append(changes.Code, evmonly.CodeChange{ + Address: addr, + Code: cloneBytes(acct.Code), + Delete: len(acct.Code) == 0, + }) + } + storageKeys := storageKeyUnion(base.Storage, acct.Storage) + for _, key := range storageKeys { + oldValue := base.Storage[key] + newValue := acct.Storage[key] + if oldValue == newValue { + continue + } + changes.Storage = append(changes.Storage, evmonly.StorageChange{ + Address: addr, + Key: key, + Value: newValue, + Delete: newValue == (common.Hash{}), + }) + } + } + return changes +} + +func (s *nativeStateDB) CreateAccount(addr common.Address) { + acct := s.account(addr) + balance := acct.Balance.Clone() + *acct = nativeAccount{ + Balance: balance, + Storage: map[common.Hash]common.Hash{}, + Created: true, + } +} + +func (s *nativeStateDB) CreateContract(addr common.Address) { + s.account(addr).Created = true +} + +func (s *nativeStateDB) SubBalance(addr common.Address, amount *uint256.Int, _ tracing.BalanceChangeReason) uint256.Int { + prev := *s.GetBalance(addr) + if amount == nil || amount.IsZero() { + return prev + } + acct := s.account(addr) + if acct.Balance.Cmp(amount) < 0 { + s.err = errInsufficientBalance + acct.Balance.Clear() + return prev + } + acct.Balance.Sub(acct.Balance, amount) + return prev +} + +func (s *nativeStateDB) AddBalance(addr common.Address, amount *uint256.Int, _ tracing.BalanceChangeReason) uint256.Int { + prev := *s.GetBalance(addr) + if amount == nil || amount.IsZero() { + return prev + } + acct := s.account(addr) + acct.Balance.Add(acct.Balance, amount) + return prev +} + +func (s *nativeStateDB) GetBalance(addr common.Address) *uint256.Int { + return s.account(addr).Balance.Clone() +} + +func (s *nativeStateDB) SetBalance(addr common.Address, balance *uint256.Int, _ tracing.BalanceChangeReason) { + acct := s.account(addr) + if balance == nil { + acct.Balance = uint256.NewInt(0) + return + } + acct.Balance = balance.Clone() +} + +func (s *nativeStateDB) GetNonce(addr common.Address) uint64 { + return s.account(addr).Nonce +} + +func (s *nativeStateDB) SetNonce(addr common.Address, nonce uint64, _ tracing.NonceChangeReason) { + s.account(addr).Nonce = nonce +} + +func (s *nativeStateDB) GetCodeHash(addr common.Address) common.Hash { + code := s.GetCode(addr) + if len(code) == 0 { + return common.Hash{} + } + return crypto.Keccak256Hash(code) +} + +func (s *nativeStateDB) GetCode(addr common.Address) []byte { + return cloneBytes(s.account(addr).Code) +} + +func (s *nativeStateDB) SetCode(addr common.Address, code []byte) []byte { + acct := s.account(addr) + prev := cloneBytes(acct.Code) + acct.Code = cloneBytes(code) + return prev +} + +func (s *nativeStateDB) GetCodeSize(addr common.Address) int { + return len(s.account(addr).Code) +} + +func (s *nativeStateDB) AddRefund(gas uint64) { + s.refund += gas +} + +func (s *nativeStateDB) SubRefund(gas uint64) { + if gas > s.refund { + panic("refund counter underflow") + } + s.refund -= gas +} + +func (s *nativeStateDB) GetRefund() uint64 { + return s.refund +} + +func (s *nativeStateDB) GetCommittedState(addr common.Address, key common.Hash) common.Hash { + s.ensureStorage(addr, key) + return s.baseAccount(addr).Storage[key] +} + +func (s *nativeStateDB) GetState(addr common.Address, key common.Hash) common.Hash { + s.ensureStorage(addr, key) + return s.account(addr).Storage[key] +} + +func (s *nativeStateDB) SetState(addr common.Address, key common.Hash, value common.Hash) common.Hash { + s.ensureStorage(addr, key) + acct := s.account(addr) + prev := acct.Storage[key] + if value == (common.Hash{}) { + delete(acct.Storage, key) + } else { + acct.Storage[key] = value + } + return prev +} + +func (s *nativeStateDB) SetStorage(addr common.Address, states map[common.Hash]common.Hash) { + acct := s.account(addr) + acct.Storage = map[common.Hash]common.Hash{} + for key, value := range states { + if value != (common.Hash{}) { + acct.Storage[key] = value + } + } +} + +func (s *nativeStateDB) GetStorageRoot(common.Address) common.Hash { + return common.Hash{} +} + +func (s *nativeStateDB) GetTransientState(addr common.Address, key common.Hash) common.Hash { + if states, ok := s.transientStates[addr]; ok { + return states[key] + } + return common.Hash{} +} + +func (s *nativeStateDB) SetTransientState(addr common.Address, key, value common.Hash) { + states, ok := s.transientStates[addr] + if !ok { + states = map[common.Hash]common.Hash{} + s.transientStates[addr] = states + } + if value == (common.Hash{}) { + delete(states, key) + return + } + states[key] = value +} + +func (s *nativeStateDB) SelfDestruct(addr common.Address) uint256.Int { + acct := s.account(addr) + prev := *acct.Balance.Clone() + acct.Balance.Clear() + acct.SelfDestructed = true + return prev +} + +func (s *nativeStateDB) SelfDestruct6780(addr common.Address) (uint256.Int, bool) { + if !s.account(addr).Created { + return *uint256.NewInt(0), false + } + return s.SelfDestruct(addr), true +} + +func (s *nativeStateDB) HasSelfDestructed(addr common.Address) bool { + return s.account(addr).SelfDestructed +} + +func (s *nativeStateDB) Exist(addr common.Address) bool { + acct := s.account(addr) + return acct.SelfDestructed || acct.Nonce != 0 || !acct.Balance.IsZero() || len(acct.Code) != 0 +} + +func (s *nativeStateDB) Empty(addr common.Address) bool { + acct := s.account(addr) + return acct.Nonce == 0 && acct.Balance.IsZero() && len(acct.Code) == 0 +} + +func (s *nativeStateDB) AddressInAccessList(addr common.Address) bool { + _, ok := s.accessList.addresses[addr] + return ok +} + +func (s *nativeStateDB) SlotInAccessList(addr common.Address, slot common.Hash) (bool, bool) { + _, addressOk := s.accessList.addresses[addr] + if !addressOk { + return false, false + } + slots, ok := s.accessList.slots[addr] + if !ok { + return true, false + } + _, slotOk := slots[slot] + return true, slotOk +} + +func (s *nativeStateDB) AddAddressToAccessList(addr common.Address) { + s.accessList.addresses[addr] = struct{}{} +} + +func (s *nativeStateDB) AddSlotToAccessList(addr common.Address, slot common.Hash) { + s.AddAddressToAccessList(addr) + slots, ok := s.accessList.slots[addr] + if !ok { + slots = map[common.Hash]struct{}{} + s.accessList.slots[addr] = slots + } + slots[slot] = struct{}{} +} + +func (s *nativeStateDB) Prepare(_ params.Rules, sender, coinbase common.Address, dest *common.Address, precompiles []common.Address, txAccesses ethtypes.AccessList) { + s.accessList = newAccessList() + s.AddAddressToAccessList(sender) + s.AddAddressToAccessList(coinbase) + if dest != nil { + s.AddAddressToAccessList(*dest) + } + for _, addr := range precompiles { + s.AddAddressToAccessList(addr) + } + for _, tuple := range txAccesses { + s.AddAddressToAccessList(tuple.Address) + for _, key := range tuple.StorageKeys { + s.AddSlotToAccessList(tuple.Address, key) + } + } +} + +func (s *nativeStateDB) PointCache() *ethutils.PointCache { + return nil +} + +func (s *nativeStateDB) Snapshot() int { + id := len(s.snapshots) + s.snapshots = append(s.snapshots, nativeSnapshot{ + accounts: cloneAccounts(s.accounts), + refund: s.refund, + logs: append([]*ethtypes.Log(nil), s.logs...), + accessList: cloneAccessList(s.accessList), + transientStates: cloneTransientStates(s.transientStates), + preimages: clonePreimages(s.preimages), + err: s.err, + }) + return id +} + +func (s *nativeStateDB) RevertToSnapshot(id int) { + if id < 0 || id >= len(s.snapshots) { + panic("invalid state snapshot") + } + snapshot := s.snapshots[id] + s.accounts = cloneAccounts(snapshot.accounts) + s.refund = snapshot.refund + s.logs = append([]*ethtypes.Log(nil), snapshot.logs...) + s.accessList = cloneAccessList(snapshot.accessList) + s.transientStates = cloneTransientStates(snapshot.transientStates) + s.preimages = clonePreimages(snapshot.preimages) + s.err = snapshot.err + s.snapshots = s.snapshots[:id] +} + +func (s *nativeStateDB) AddLog(log *ethtypes.Log) { + log.TxHash = s.txHash + log.TxIndex = uint(s.txIndex) + log.Index = uint(len(s.logs)) + s.logs = append(s.logs, log) +} + +func (s *nativeStateDB) AddPreimage(hash common.Hash, preimage []byte) { + s.preimages[hash] = cloneBytes(preimage) +} + +func (s *nativeStateDB) Witness() *stateless.Witness { + return nil +} + +func (s *nativeStateDB) AccessEvents() *vm.AccessEvents { + return nil +} + +func (s *nativeStateDB) Finalise(bool) { + for _, acct := range s.accounts { + if acct.SelfDestructed { + acct.Code = nil + acct.Storage = map[common.Hash]common.Hash{} + } + } +} + +func (s *nativeStateDB) Error() error { + return s.err +} + +func (s *nativeStateDB) Commit(uint64, bool, bool) (common.Hash, error) { + return common.Hash{}, s.err +} + +func (s *nativeStateDB) SetTxContext(hash common.Hash, index int) { + s.txHash = hash + s.txIndex = index +} + +func (s *nativeStateDB) Copy() vm.StateDB { + cp := &nativeStateDB{ + source: s.source, + accounts: cloneAccounts(s.accounts), + base: cloneAccounts(s.base), + refund: s.refund, + logs: append([]*ethtypes.Log(nil), s.logs...), + preimages: clonePreimages(s.preimages), + accessList: cloneAccessList(s.accessList), + transientStates: cloneTransientStates(s.transientStates), + snapshots: cloneSnapshots(s.snapshots), + txHash: s.txHash, + txIndex: s.txIndex, + err: s.err, + evm: s.evm, + } + return cp +} + +func (s *nativeStateDB) IntermediateRoot(bool) common.Hash { + return common.Hash{} +} + +func (s *nativeStateDB) GetLogs(common.Hash, uint64, common.Hash) []*ethtypes.Log { + return s.Logs() +} + +func (s *nativeStateDB) TxIndex() int { + return s.txIndex +} + +func (s *nativeStateDB) Preimages() map[common.Hash][]byte { + return clonePreimages(s.preimages) +} + +func (s *nativeStateDB) Logs() []*ethtypes.Log { + return append([]*ethtypes.Log(nil), s.logs...) +} + +func (s *nativeStateDB) SetEVM(evm *vm.EVM) { + s.evm = evm +} + +func (s *nativeStateDB) account(addr common.Address) *nativeAccount { + if acct, ok := s.accounts[addr]; ok { + return acct + } + acct := s.loadAccount(addr) + s.accounts[addr] = acct.clone() + s.base[addr] = acct.clone() + return s.accounts[addr] +} + +func (s *nativeStateDB) baseAccount(addr common.Address) *nativeAccount { + if acct, ok := s.base[addr]; ok { + return acct + } + acct := s.loadAccount(addr) + s.base[addr] = acct.clone() + return s.base[addr] +} + +func (s *nativeStateDB) ensureStorage(addr common.Address, key common.Hash) { + base := s.baseAccount(addr) + if _, ok := base.Storage[key]; !ok { + if value := s.source.GetState(addr, key); value != (common.Hash{}) { + base.Storage[key] = value + } + } + acct := s.account(addr) + if _, ok := acct.Storage[key]; !ok { + if value := base.Storage[key]; value != (common.Hash{}) { + acct.Storage[key] = value + } + } +} + +func (s *nativeStateDB) loadAccount(addr common.Address) *nativeAccount { + acct := &nativeAccount{ + Balance: uint256FromBig(s.source.GetBalance(addr)), + Nonce: s.source.GetNonce(addr), + Code: cloneBytes(s.source.GetCode(addr)), + Storage: map[common.Hash]common.Hash{}, + } + return acct +} + +func (a *nativeAccount) clone() *nativeAccount { + if a == nil { + return &nativeAccount{Balance: uint256.NewInt(0), Storage: map[common.Hash]common.Hash{}} + } + cp := &nativeAccount{ + Balance: uint256.NewInt(0), + Nonce: a.Nonce, + Code: cloneBytes(a.Code), + Storage: map[common.Hash]common.Hash{}, + SelfDestructed: a.SelfDestructed, + Created: a.Created, + } + if a.Balance != nil { + cp.Balance = a.Balance.Clone() + } + for key, value := range a.Storage { + cp.Storage[key] = value + } + return cp +} + +func newAccessList() accessList { + return accessList{ + addresses: map[common.Address]struct{}{}, + slots: map[common.Address]map[common.Hash]struct{}{}, + } +} + +func cloneAccessList(al accessList) accessList { + cp := newAccessList() + for addr := range al.addresses { + cp.addresses[addr] = struct{}{} + } + for addr, slots := range al.slots { + cp.slots[addr] = map[common.Hash]struct{}{} + for slot := range slots { + cp.slots[addr][slot] = struct{}{} + } + } + return cp +} + +func cloneAccounts(accounts map[common.Address]*nativeAccount) map[common.Address]*nativeAccount { + cp := make(map[common.Address]*nativeAccount, len(accounts)) + for addr, acct := range accounts { + cp[addr] = acct.clone() + } + return cp +} + +func cloneTransientStates(states map[common.Address]map[common.Hash]common.Hash) map[common.Address]map[common.Hash]common.Hash { + cp := make(map[common.Address]map[common.Hash]common.Hash, len(states)) + for addr, slots := range states { + cp[addr] = map[common.Hash]common.Hash{} + for key, value := range slots { + cp[addr][key] = value + } + } + return cp +} + +func clonePreimages(preimages map[common.Hash][]byte) map[common.Hash][]byte { + cp := make(map[common.Hash][]byte, len(preimages)) + for hash, preimage := range preimages { + cp[hash] = cloneBytes(preimage) + } + return cp +} + +func cloneSnapshots(snapshots []nativeSnapshot) []nativeSnapshot { + cp := make([]nativeSnapshot, len(snapshots)) + for i, snapshot := range snapshots { + cp[i] = nativeSnapshot{ + accounts: cloneAccounts(snapshot.accounts), + refund: snapshot.refund, + logs: append([]*ethtypes.Log(nil), snapshot.logs...), + accessList: cloneAccessList(snapshot.accessList), + transientStates: cloneTransientStates(snapshot.transientStates), + preimages: clonePreimages(snapshot.preimages), + err: snapshot.err, + } + } + return cp +} + +func storageKeyUnion(a, b map[common.Hash]common.Hash) []common.Hash { + seen := map[common.Hash]struct{}{} + for key := range a { + seen[key] = struct{}{} + } + for key := range b { + seen[key] = struct{}{} + } + keys := make([]common.Hash, 0, len(seen)) + for key := range seen { + keys = append(keys, key) + } + sort.Slice(keys, func(i, j int) bool { + return bytes.Compare(keys[i][:], keys[j][:]) < 0 + }) + return keys +} + +func uint256FromBig(v *big.Int) *uint256.Int { + if v == nil { + return uint256.NewInt(0) + } + u, overflow := uint256.FromBig(v) + if overflow { + panic("state balance exceeds uint256") + } + if u == nil { + return uint256.NewInt(0) + } + return u +} + +func cloneBytes(v []byte) []byte { + if len(v) == 0 { + return nil + } + return append([]byte(nil), v...) +} diff --git a/giga/evmonly/state.go b/giga/evmonly/state.go new file mode 100644 index 0000000000..8e92570166 --- /dev/null +++ b/giga/evmonly/state.go @@ -0,0 +1,171 @@ +package evmonly + +import ( + "math/big" + "sync" + + "github.com/ethereum/go-ethereum/common" +) + +// StateReader supplies EVM-native state to an executor. +type StateReader interface { + GetBalance(common.Address) *big.Int + GetNonce(common.Address) uint64 + GetCode(common.Address) []byte + GetState(common.Address, common.Hash) common.Hash +} + +// StateWriter persists an executor-produced changeset. +type StateWriter interface { + ApplyChangeSet(StateChangeSet) +} + +// StateBackend is the minimal state boundary needed by the EVM-only executor. +type StateBackend interface { + StateReader + StateWriter +} + +// MemoryState is a small EVM-native state backend for tests and early wiring. +type MemoryState struct { + mu sync.RWMutex + accounts map[common.Address]*StateAccount +} + +// StateAccount is an EVM-native account snapshot. +type StateAccount struct { + Balance *big.Int + Nonce uint64 + Code []byte + Storage map[common.Hash]common.Hash +} + +func NewMemoryState() *MemoryState { + return &MemoryState{accounts: map[common.Address]*StateAccount{}} +} + +func (s *MemoryState) GetBalance(addr common.Address) *big.Int { + s.mu.RLock() + defer s.mu.RUnlock() + if acct, ok := s.accounts[addr]; ok && acct.Balance != nil { + return new(big.Int).Set(acct.Balance) + } + return new(big.Int) +} + +func (s *MemoryState) SetBalance(addr common.Address, balance *big.Int) { + s.mu.Lock() + defer s.mu.Unlock() + acct := s.getOrCreateAccountLocked(addr) + acct.Balance = cloneBig(balance) +} + +func (s *MemoryState) GetNonce(addr common.Address) uint64 { + s.mu.RLock() + defer s.mu.RUnlock() + if acct, ok := s.accounts[addr]; ok { + return acct.Nonce + } + return 0 +} + +func (s *MemoryState) SetNonce(addr common.Address, nonce uint64) { + s.mu.Lock() + defer s.mu.Unlock() + acct := s.getOrCreateAccountLocked(addr) + acct.Nonce = nonce +} + +func (s *MemoryState) GetCode(addr common.Address) []byte { + s.mu.RLock() + defer s.mu.RUnlock() + if acct, ok := s.accounts[addr]; ok { + return cloneBytes(acct.Code) + } + return nil +} + +func (s *MemoryState) SetCode(addr common.Address, code []byte) { + s.mu.Lock() + defer s.mu.Unlock() + acct := s.getOrCreateAccountLocked(addr) + acct.Code = cloneBytes(code) +} + +func (s *MemoryState) GetState(addr common.Address, key common.Hash) common.Hash { + s.mu.RLock() + defer s.mu.RUnlock() + if acct, ok := s.accounts[addr]; ok && acct.Storage != nil { + return acct.Storage[key] + } + return common.Hash{} +} + +func (s *MemoryState) SetState(addr common.Address, key common.Hash, value common.Hash) { + s.mu.Lock() + defer s.mu.Unlock() + acct := s.getOrCreateAccountLocked(addr) + if acct.Storage == nil { + acct.Storage = map[common.Hash]common.Hash{} + } + if value == (common.Hash{}) { + delete(acct.Storage, key) + return + } + acct.Storage[key] = value +} + +func (s *MemoryState) ApplyChangeSet(cs StateChangeSet) { + s.mu.Lock() + defer s.mu.Unlock() + for _, change := range cs.Balances { + acct := s.getOrCreateAccountLocked(change.Address) + acct.Balance = cloneBig(change.Balance) + } + for _, change := range cs.Nonces { + acct := s.getOrCreateAccountLocked(change.Address) + acct.Nonce = change.Nonce + } + for _, change := range cs.Code { + acct := s.getOrCreateAccountLocked(change.Address) + if change.Delete { + acct.Code = nil + } else { + acct.Code = cloneBytes(change.Code) + } + } + for _, change := range cs.Storage { + acct := s.getOrCreateAccountLocked(change.Address) + if acct.Storage == nil { + acct.Storage = map[common.Hash]common.Hash{} + } + if change.Delete { + delete(acct.Storage, change.Key) + } else { + acct.Storage[change.Key] = change.Value + } + } +} + +func (s *MemoryState) getOrCreateAccountLocked(addr common.Address) *StateAccount { + acct, ok := s.accounts[addr] + if !ok { + acct = &StateAccount{Balance: new(big.Int), Storage: map[common.Hash]common.Hash{}} + s.accounts[addr] = acct + } + return acct +} + +func cloneBig(v *big.Int) *big.Int { + if v == nil { + return new(big.Int) + } + return new(big.Int).Set(v) +} + +func cloneBytes(v []byte) []byte { + if len(v) == 0 { + return nil + } + return append([]byte(nil), v...) +} From 93e75ff46b0bcd44ed421ca53354c4fce52dbd14 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Mon, 15 Jun 2026 15:11:56 +0800 Subject: [PATCH 03/27] clean up evm-only executor scaffold leftovers --- giga/evmonly/README.md | 32 ++++++++++++++++++++++++----- giga/evmonly/seiv3/config.go | 13 +----------- giga/evmonly/seiv3/executor.go | 13 +++++------- giga/evmonly/seiv3/executor_test.go | 5 ++--- giga/evmonly/seiv3/runtime.go | 7 ------- giga/evmonly/seiv3/state_db.go | 1 - giga/evmonly/types.go | 3 --- 7 files changed, 35 insertions(+), 39 deletions(-) delete mode 100644 giga/evmonly/seiv3/runtime.go diff --git a/giga/evmonly/README.md b/giga/evmonly/README.md index 75aa00d30b..11e0eb3cc3 100644 --- a/giga/evmonly/README.md +++ b/giga/evmonly/README.md @@ -13,11 +13,28 @@ The target execution model is based on the `sei-v3` executor: - hot execution should not allocate `sdk.Context`, `sdk.Tx`, `MsgEVMTransaction`, Cosmos messages, or Cosmos coins -The current port executes raw RLP transactions with go-ethereum against an -EVM-native state backend, then returns a changeset plus Ethereum receipts. -Custom precompiles are still placeholders. The open work is to port them behind -an EVM-native context that is visible to the executor's conflict tracking -without reintroducing Cosmos keeper dependencies. +The current implementation executes raw RLP transactions with go-ethereum +against an EVM-native state backend, then returns a changeset plus Ethereum +receipts. Custom precompiles are still placeholders. The open work is to port +them behind an EVM-native context that is visible to the executor's conflict +tracking without reintroducing Cosmos keeper dependencies. + +## Current implementation + +The `seiv3` package currently provides: + +- sequential execution of the ordered block transaction list +- RLP decoding and sender recovery through go-ethereum signers +- go-ethereum `core.ApplyMessage` execution against an SDK-free `vm.StateDB` +- key-addressable state reads for balance, nonce, code, and storage +- deterministic post-block `StateChangeSet` construction +- Ethereum receipt construction with logs, bloom, gas, tx hash, block metadata, + contract address, and effective gas price +- a map-backed `MemoryState` for tests and early integration +- fail-closed custom precompile placeholders + +The executor accepts config for nonce checks, gas-price checks, minimum gas +price, chain config, and the custom precompile registry. ## Executor interface @@ -59,6 +76,10 @@ providing the block context. The executor is responsible for parsing tx RLP, recovering senders, validating EVM nonce/fee/intrinsic-gas rules, executing EVM state transitions, and producing deterministic outputs. +`BlockHash` is used for receipts and log metadata. The current `BLOCKHASH` +opcode callback only exposes `ParentHash`; older historical hashes require a +runtime-provided hash source in a later integration step. + ## Output format `BlockResult` contains two primary outputs: @@ -107,3 +128,4 @@ custom precompile addresses return `ErrCustomPrecompilesOpen`. by `(address, slot)` and does not require or expose range iteration. - The map-backed `MemoryState` is for tests and early integration; production should provide a durable native state backend. +- Historical `BLOCKHASH` lookups beyond the parent block are not wired yet. diff --git a/giga/evmonly/seiv3/config.go b/giga/evmonly/seiv3/config.go index e19cafbd2b..2e7e4761b7 100644 --- a/giga/evmonly/seiv3/config.go +++ b/giga/evmonly/seiv3/config.go @@ -1,7 +1,6 @@ package seiv3 import ( - "math" "math/big" "github.com/ethereum/go-ethereum/params" @@ -11,8 +10,6 @@ import ( // Config captures the sei-v3 executor knobs needed by the EVM-only path. type Config struct { - OCCWorkers int - FlushBatchSize int DisableNonceCheck bool DisableGasPriceCheck bool MinGasPrice *big.Int @@ -22,20 +19,12 @@ type Config struct { func DefaultConfig() Config { return Config{ - OCCWorkers: int(math.Min(12, float64(runtimeCPU()))), - FlushBatchSize: 100, - MinGasPrice: big.NewInt(1_000_000_000), + MinGasPrice: big.NewInt(1_000_000_000), } } func (c Config) WithDefaults() Config { defaults := DefaultConfig() - if c.OCCWorkers == 0 { - c.OCCWorkers = defaults.OCCWorkers - } - if c.FlushBatchSize == 0 { - c.FlushBatchSize = defaults.FlushBatchSize - } if c.MinGasPrice == nil { c.MinGasPrice = new(big.Int).Set(defaults.MinGasPrice) } diff --git a/giga/evmonly/seiv3/executor.go b/giga/evmonly/seiv3/executor.go index 52f2715e63..9024c2a47d 100644 --- a/giga/evmonly/seiv3/executor.go +++ b/giga/evmonly/seiv3/executor.go @@ -82,7 +82,7 @@ func (e *Executor) ExecuteBlock(ctx context.Context, req evmonly.BlockRequest) ( return nil, ctx.Err() default: } - txResult, receipt, err := e.executeTx(evm, stateDB, gasPool, req.Context, p, txIndex, baseFee) + txResult, receipt, err := e.executeTx(evm, stateDB, gasPool, req.Context, p, txIndex, baseFee, signer) if err != nil { return nil, fmt.Errorf("execute tx %d %s: %w", txIndex, p.tx.Hash(), err) } @@ -105,6 +105,7 @@ func (e *Executor) executeTx( p parsedTx, txIndex int, baseFee *big.Int, + signer ethtypes.Signer, ) (evmonly.TxResult, *ethtypes.Receipt, error) { tx := p.tx if e.cfg.CustomPrecompiles != nil && tx.To() != nil { @@ -122,7 +123,7 @@ func (e *Executor) executeTx( } } - msg, err := core.TransactionToMessage(tx, ethtypes.MakeSigner(e.chainConfig(block), new(big.Int).SetUint64(block.Number), block.Time), baseFee) + msg, err := core.TransactionToMessage(tx, signer, baseFee) if err != nil { return evmonly.TxResult{Hash: tx.Hash(), Sender: p.sender, To: tx.To(), Err: err}, nil, err } @@ -190,14 +191,10 @@ func buildBlockContext(ctx evmonly.BlockContext) vm.BlockContext { CanTransfer: core.CanTransfer, Transfer: core.Transfer, GetHash: func(n uint64) common.Hash { - switch { - case n == ctx.Number: - return ctx.BlockHash - case ctx.Number > 0 && n == ctx.Number-1: + if ctx.Number > 0 && n == ctx.Number-1 { return ctx.ParentHash - default: - return common.Hash{} } + return common.Hash{} }, Coinbase: ctx.Coinbase, GasLimit: gasLimit, diff --git a/giga/evmonly/seiv3/executor_test.go b/giga/evmonly/seiv3/executor_test.go index 6664103fae..7ca19c15a0 100644 --- a/giga/evmonly/seiv3/executor_test.go +++ b/giga/evmonly/seiv3/executor_test.go @@ -17,7 +17,7 @@ import ( ) func TestExecutorEmptyBlock(t *testing.T) { - executor := NewExecutor(Config{OCCWorkers: 1}) + executor := NewExecutor(Config{}) result, err := executor.ExecuteBlock(context.Background(), evmonly.BlockRequest{}) @@ -36,7 +36,7 @@ func TestExecutorTransferTx(t *testing.T) { state.SetBalance(sender, big.NewInt(200_000_000_000_000)) rawTx := signLegacyTx(t, key, chainID, 0, &recipient, big.NewInt(7), nil) - executor := NewExecutor(Config{OCCWorkers: 1}, WithState(state)) + executor := NewExecutor(Config{}, WithState(state)) result, err := executor.ExecuteBlock(context.Background(), evmonly.BlockRequest{ Context: blockContext(chainID), @@ -67,7 +67,6 @@ func TestExecutorCustomPrecompilePlaceholder(t *testing.T) { rawTx := signLegacyTx(t, key, chainID, 0, &customAddr, big.NewInt(0), []byte{0x01}) executor := NewExecutor(Config{ - OCCWorkers: 1, CustomPrecompiles: staticPrecompileRegistry{addr: customAddr}, }, WithState(state)) diff --git a/giga/evmonly/seiv3/runtime.go b/giga/evmonly/seiv3/runtime.go deleted file mode 100644 index cd5d04da9b..0000000000 --- a/giga/evmonly/seiv3/runtime.go +++ /dev/null @@ -1,7 +0,0 @@ -package seiv3 - -import "runtime" - -func runtimeCPU() int { - return runtime.NumCPU() -} diff --git a/giga/evmonly/seiv3/state_db.go b/giga/evmonly/seiv3/state_db.go index a641ea7f10..2008d6ddb5 100644 --- a/giga/evmonly/seiv3/state_db.go +++ b/giga/evmonly/seiv3/state_db.go @@ -152,7 +152,6 @@ func (s *nativeStateDB) SubBalance(addr common.Address, amount *uint256.Int, _ t acct := s.account(addr) if acct.Balance.Cmp(amount) < 0 { s.err = errInsufficientBalance - acct.Balance.Clear() return prev } acct.Balance.Sub(acct.Balance, amount) diff --git a/giga/evmonly/types.go b/giga/evmonly/types.go index c7c835b3ad..92ad6f34aa 100644 --- a/giga/evmonly/types.go +++ b/giga/evmonly/types.go @@ -2,15 +2,12 @@ package evmonly import ( "context" - "errors" "math/big" "github.com/ethereum/go-ethereum/common" ethtypes "github.com/ethereum/go-ethereum/core/types" ) -var ErrNotImplemented = errors.New("evm-only executor is not implemented") - // Executor is the Cosmos-free block execution boundary for the EVM-only path. type Executor interface { ExecuteBlock(context.Context, BlockRequest) (*BlockResult, error) From 7338faf46820212096c4f13948714a877635d662 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Mon, 15 Jun 2026 15:47:21 +0800 Subject: [PATCH 04/27] flatten evm-only executor package --- giga/evmonly/README.md | 9 +++-- giga/evmonly/{seiv3 => }/config.go | 2 +- giga/evmonly/{seiv3 => }/executor.go | 42 +++++++++-------------- giga/evmonly/{seiv3 => }/executor_test.go | 17 +++++---- giga/evmonly/{seiv3 => }/parser.go | 2 +- giga/evmonly/{seiv3 => }/state_db.go | 29 ++++++---------- giga/evmonly/types.go | 4 +-- 7 files changed, 43 insertions(+), 62 deletions(-) rename giga/evmonly/{seiv3 => }/config.go (97%) rename giga/evmonly/{seiv3 => }/executor.go (84%) rename giga/evmonly/{seiv3 => }/executor_test.go (86%) rename giga/evmonly/{seiv3 => }/parser.go (98%) rename giga/evmonly/{seiv3 => }/state_db.go (96%) diff --git a/giga/evmonly/README.md b/giga/evmonly/README.md index 11e0eb3cc3..d0ab7e8e92 100644 --- a/giga/evmonly/README.md +++ b/giga/evmonly/README.md @@ -21,7 +21,7 @@ tracking without reintroducing Cosmos keeper dependencies. ## Current implementation -The `seiv3` package currently provides: +The `evmonly` package currently provides: - sequential execution of the ordered block transaction list - RLP decoding and sender recovery through go-ethereum signers @@ -47,9 +47,8 @@ ExecuteBlock(context.Context, BlockRequest) (*BlockResult, error) The executor should be commit-neutral. It executes an ordered EVM block and returns the state writes and receipts produced by that block. The caller owns durable persistence, state commitment, block indexing, and receipt publication. -The `seiv3` implementation accepts a `StateReader` backend through -`WithState(...)`; callers can persist the returned `ChangeSet` with a matching -`StateWriter`. +The concrete `Executor` accepts a `StateReader` backend through `WithState(...)`; +callers can persist the returned `ChangeSet` with a matching `StateWriter`. ## Input block format @@ -116,7 +115,7 @@ state as contract storage owned by that precompile address. With no range reads and no side state, precompile reads and writes can then flow through ordinary `(address, slot)` storage tracking. -Until that design is implemented, the `seiv3` executor accepts a custom +Until that design is implemented, the `evmonly` executor accepts a custom precompile registry only as a fail-closed placeholder. Calls to registered custom precompile addresses return `ErrCustomPrecompilesOpen`. diff --git a/giga/evmonly/seiv3/config.go b/giga/evmonly/config.go similarity index 97% rename from giga/evmonly/seiv3/config.go rename to giga/evmonly/config.go index 2e7e4761b7..8371b4c845 100644 --- a/giga/evmonly/seiv3/config.go +++ b/giga/evmonly/config.go @@ -1,4 +1,4 @@ -package seiv3 +package evmonly import ( "math/big" diff --git a/giga/evmonly/seiv3/executor.go b/giga/evmonly/executor.go similarity index 84% rename from giga/evmonly/seiv3/executor.go rename to giga/evmonly/executor.go index 9024c2a47d..370d8315d9 100644 --- a/giga/evmonly/seiv3/executor.go +++ b/giga/evmonly/executor.go @@ -1,4 +1,4 @@ -package seiv3 +package evmonly import ( "context" @@ -13,19 +13,18 @@ import ( "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/params" - "github.com/sei-protocol/sei-chain/giga/evmonly" "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles" ) // Executor runs raw EVM transactions against an EVM-native state backend. type Executor struct { cfg Config - state evmonly.StateReader + state StateReader } type Option func(*Executor) -func WithState(state evmonly.StateReader) Option { +func WithState(state StateReader) Option { return func(e *Executor) { if state != nil { e.state = state @@ -36,7 +35,7 @@ func WithState(state evmonly.StateReader) Option { func NewExecutor(cfg Config, opts ...Option) *Executor { e := &Executor{ cfg: cfg.WithDefaults(), - state: evmonly.NewMemoryState(), + state: NewMemoryState(), } for _, opt := range opts { opt(e) @@ -48,9 +47,9 @@ func (e *Executor) Config() Config { return e.cfg } -func (e *Executor) ExecuteBlock(ctx context.Context, req evmonly.BlockRequest) (*evmonly.BlockResult, error) { +func (e *Executor) ExecuteBlock(ctx context.Context, req BlockRequest) (*BlockResult, error) { if len(req.Txs) == 0 { - return &evmonly.BlockResult{}, nil + return &BlockResult{}, nil } chainConfig := e.chainConfig(req.Context) @@ -72,8 +71,8 @@ func (e *Executor) ExecuteBlock(ctx context.Context, req evmonly.BlockRequest) ( gasPool := new(core.GasPool).AddGas(gasLimit) baseFee := cloneBig(req.Context.BaseFee) - result := &evmonly.BlockResult{ - Txs: make([]evmonly.TxResult, 0, len(parsed)), + result := &BlockResult{ + Txs: make([]TxResult, 0, len(parsed)), Receipts: make(ethtypes.Receipts, 0, len(parsed)), } for txIndex, p := range parsed { @@ -101,23 +100,23 @@ func (e *Executor) executeTx( evm *vm.EVM, stateDB *nativeStateDB, gasPool *core.GasPool, - block evmonly.BlockContext, + block BlockContext, p parsedTx, txIndex int, baseFee *big.Int, signer ethtypes.Signer, -) (evmonly.TxResult, *ethtypes.Receipt, error) { +) (TxResult, *ethtypes.Receipt, error) { tx := p.tx if e.cfg.CustomPrecompiles != nil && tx.To() != nil { if _, ok := e.cfg.CustomPrecompiles.Get(*tx.To()); ok { - return evmonly.TxResult{Hash: tx.Hash(), Sender: p.sender, To: tx.To(), Err: precompiles.ErrCustomPrecompilesOpen}, + return TxResult{Hash: tx.Hash(), Sender: p.sender, To: tx.To(), Err: precompiles.ErrCustomPrecompilesOpen}, nil, precompiles.ErrCustomPrecompilesOpen } } if !e.cfg.DisableGasPriceCheck && e.cfg.MinGasPrice != nil { if effectiveGasPrice(tx, baseFee).Cmp(e.cfg.MinGasPrice) < 0 { - return evmonly.TxResult{Hash: tx.Hash(), Sender: p.sender, To: tx.To(), Err: errInsufficientGasPrice}, + return TxResult{Hash: tx.Hash(), Sender: p.sender, To: tx.To(), Err: errInsufficientGasPrice}, nil, errInsufficientGasPrice } @@ -125,7 +124,7 @@ func (e *Executor) executeTx( msg, err := core.TransactionToMessage(tx, signer, baseFee) if err != nil { - return evmonly.TxResult{Hash: tx.Hash(), Sender: p.sender, To: tx.To(), Err: err}, nil, err + return TxResult{Hash: tx.Hash(), Sender: p.sender, To: tx.To(), Err: err}, nil, err } msg.SkipNonceChecks = e.cfg.DisableNonceCheck @@ -134,7 +133,7 @@ func (e *Executor) executeTx( evm.SetTxContext(core.NewEVMTxContext(msg)) execResult, err := core.ApplyMessage(evm, msg, gasPool) if err != nil { - return evmonly.TxResult{Hash: tx.Hash(), Sender: p.sender, To: tx.To(), Err: err}, nil, err + return TxResult{Hash: tx.Hash(), Sender: p.sender, To: tx.To(), Err: err}, nil, err } txLogs := append([]*ethtypes.Log(nil), stateDB.logs[logStart:]...) @@ -165,7 +164,7 @@ func (e *Executor) executeTx( } receipt.Bloom = ethtypes.CreateBloom(receipt) - txResult := evmonly.TxResult{ + txResult := TxResult{ Hash: tx.Hash(), Sender: p.sender, To: tx.To(), @@ -179,7 +178,7 @@ func (e *Executor) executeTx( return txResult, receipt, nil } -func buildBlockContext(ctx evmonly.BlockContext) vm.BlockContext { +func buildBlockContext(ctx BlockContext) vm.BlockContext { prevRandao := ctx.PrevRandao baseFee := cloneBig(ctx.BaseFee) blobBaseFee := cloneBig(ctx.BlobBaseFee) @@ -232,7 +231,7 @@ func customPrecompileMap(registry precompiles.Registry) map[common.Address]vm.Pr return contracts } -func (e *Executor) chainConfig(ctx evmonly.BlockContext) *params.ChainConfig { +func (e *Executor) chainConfig(ctx BlockContext) *params.ChainConfig { var cfg params.ChainConfig if e.cfg.ChainConfig != nil { cfg = *e.cfg.ChainConfig @@ -259,11 +258,4 @@ func effectiveGasPrice(tx *ethtypes.Transaction, baseFee *big.Int) *big.Int { return tx.GasPrice() } -func cloneBig(v *big.Int) *big.Int { - if v == nil { - return new(big.Int) - } - return new(big.Int).Set(v) -} - var errInsufficientGasPrice = fmt.Errorf("insufficient gas price") diff --git a/giga/evmonly/seiv3/executor_test.go b/giga/evmonly/executor_test.go similarity index 86% rename from giga/evmonly/seiv3/executor_test.go rename to giga/evmonly/executor_test.go index 7ca19c15a0..40fa4898db 100644 --- a/giga/evmonly/seiv3/executor_test.go +++ b/giga/evmonly/executor_test.go @@ -1,4 +1,4 @@ -package seiv3 +package evmonly import ( "context" @@ -12,14 +12,13 @@ import ( "github.com/ethereum/go-ethereum/crypto" "github.com/stretchr/testify/require" - "github.com/sei-protocol/sei-chain/giga/evmonly" "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles" ) func TestExecutorEmptyBlock(t *testing.T) { executor := NewExecutor(Config{}) - result, err := executor.ExecuteBlock(context.Background(), evmonly.BlockRequest{}) + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{}) require.NoError(t, err) require.NotNil(t, result) @@ -32,13 +31,13 @@ func TestExecutorTransferTx(t *testing.T) { sender := crypto.PubkeyToAddress(key.PublicKey) recipient := common.HexToAddress("0x00000000000000000000000000000000000000a1") - state := evmonly.NewMemoryState() + state := NewMemoryState() state.SetBalance(sender, big.NewInt(200_000_000_000_000)) rawTx := signLegacyTx(t, key, chainID, 0, &recipient, big.NewInt(7), nil) executor := NewExecutor(Config{}, WithState(state)) - result, err := executor.ExecuteBlock(context.Background(), evmonly.BlockRequest{ + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ Context: blockContext(chainID), Txs: [][]byte{rawTx}, }) @@ -62,7 +61,7 @@ func TestExecutorCustomPrecompilePlaceholder(t *testing.T) { sender := crypto.PubkeyToAddress(key.PublicKey) customAddr := common.HexToAddress("0x0000000000000000000000000000000000001001") - state := evmonly.NewMemoryState() + state := NewMemoryState() state.SetBalance(sender, big.NewInt(200_000_000_000_000)) rawTx := signLegacyTx(t, key, chainID, 0, &customAddr, big.NewInt(0), []byte{0x01}) @@ -70,7 +69,7 @@ func TestExecutorCustomPrecompilePlaceholder(t *testing.T) { CustomPrecompiles: staticPrecompileRegistry{addr: customAddr}, }, WithState(state)) - _, err = executor.ExecuteBlock(context.Background(), evmonly.BlockRequest{ + _, err = executor.ExecuteBlock(context.Background(), BlockRequest{ Context: blockContext(chainID), Txs: [][]byte{rawTx}, }) @@ -96,8 +95,8 @@ func signLegacyTx(t *testing.T, key *ecdsa.PrivateKey, chainID *big.Int, nonce u return raw } -func blockContext(chainID *big.Int) evmonly.BlockContext { - return evmonly.BlockContext{ +func blockContext(chainID *big.Int) BlockContext { + return BlockContext{ Number: 1, Time: 1, GasLimit: 30_000_000, diff --git a/giga/evmonly/seiv3/parser.go b/giga/evmonly/parser.go similarity index 98% rename from giga/evmonly/seiv3/parser.go rename to giga/evmonly/parser.go index 76a4da311a..f2c0776e3d 100644 --- a/giga/evmonly/seiv3/parser.go +++ b/giga/evmonly/parser.go @@ -1,4 +1,4 @@ -package seiv3 +package evmonly import ( "context" diff --git a/giga/evmonly/seiv3/state_db.go b/giga/evmonly/state_db.go similarity index 96% rename from giga/evmonly/seiv3/state_db.go rename to giga/evmonly/state_db.go index 2008d6ddb5..e888d5a257 100644 --- a/giga/evmonly/seiv3/state_db.go +++ b/giga/evmonly/state_db.go @@ -1,4 +1,4 @@ -package seiv3 +package evmonly import ( "bytes" @@ -15,14 +15,12 @@ import ( "github.com/ethereum/go-ethereum/params" ethutils "github.com/ethereum/go-ethereum/trie/utils" "github.com/holiman/uint256" - - "github.com/sei-protocol/sei-chain/giga/evmonly" ) var errInsufficientBalance = errors.New("insufficient balance") type nativeStateDB struct { - source evmonly.StateReader + source StateReader accounts map[common.Address]*nativeAccount base map[common.Address]*nativeAccount @@ -65,9 +63,9 @@ type accessList struct { slots map[common.Address]map[common.Hash]struct{} } -func newNativeStateDB(source evmonly.StateReader) *nativeStateDB { +func newNativeStateDB(source StateReader) *nativeStateDB { if source == nil { - source = evmonly.NewMemoryState() + source = NewMemoryState() } return &nativeStateDB{ source: source, @@ -79,7 +77,7 @@ func newNativeStateDB(source evmonly.StateReader) *nativeStateDB { } } -func (s *nativeStateDB) ChangeSet() evmonly.StateChangeSet { +func (s *nativeStateDB) ChangeSet() StateChangeSet { addresses := make([]common.Address, 0, len(s.accounts)) for addr := range s.accounts { addresses = append(addresses, addr) @@ -88,25 +86,25 @@ func (s *nativeStateDB) ChangeSet() evmonly.StateChangeSet { return bytes.Compare(addresses[i][:], addresses[j][:]) < 0 }) - var changes evmonly.StateChangeSet + var changes StateChangeSet for _, addr := range addresses { acct := s.accounts[addr] base := s.baseAccount(addr) if !acct.Balance.Eq(base.Balance) { - changes.Balances = append(changes.Balances, evmonly.BalanceChange{ + changes.Balances = append(changes.Balances, BalanceChange{ Address: addr, Balance: acct.Balance.ToBig(), }) } if acct.Nonce != base.Nonce { - changes.Nonces = append(changes.Nonces, evmonly.NonceChange{ + changes.Nonces = append(changes.Nonces, NonceChange{ Address: addr, Nonce: acct.Nonce, }) } if !bytes.Equal(acct.Code, base.Code) { - changes.Code = append(changes.Code, evmonly.CodeChange{ + changes.Code = append(changes.Code, CodeChange{ Address: addr, Code: cloneBytes(acct.Code), Delete: len(acct.Code) == 0, @@ -119,7 +117,7 @@ func (s *nativeStateDB) ChangeSet() evmonly.StateChangeSet { if oldValue == newValue { continue } - changes.Storage = append(changes.Storage, evmonly.StorageChange{ + changes.Storage = append(changes.Storage, StorageChange{ Address: addr, Key: key, Value: newValue, @@ -639,10 +637,3 @@ func uint256FromBig(v *big.Int) *uint256.Int { } return u } - -func cloneBytes(v []byte) []byte { - if len(v) == 0 { - return nil - } - return append([]byte(nil), v...) -} diff --git a/giga/evmonly/types.go b/giga/evmonly/types.go index 92ad6f34aa..708c10e91f 100644 --- a/giga/evmonly/types.go +++ b/giga/evmonly/types.go @@ -8,8 +8,8 @@ import ( ethtypes "github.com/ethereum/go-ethereum/core/types" ) -// Executor is the Cosmos-free block execution boundary for the EVM-only path. -type Executor interface { +// BlockExecutor is the Cosmos-free block execution boundary for the EVM-only path. +type BlockExecutor interface { ExecuteBlock(context.Context, BlockRequest) (*BlockResult, error) } From 5de0066c6ea5bf723a23398d2e21db0e90dac97f Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Mon, 15 Jun 2026 19:25:27 +0800 Subject: [PATCH 05/27] fix evmonly lint after rebase --- giga/evmonly/executor.go | 11 +++++++---- giga/evmonly/state_db.go | 18 +++++++++++++----- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/giga/evmonly/executor.go b/giga/evmonly/executor.go index 370d8315d9..f5d97644f3 100644 --- a/giga/evmonly/executor.go +++ b/giga/evmonly/executor.go @@ -75,13 +75,14 @@ func (e *Executor) ExecuteBlock(ctx context.Context, req BlockRequest) (*BlockRe Txs: make([]TxResult, 0, len(parsed)), Receipts: make(ethtypes.Receipts, 0, len(parsed)), } + var txIndexUint uint for txIndex, p := range parsed { select { case <-ctx.Done(): return nil, ctx.Err() default: } - txResult, receipt, err := e.executeTx(evm, stateDB, gasPool, req.Context, p, txIndex, baseFee, signer) + txResult, receipt, err := e.executeTx(evm, stateDB, gasPool, req.Context, p, txIndex, txIndexUint, baseFee, signer) if err != nil { return nil, fmt.Errorf("execute tx %d %s: %w", txIndex, p.tx.Hash(), err) } @@ -90,6 +91,7 @@ func (e *Executor) ExecuteBlock(ctx context.Context, req BlockRequest) (*BlockRe result.Txs = append(result.Txs, txResult) result.Receipts = append(result.Receipts, receipt) result.GasUsed += txResult.GasUsed + txIndexUint++ } stateDB.Finalise(true) result.ChangeSet = stateDB.ChangeSet() @@ -103,6 +105,7 @@ func (e *Executor) executeTx( block BlockContext, p parsedTx, txIndex int, + txIndexUint uint, baseFee *big.Int, signer ethtypes.Signer, ) (TxResult, *ethtypes.Receipt, error) { @@ -128,7 +131,7 @@ func (e *Executor) executeTx( } msg.SkipNonceChecks = e.cfg.DisableNonceCheck - stateDB.SetTxContext(tx.Hash(), txIndex) + stateDB.setTxContext(tx.Hash(), txIndex, txIndexUint) logStart := len(stateDB.logs) evm.SetTxContext(core.NewEVMTxContext(msg)) execResult, err := core.ApplyMessage(evm, msg, gasPool) @@ -141,7 +144,7 @@ func (e *Executor) executeTx( log.BlockNumber = block.Number log.BlockHash = block.BlockHash log.TxHash = tx.Hash() - log.TxIndex = uint(txIndex) + log.TxIndex = txIndexUint } status := ethtypes.ReceiptStatusSuccessful @@ -157,7 +160,7 @@ func (e *Executor) executeTx( EffectiveGasPrice: effectiveGasPrice(tx, baseFee), BlockHash: block.BlockHash, BlockNumber: new(big.Int).SetUint64(block.Number), - TransactionIndex: uint(txIndex), + TransactionIndex: txIndexUint, } if tx.To() == nil { receipt.ContractAddress = crypto.CreateAddress(p.sender, tx.Nonce()) diff --git a/giga/evmonly/state_db.go b/giga/evmonly/state_db.go index e888d5a257..8038488b4b 100644 --- a/giga/evmonly/state_db.go +++ b/giga/evmonly/state_db.go @@ -33,10 +33,11 @@ type nativeStateDB struct { transientStates map[common.Address]map[common.Hash]common.Hash snapshots []nativeSnapshot - txHash common.Hash - txIndex int - err error - evm *vm.EVM + txHash common.Hash + txIndex int + txIndexUint uint + err error + evm *vm.EVM } type nativeAccount struct { @@ -395,7 +396,7 @@ func (s *nativeStateDB) RevertToSnapshot(id int) { func (s *nativeStateDB) AddLog(log *ethtypes.Log) { log.TxHash = s.txHash - log.TxIndex = uint(s.txIndex) + log.TxIndex = s.txIndexUint log.Index = uint(len(s.logs)) s.logs = append(s.logs, log) } @@ -434,6 +435,12 @@ func (s *nativeStateDB) SetTxContext(hash common.Hash, index int) { s.txIndex = index } +func (s *nativeStateDB) setTxContext(hash common.Hash, index int, indexUint uint) { + s.txHash = hash + s.txIndex = index + s.txIndexUint = indexUint +} + func (s *nativeStateDB) Copy() vm.StateDB { cp := &nativeStateDB{ source: s.source, @@ -447,6 +454,7 @@ func (s *nativeStateDB) Copy() vm.StateDB { snapshots: cloneSnapshots(s.snapshots), txHash: s.txHash, txIndex: s.txIndex, + txIndexUint: s.txIndexUint, err: s.err, evm: s.evm, } From 4a5612578168187e4e726a220200888c5c9ef96c Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Tue, 16 Jun 2026 15:05:30 +0800 Subject: [PATCH 06/27] address evmonly cursor review comments --- giga/evmonly/executor.go | 1 + giga/evmonly/executor_test.go | 115 ++++++++++++++++++++++++++++++++++ giga/evmonly/parser.go | 3 +- giga/evmonly/state_db.go | 1 + 4 files changed, 118 insertions(+), 2 deletions(-) diff --git a/giga/evmonly/executor.go b/giga/evmonly/executor.go index f5d97644f3..ad427fcd31 100644 --- a/giga/evmonly/executor.go +++ b/giga/evmonly/executor.go @@ -138,6 +138,7 @@ func (e *Executor) executeTx( if err != nil { return TxResult{Hash: tx.Hash(), Sender: p.sender, To: tx.To(), Err: err}, nil, err } + stateDB.Finalise(true) txLogs := append([]*ethtypes.Log(nil), stateDB.logs[logStart:]...) for _, log := range txLogs { diff --git a/giga/evmonly/executor_test.go b/giga/evmonly/executor_test.go index 40fa4898db..f8fe9589d3 100644 --- a/giga/evmonly/executor_test.go +++ b/giga/evmonly/executor_test.go @@ -10,6 +10,7 @@ import ( "github.com/ethereum/go-ethereum/common" ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/params" "github.com/stretchr/testify/require" "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles" @@ -54,6 +55,78 @@ func TestExecutorTransferTx(t *testing.T) { require.Equal(t, uint64(1), state.GetNonce(sender)) } +func TestExecutorDynamicFeeTx(t *testing.T) { + chainID := big.NewInt(713715) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + recipient := common.HexToAddress("0x00000000000000000000000000000000000000a2") + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(200_000_000_000_000)) + + rawTx := signDynamicFeeTx(t, key, chainID, 0, &recipient, big.NewInt(11), nil) + executor := NewExecutor(Config{}, WithState(state)) + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: blockContext(chainID), + Txs: [][]byte{rawTx}, + }) + + require.NoError(t, err) + require.Len(t, result.Txs, 1) + require.Equal(t, uint8(ethtypes.DynamicFeeTxType), result.Receipts[0].Type) + + state.ApplyChangeSet(result.ChangeSet) + require.Equal(t, big.NewInt(11), state.GetBalance(recipient)) +} + +func TestExecutorFinalisesAfterEachTx(t *testing.T) { + chainID := big.NewInt(713715) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + contract := common.HexToAddress("0x00000000000000000000000000000000000000c1") + beneficiary := common.HexToAddress("0x00000000000000000000000000000000000000b1") + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(500_000_000_000_000)) + state.SetCode(contract, selfDestructCode(beneficiary)) + + firstCall := signLegacyTx(t, key, chainID, 0, &contract, big.NewInt(0), nil) + secondCall := signLegacyTx(t, key, chainID, 1, &contract, big.NewInt(5), nil) + executor := NewExecutor(Config{ + ChainConfig: legacySelfDestructChainConfig(chainID), + }, WithState(state)) + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: blockContext(chainID), + Txs: [][]byte{firstCall, secondCall}, + }) + + require.NoError(t, err) + require.Len(t, result.Receipts, 2) + + state.ApplyChangeSet(result.ChangeSet) + require.Empty(t, state.GetCode(contract)) + require.Equal(t, big.NewInt(5), state.GetBalance(contract)) + require.Equal(t, big.NewInt(0), state.GetBalance(beneficiary)) +} + +func TestPrepareClearsTransientStorage(t *testing.T) { + stateDB := newNativeStateDB(NewMemoryState()) + addr := common.HexToAddress("0x00000000000000000000000000000000000000a3") + key := common.HexToHash("0x01") + value := common.HexToHash("0x02") + + stateDB.SetTransientState(addr, key, value) + require.Equal(t, value, stateDB.GetTransientState(addr, key)) + + stateDB.Prepare(params.Rules{}, addr, common.Address{}, nil, nil, nil) + + require.Equal(t, common.Hash{}, stateDB.GetTransientState(addr, key)) +} + func TestExecutorCustomPrecompilePlaceholder(t *testing.T) { chainID := big.NewInt(713715) key, err := crypto.GenerateKey() @@ -95,6 +168,25 @@ func signLegacyTx(t *testing.T, key *ecdsa.PrivateKey, chainID *big.Int, nonce u return raw } +func signDynamicFeeTx(t *testing.T, key *ecdsa.PrivateKey, chainID *big.Int, nonce uint64, to *common.Address, value *big.Int, data []byte) []byte { + t.Helper() + tx := ethtypes.NewTx(ðtypes.DynamicFeeTx{ + ChainID: chainID, + Nonce: nonce, + GasTipCap: big.NewInt(1_000_000_000), + GasFeeCap: big.NewInt(1_000_000_000), + Gas: 100_000, + To: to, + Value: value, + Data: data, + }) + signed, err := ethtypes.SignTx(tx, ethtypes.LatestSignerForChainID(chainID), key) + require.NoError(t, err) + raw, err := signed.MarshalBinary() + require.NoError(t, err) + return raw +} + func blockContext(chainID *big.Int) BlockContext { return BlockContext{ Number: 1, @@ -106,6 +198,29 @@ func blockContext(chainID *big.Int) BlockContext { } } +func legacySelfDestructChainConfig(chainID *big.Int) *params.ChainConfig { + return ¶ms.ChainConfig{ + ChainID: chainID, + HomesteadBlock: big.NewInt(0), + DAOForkBlock: nil, + DAOForkSupport: false, + EIP150Block: big.NewInt(0), + EIP155Block: big.NewInt(0), + EIP158Block: big.NewInt(0), + ByzantiumBlock: big.NewInt(0), + ConstantinopleBlock: big.NewInt(0), + PetersburgBlock: big.NewInt(0), + IstanbulBlock: big.NewInt(0), + BerlinBlock: big.NewInt(0), + LondonBlock: big.NewInt(0), + } +} + +func selfDestructCode(beneficiary common.Address) []byte { + code := append([]byte{0x73}, beneficiary.Bytes()...) + return append(code, 0xff) +} + type staticPrecompileRegistry struct { addr common.Address } diff --git a/giga/evmonly/parser.go b/giga/evmonly/parser.go index f2c0776e3d..d5a47fffb6 100644 --- a/giga/evmonly/parser.go +++ b/giga/evmonly/parser.go @@ -6,7 +6,6 @@ import ( "github.com/ethereum/go-ethereum/common" ethtypes "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/rlp" ) type parsedTx struct { @@ -33,7 +32,7 @@ func parseBlockTxs(ctx context.Context, txs [][]byte, signer ethtypes.Signer) ([ func parseTx(raw []byte, signer ethtypes.Signer) (*ethtypes.Transaction, common.Address, error) { var tx ethtypes.Transaction - if err := rlp.DecodeBytes(raw, &tx); err != nil { + if err := tx.UnmarshalBinary(raw); err != nil { return nil, common.Address{}, err } sender, err := ethtypes.Sender(signer, &tx) diff --git a/giga/evmonly/state_db.go b/giga/evmonly/state_db.go index 8038488b4b..885f24109b 100644 --- a/giga/evmonly/state_db.go +++ b/giga/evmonly/state_db.go @@ -345,6 +345,7 @@ func (s *nativeStateDB) AddSlotToAccessList(addr common.Address, slot common.Has func (s *nativeStateDB) Prepare(_ params.Rules, sender, coinbase common.Address, dest *common.Address, precompiles []common.Address, txAccesses ethtypes.AccessList) { s.accessList = newAccessList() + s.transientStates = map[common.Address]map[common.Hash]common.Hash{} s.AddAddressToAccessList(sender) s.AddAddressToAccessList(coinbase) if dest != nil { From f0d8315aa928bcab961c29df90a5491cbbc5ea9a Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Tue, 16 Jun 2026 15:22:22 +0800 Subject: [PATCH 07/27] fix evmonly state snapshot finalization --- giga/evmonly/executor_test.go | 26 ++++++++++++++++++++++++++ giga/evmonly/state_db.go | 5 +++++ 2 files changed, 31 insertions(+) diff --git a/giga/evmonly/executor_test.go b/giga/evmonly/executor_test.go index f8fe9589d3..1f2730b9df 100644 --- a/giga/evmonly/executor_test.go +++ b/giga/evmonly/executor_test.go @@ -127,6 +127,32 @@ func TestPrepareClearsTransientStorage(t *testing.T) { require.Equal(t, common.Hash{}, stateDB.GetTransientState(addr, key)) } +func TestSnapshotRevertRestoresBaseState(t *testing.T) { + addr := common.HexToAddress("0x00000000000000000000000000000000000000a4") + key := common.HexToHash("0x01") + value := common.HexToHash("0x02") + + state := NewMemoryState() + state.SetState(addr, key, value) + stateDB := newNativeStateDB(state) + stateDB.GetBalance(addr) + + snapshot := stateDB.Snapshot() + require.Equal(t, value, stateDB.GetState(addr, key)) + stateDB.RevertToSnapshot(snapshot) + + require.Empty(t, stateDB.ChangeSet().Storage) +} + +func TestFinaliseClearsRefund(t *testing.T) { + stateDB := newNativeStateDB(NewMemoryState()) + stateDB.AddRefund(12) + + stateDB.Finalise(true) + + require.Zero(t, stateDB.GetRefund()) +} + func TestExecutorCustomPrecompilePlaceholder(t *testing.T) { chainID := big.NewInt(713715) key, err := crypto.GenerateKey() diff --git a/giga/evmonly/state_db.go b/giga/evmonly/state_db.go index 885f24109b..210a5ab7f7 100644 --- a/giga/evmonly/state_db.go +++ b/giga/evmonly/state_db.go @@ -51,6 +51,7 @@ type nativeAccount struct { type nativeSnapshot struct { accounts map[common.Address]*nativeAccount + base map[common.Address]*nativeAccount refund uint64 logs []*ethtypes.Log accessList accessList @@ -370,6 +371,7 @@ func (s *nativeStateDB) Snapshot() int { id := len(s.snapshots) s.snapshots = append(s.snapshots, nativeSnapshot{ accounts: cloneAccounts(s.accounts), + base: cloneAccounts(s.base), refund: s.refund, logs: append([]*ethtypes.Log(nil), s.logs...), accessList: cloneAccessList(s.accessList), @@ -386,6 +388,7 @@ func (s *nativeStateDB) RevertToSnapshot(id int) { } snapshot := s.snapshots[id] s.accounts = cloneAccounts(snapshot.accounts) + s.base = cloneAccounts(snapshot.base) s.refund = snapshot.refund s.logs = append([]*ethtypes.Log(nil), snapshot.logs...) s.accessList = cloneAccessList(snapshot.accessList) @@ -421,6 +424,7 @@ func (s *nativeStateDB) Finalise(bool) { acct.Storage = map[common.Hash]common.Hash{} } } + s.refund = 0 } func (s *nativeStateDB) Error() error { @@ -604,6 +608,7 @@ func cloneSnapshots(snapshots []nativeSnapshot) []nativeSnapshot { for i, snapshot := range snapshots { cp[i] = nativeSnapshot{ accounts: cloneAccounts(snapshot.accounts), + base: cloneAccounts(snapshot.base), refund: snapshot.refund, logs: append([]*ethtypes.Log(nil), snapshot.logs...), accessList: cloneAccessList(snapshot.accessList), From 298afddfa1cdb2dbc349e3588d8cc3dddd26065e Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Wed, 17 Jun 2026 10:30:51 +0800 Subject: [PATCH 08/27] let custom precompile placeholders fail in receipts --- giga/evmonly/README.md | 4 ++++ giga/evmonly/executor.go | 9 ++------- giga/evmonly/executor_test.go | 9 ++++++--- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/giga/evmonly/README.md b/giga/evmonly/README.md index d0ab7e8e92..928a203ce3 100644 --- a/giga/evmonly/README.md +++ b/giga/evmonly/README.md @@ -50,6 +50,10 @@ durable persistence, state commitment, block indexing, and receipt publication. The concrete `Executor` accepts a `StateReader` backend through `WithState(...)`; callers can persist the returned `ChangeSet` with a matching `StateWriter`. +A non-nil `error` means block validation failed and the caller must not commit a +partial output. EVM call failures inside an otherwise valid transaction are +represented in `Receipts` and `Txs` with failed status. + ## Input block format `BlockRequest` is the expected input: diff --git a/giga/evmonly/executor.go b/giga/evmonly/executor.go index ad427fcd31..ad49d485f7 100644 --- a/giga/evmonly/executor.go +++ b/giga/evmonly/executor.go @@ -110,14 +110,9 @@ func (e *Executor) executeTx( signer ethtypes.Signer, ) (TxResult, *ethtypes.Receipt, error) { tx := p.tx - if e.cfg.CustomPrecompiles != nil && tx.To() != nil { - if _, ok := e.cfg.CustomPrecompiles.Get(*tx.To()); ok { - return TxResult{Hash: tx.Hash(), Sender: p.sender, To: tx.To(), Err: precompiles.ErrCustomPrecompilesOpen}, - nil, - precompiles.ErrCustomPrecompilesOpen - } - } if !e.cfg.DisableGasPriceCheck && e.cfg.MinGasPrice != nil { + // MinGasPrice is block-validity policy; unlike EVM call failures, it + // does not produce a receipt for an otherwise invalid block. if effectiveGasPrice(tx, baseFee).Cmp(e.cfg.MinGasPrice) < 0 { return TxResult{Hash: tx.Hash(), Sender: p.sender, To: tx.To(), Err: errInsufficientGasPrice}, nil, diff --git a/giga/evmonly/executor_test.go b/giga/evmonly/executor_test.go index 1f2730b9df..3d7f4d000e 100644 --- a/giga/evmonly/executor_test.go +++ b/giga/evmonly/executor_test.go @@ -168,13 +168,16 @@ func TestExecutorCustomPrecompilePlaceholder(t *testing.T) { CustomPrecompiles: staticPrecompileRegistry{addr: customAddr}, }, WithState(state)) - _, err = executor.ExecuteBlock(context.Background(), BlockRequest{ + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ Context: blockContext(chainID), Txs: [][]byte{rawTx}, }) - require.Error(t, err) - require.True(t, errors.Is(err, precompiles.ErrCustomPrecompilesOpen)) + require.NoError(t, err) + require.Len(t, result.Txs, 1) + require.Len(t, result.Receipts, 1) + require.Equal(t, ethtypes.ReceiptStatusFailed, result.Txs[0].Status) + require.True(t, errors.Is(result.Txs[0].Err, precompiles.ErrCustomPrecompilesOpen)) } func signLegacyTx(t *testing.T, key *ecdsa.PrivateKey, chainID *big.Int, nonce uint64, to *common.Address, value *big.Int, data []byte) []byte { From 0a6110a935302ad714cd898a1c3a44cb80b2b0e6 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Wed, 24 Jun 2026 11:31:41 +0800 Subject: [PATCH 09/27] expand evmonly executor test coverage --- giga/evmonly/executor_test.go | 263 +++++++++++++++++++++++++++++++++- 1 file changed, 262 insertions(+), 1 deletion(-) diff --git a/giga/evmonly/executor_test.go b/giga/evmonly/executor_test.go index 3d7f4d000e..31c84bc00a 100644 --- a/giga/evmonly/executor_test.go +++ b/giga/evmonly/executor_test.go @@ -8,7 +8,9 @@ import ( "testing" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/params" "github.com/stretchr/testify/require" @@ -81,6 +83,217 @@ func TestExecutorDynamicFeeTx(t *testing.T) { require.Equal(t, big.NewInt(11), state.GetBalance(recipient)) } +func TestExecutorReceiptAndLogMetadata(t *testing.T) { + chainID := big.NewInt(713715) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + recipient := testAddress(0xa5) + logContract := testAddress(0xc2) + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000_000_000_000)) + state.SetCode(logContract, log0Code()) + + transfer := signLegacyTx(t, key, chainID, 0, &recipient, big.NewInt(3), nil) + emitLog := signLegacyTx(t, key, chainID, 1, &logContract, big.NewInt(0), nil) + transferTx := decodeTx(t, transfer) + emitLogTx := decodeTx(t, emitLog) + ctx := blockContext(chainID) + ctx.Number = 42 + ctx.BlockHash = testHash(0x42) + executor := NewExecutor(Config{}, WithState(state)) + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: ctx, + Txs: [][]byte{transfer, emitLog}, + }) + + require.NoError(t, err) + require.Len(t, result.Txs, 2) + require.Len(t, result.Receipts, 2) + + require.Equal(t, transferTx.Hash(), result.Receipts[0].TxHash) + require.Equal(t, uint(0), result.Receipts[0].TransactionIndex) + require.Equal(t, ctx.BlockHash, result.Receipts[0].BlockHash) + require.Equal(t, new(big.Int).SetUint64(ctx.Number), result.Receipts[0].BlockNumber) + require.Equal(t, result.Txs[0].GasUsed, result.Receipts[0].CumulativeGasUsed) + + require.Equal(t, emitLogTx.Hash(), result.Receipts[1].TxHash) + require.Equal(t, uint(1), result.Receipts[1].TransactionIndex) + require.Equal(t, result.GasUsed, result.Receipts[1].CumulativeGasUsed) + require.Len(t, result.Receipts[1].Logs, 1) + log := result.Receipts[1].Logs[0] + require.Equal(t, logContract, log.Address) + require.Equal(t, ctx.Number, log.BlockNumber) + require.Equal(t, ctx.BlockHash, log.BlockHash) + require.Equal(t, emitLogTx.Hash(), log.TxHash) + require.Equal(t, uint(1), log.TxIndex) + require.Equal(t, uint(0), log.Index) + + state.ApplyChangeSet(result.ChangeSet) + require.Equal(t, big.NewInt(3), state.GetBalance(recipient)) + require.Equal(t, uint64(2), state.GetNonce(sender)) +} + +func TestExecutorEVMFailureProducesReceiptAndContinues(t *testing.T) { + chainID := big.NewInt(713715) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + oogContract := testAddress(0xc3) + recipient := testAddress(0xa6) + keySlot := testHash(0x01) + value := testHash(0x02) + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000_000_000_000)) + state.SetCode(oogContract, storeCode(keySlot, value)) + + oogCall := signLegacyTxWithGas(t, key, chainID, 0, &oogContract, big.NewInt(0), nil, 22_000) + laterTransfer := signLegacyTx(t, key, chainID, 1, &recipient, big.NewInt(5), nil) + executor := NewExecutor(Config{}, WithState(state)) + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: blockContext(chainID), + Txs: [][]byte{oogCall, laterTransfer}, + }) + + require.NoError(t, err) + require.Len(t, result.Txs, 2) + require.Equal(t, ethtypes.ReceiptStatusFailed, result.Txs[0].Status) + require.True(t, errors.Is(result.Txs[0].Err, vm.ErrOutOfGas)) + require.Equal(t, uint64(22_000), result.Txs[0].GasUsed) + require.Equal(t, ethtypes.ReceiptStatusSuccessful, result.Txs[1].Status) + require.Equal(t, result.GasUsed, result.Receipts[1].CumulativeGasUsed) + + state.ApplyChangeSet(result.ChangeSet) + require.Equal(t, common.Hash{}, state.GetState(oogContract, keySlot)) + require.Equal(t, big.NewInt(5), state.GetBalance(recipient)) + require.Equal(t, uint64(2), state.GetNonce(sender)) +} + +func TestExecutorValidationFailuresAbortBlock(t *testing.T) { + chainID := big.NewInt(713715) + recipient := testAddress(0xa7) + + t.Run("invalid nonce", func(t *testing.T) { + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000_000_000_000)) + rawTx := signLegacyTx(t, key, chainID, 1, &recipient, big.NewInt(1), nil) + executor := NewExecutor(Config{}, WithState(state)) + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: blockContext(chainID), + Txs: [][]byte{rawTx}, + }) + + require.Error(t, err) + require.True(t, errors.Is(err, core.ErrNonceTooHigh)) + require.Nil(t, result) + require.Equal(t, uint64(0), state.GetNonce(sender)) + require.Equal(t, big.NewInt(0), state.GetBalance(recipient)) + }) + + t.Run("insufficient balance", func(t *testing.T) { + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1)) + rawTx := signLegacyTx(t, key, chainID, 0, &recipient, big.NewInt(1), nil) + executor := NewExecutor(Config{}, WithState(state)) + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: blockContext(chainID), + Txs: [][]byte{rawTx}, + }) + + require.Error(t, err) + require.True(t, errors.Is(err, core.ErrInsufficientFunds)) + require.Nil(t, result) + require.Equal(t, uint64(0), state.GetNonce(sender)) + require.Equal(t, big.NewInt(0), state.GetBalance(recipient)) + }) +} + +func TestExecutorCreatesContractThenUpdatesStorage(t *testing.T) { + chainID := big.NewInt(713715) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + storageKey := testHash(0x11) + storageValue := testHash(0x22) + runtime := storeCode(storageKey, storageValue) + contractAddr := crypto.CreateAddress(sender, 0) + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(2_000_000_000_000_000)) + + createContract := signLegacyTxWithGas(t, key, chainID, 0, nil, big.NewInt(0), initCode(runtime), 300_000) + callContract := signLegacyTx(t, key, chainID, 1, &contractAddr, big.NewInt(0), nil) + executor := NewExecutor(Config{}, WithState(state)) + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: blockContext(chainID), + Txs: [][]byte{createContract, callContract}, + }) + + require.NoError(t, err) + require.Len(t, result.Receipts, 2) + require.Equal(t, ethtypes.ReceiptStatusSuccessful, result.Txs[0].Status) + require.Equal(t, contractAddr, result.Txs[0].ContractAddress) + require.Equal(t, contractAddr, result.Receipts[0].ContractAddress) + require.Equal(t, ethtypes.ReceiptStatusSuccessful, result.Txs[1].Status) + + state.ApplyChangeSet(result.ChangeSet) + require.Equal(t, runtime, state.GetCode(contractAddr)) + require.Equal(t, storageValue, state.GetState(contractAddr, storageKey)) + require.Equal(t, uint64(2), state.GetNonce(sender)) +} + +func TestExecutorCreateSelfDestructThenTransferSameAddress(t *testing.T) { + chainID := big.NewInt(713715) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + beneficiary := testAddress(0xb2) + runtime := selfDestructCode(beneficiary) + contractAddr := crypto.CreateAddress(sender, 0) + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(2_000_000_000_000_000)) + + createContract := signLegacyTxWithGas(t, key, chainID, 0, nil, big.NewInt(0), initCode(runtime), 300_000) + destroyContract := signLegacyTx(t, key, chainID, 1, &contractAddr, big.NewInt(0), nil) + transferToDestroyed := signLegacyTx(t, key, chainID, 2, &contractAddr, big.NewInt(9), nil) + executor := NewExecutor(Config{ + ChainConfig: legacySelfDestructChainConfig(chainID), + }, WithState(state)) + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: blockContext(chainID), + Txs: [][]byte{createContract, destroyContract, transferToDestroyed}, + }) + + require.NoError(t, err) + require.Len(t, result.Receipts, 3) + for _, txResult := range result.Txs { + require.Equal(t, ethtypes.ReceiptStatusSuccessful, txResult.Status) + } + + state.ApplyChangeSet(result.ChangeSet) + require.Empty(t, state.GetCode(contractAddr)) + require.Equal(t, big.NewInt(9), state.GetBalance(contractAddr)) + require.Equal(t, big.NewInt(0), state.GetBalance(beneficiary)) + require.Equal(t, uint64(3), state.GetNonce(sender)) +} + func TestExecutorFinalisesAfterEachTx(t *testing.T) { chainID := big.NewInt(713715) key, err := crypto.GenerateKey() @@ -181,11 +394,16 @@ func TestExecutorCustomPrecompilePlaceholder(t *testing.T) { } func signLegacyTx(t *testing.T, key *ecdsa.PrivateKey, chainID *big.Int, nonce uint64, to *common.Address, value *big.Int, data []byte) []byte { + t.Helper() + return signLegacyTxWithGas(t, key, chainID, nonce, to, value, data, 100_000) +} + +func signLegacyTxWithGas(t *testing.T, key *ecdsa.PrivateKey, chainID *big.Int, nonce uint64, to *common.Address, value *big.Int, data []byte, gas uint64) []byte { t.Helper() tx := ethtypes.NewTx(ðtypes.LegacyTx{ Nonce: nonce, GasPrice: big.NewInt(1_000_000_000), - Gas: 100_000, + Gas: gas, To: to, Value: value, Data: data, @@ -197,6 +415,13 @@ func signLegacyTx(t *testing.T, key *ecdsa.PrivateKey, chainID *big.Int, nonce u return raw } +func decodeTx(t *testing.T, raw []byte) *ethtypes.Transaction { + t.Helper() + var tx ethtypes.Transaction + require.NoError(t, tx.UnmarshalBinary(raw)) + return &tx +} + func signDynamicFeeTx(t *testing.T, key *ecdsa.PrivateKey, chainID *big.Int, nonce uint64, to *common.Address, value *big.Int, data []byte) []byte { t.Helper() tx := ethtypes.NewTx(ðtypes.DynamicFeeTx{ @@ -250,6 +475,42 @@ func selfDestructCode(beneficiary common.Address) []byte { return append(code, 0xff) } +func log0Code() []byte { + return []byte{0x60, 0x00, 0x60, 0x00, 0xa0, 0x00} +} + +func storeCode(key, value common.Hash) []byte { + code := append([]byte{0x7f}, value.Bytes()...) + code = append(code, 0x7f) + code = append(code, key.Bytes()...) + return append(code, 0x55, 0x00) +} + +func initCode(runtime []byte) []byte { + if len(runtime) > 255 { + panic("test runtime too large") + } + runtimeLen := byte(len(runtime)) //nolint:gosec // bounded by the check above. + code := []byte{ + 0x60, runtimeLen, + 0x60, 0x0c, + 0x60, 0x00, + 0x39, + 0x60, runtimeLen, + 0x60, 0x00, + 0xf3, + } + return append(code, runtime...) +} + +func testAddress(suffix byte) common.Address { + return common.BytesToAddress([]byte{suffix}) +} + +func testHash(suffix byte) common.Hash { + return common.BytesToHash([]byte{suffix}) +} + type staticPrecompileRegistry struct { addr common.Address } From 23fc6d37075a2deb6ab938b6e9a6a096c4966998 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Wed, 24 Jun 2026 11:47:45 +0800 Subject: [PATCH 10/27] add evmonly validation edge case tests --- giga/evmonly/executor_test.go | 237 +++++++++++++++++++++++++++++++++- 1 file changed, 232 insertions(+), 5 deletions(-) diff --git a/giga/evmonly/executor_test.go b/giga/evmonly/executor_test.go index 31c84bc00a..9e04a4fdc0 100644 --- a/giga/evmonly/executor_test.go +++ b/giga/evmonly/executor_test.go @@ -18,6 +18,8 @@ import ( "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles" ) +const testGasPriceWei = 1_000_000_000 + func TestExecutorEmptyBlock(t *testing.T) { executor := NewExecutor(Config{}) @@ -177,7 +179,7 @@ func TestExecutorValidationFailuresAbortBlock(t *testing.T) { chainID := big.NewInt(713715) recipient := testAddress(0xa7) - t.Run("invalid nonce", func(t *testing.T) { + t.Run("nonce too high", func(t *testing.T) { key, err := crypto.GenerateKey() require.NoError(t, err) sender := crypto.PubkeyToAddress(key.PublicKey) @@ -199,6 +201,29 @@ func TestExecutorValidationFailuresAbortBlock(t *testing.T) { require.Equal(t, big.NewInt(0), state.GetBalance(recipient)) }) + t.Run("nonce too low", func(t *testing.T) { + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000_000_000_000)) + state.SetNonce(sender, 1) + rawTx := signLegacyTx(t, key, chainID, 0, &recipient, big.NewInt(1), nil) + executor := NewExecutor(Config{}, WithState(state)) + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: blockContext(chainID), + Txs: [][]byte{rawTx}, + }) + + require.Error(t, err) + require.True(t, errors.Is(err, core.ErrNonceTooLow)) + require.Nil(t, result) + require.Equal(t, uint64(1), state.GetNonce(sender)) + require.Equal(t, big.NewInt(0), state.GetBalance(recipient)) + }) + t.Run("insufficient balance", func(t *testing.T) { key, err := crypto.GenerateKey() require.NoError(t, err) @@ -220,6 +245,169 @@ func TestExecutorValidationFailuresAbortBlock(t *testing.T) { require.Equal(t, uint64(0), state.GetNonce(sender)) require.Equal(t, big.NewInt(0), state.GetBalance(recipient)) }) + + t.Run("min gas price", func(t *testing.T) { + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000_000_000_000)) + rawTx := signLegacyTxWithGasPrice(t, key, chainID, 0, &recipient, big.NewInt(1), nil, 100_000, big.NewInt(1)) + executor := NewExecutor(Config{ + MinGasPrice: big.NewInt(2), + }, WithState(state)) + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: blockContext(chainID), + Txs: [][]byte{rawTx}, + }) + + require.Error(t, err) + require.True(t, errors.Is(err, errInsufficientGasPrice)) + require.Nil(t, result) + require.Equal(t, uint64(0), state.GetNonce(sender)) + require.Equal(t, big.NewInt(0), state.GetBalance(recipient)) + }) + + t.Run("fee cap below base fee", func(t *testing.T) { + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000_000_000_000)) + rawTx := signDynamicFeeTxWithFees( + t, + key, + chainID, + 0, + &recipient, + big.NewInt(1), + nil, + big.NewInt(testGasPriceWei), + big.NewInt(testGasPriceWei), + 100_000, + ) + executor := NewExecutor(Config{ + DisableGasPriceCheck: true, + }, WithState(state)) + ctx := blockContext(chainID) + ctx.BaseFee = big.NewInt(2 * testGasPriceWei) + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: ctx, + Txs: [][]byte{rawTx}, + }) + + require.Error(t, err) + require.True(t, errors.Is(err, core.ErrFeeCapTooLow)) + require.Nil(t, result) + require.Equal(t, uint64(0), state.GetNonce(sender)) + require.Equal(t, big.NewInt(0), state.GetBalance(recipient)) + }) + + t.Run("intrinsic gas too low", func(t *testing.T) { + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000_000_000_000)) + rawTx := signLegacyTxWithGas(t, key, chainID, 0, &recipient, big.NewInt(1), nil, 20_000) + executor := NewExecutor(Config{}, WithState(state)) + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: blockContext(chainID), + Txs: [][]byte{rawTx}, + }) + + require.Error(t, err) + require.True(t, errors.Is(err, core.ErrIntrinsicGas)) + require.Nil(t, result) + require.Equal(t, uint64(0), state.GetNonce(sender)) + require.Equal(t, big.NewInt(0), state.GetBalance(recipient)) + }) + + t.Run("block gas exhausted", func(t *testing.T) { + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000_000_000_000)) + firstTransfer := signLegacyTxWithGas(t, key, chainID, 0, &recipient, big.NewInt(1), nil, 21_000) + secondTransfer := signLegacyTxWithGas(t, key, chainID, 1, &recipient, big.NewInt(1), nil, 21_000) + executor := NewExecutor(Config{}, WithState(state)) + ctx := blockContext(chainID) + ctx.GasLimit = 30_000 + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: ctx, + Txs: [][]byte{firstTransfer, secondTransfer}, + }) + + require.Error(t, err) + require.True(t, errors.Is(err, core.ErrGasLimitReached)) + require.Nil(t, result) + require.Equal(t, uint64(0), state.GetNonce(sender)) + require.Equal(t, big.NewInt(0), state.GetBalance(recipient)) + }) +} + +func TestExecutorRejectsBadSignatureBeforeExecution(t *testing.T) { + chainID := big.NewInt(713715) + recipient := testAddress(0xa8) + + t.Run("wrong chain id", func(t *testing.T) { + wrongChainID := big.NewInt(1) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000_000_000_000)) + rawTx := signLegacyTx(t, key, wrongChainID, 0, &recipient, big.NewInt(1), nil) + executor := NewExecutor(Config{}, WithState(state)) + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: blockContext(chainID), + Txs: [][]byte{rawTx}, + }) + + require.Error(t, err) + require.True(t, errors.Is(err, ethtypes.ErrInvalidChainId)) + require.Nil(t, result) + require.Equal(t, uint64(0), state.GetNonce(sender)) + require.Equal(t, big.NewInt(0), state.GetBalance(recipient)) + }) + + t.Run("invalid signature values", func(t *testing.T) { + state := NewMemoryState() + rawTx := legacyTxWithSignatureValues( + t, + 0, + &recipient, + big.NewInt(1), + nil, + 100_000, + big.NewInt(testGasPriceWei), + new(big.Int).Add(big.NewInt(35), new(big.Int).Mul(big.NewInt(2), chainID)), + new(big.Int), + new(big.Int), + ) + executor := NewExecutor(Config{}, WithState(state)) + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: blockContext(chainID), + Txs: [][]byte{rawTx}, + }) + + require.Error(t, err) + require.True(t, errors.Is(err, ethtypes.ErrInvalidSig)) + require.Nil(t, result) + require.Equal(t, big.NewInt(0), state.GetBalance(recipient)) + }) } func TestExecutorCreatesContractThenUpdatesStorage(t *testing.T) { @@ -399,10 +587,15 @@ func signLegacyTx(t *testing.T, key *ecdsa.PrivateKey, chainID *big.Int, nonce u } func signLegacyTxWithGas(t *testing.T, key *ecdsa.PrivateKey, chainID *big.Int, nonce uint64, to *common.Address, value *big.Int, data []byte, gas uint64) []byte { + t.Helper() + return signLegacyTxWithGasPrice(t, key, chainID, nonce, to, value, data, gas, big.NewInt(testGasPriceWei)) +} + +func signLegacyTxWithGasPrice(t *testing.T, key *ecdsa.PrivateKey, chainID *big.Int, nonce uint64, to *common.Address, value *big.Int, data []byte, gas uint64, gasPrice *big.Int) []byte { t.Helper() tx := ethtypes.NewTx(ðtypes.LegacyTx{ Nonce: nonce, - GasPrice: big.NewInt(1_000_000_000), + GasPrice: new(big.Int).Set(gasPrice), Gas: gas, To: to, Value: value, @@ -415,6 +608,24 @@ func signLegacyTxWithGas(t *testing.T, key *ecdsa.PrivateKey, chainID *big.Int, return raw } +func legacyTxWithSignatureValues(t *testing.T, nonce uint64, to *common.Address, value *big.Int, data []byte, gas uint64, gasPrice *big.Int, v *big.Int, r *big.Int, s *big.Int) []byte { + t.Helper() + tx := ethtypes.NewTx(ðtypes.LegacyTx{ + Nonce: nonce, + GasPrice: new(big.Int).Set(gasPrice), + Gas: gas, + To: to, + Value: value, + Data: data, + V: new(big.Int).Set(v), + R: new(big.Int).Set(r), + S: new(big.Int).Set(s), + }) + raw, err := tx.MarshalBinary() + require.NoError(t, err) + return raw +} + func decodeTx(t *testing.T, raw []byte) *ethtypes.Transaction { t.Helper() var tx ethtypes.Transaction @@ -423,13 +634,29 @@ func decodeTx(t *testing.T, raw []byte) *ethtypes.Transaction { } func signDynamicFeeTx(t *testing.T, key *ecdsa.PrivateKey, chainID *big.Int, nonce uint64, to *common.Address, value *big.Int, data []byte) []byte { + t.Helper() + return signDynamicFeeTxWithFees( + t, + key, + chainID, + nonce, + to, + value, + data, + big.NewInt(testGasPriceWei), + big.NewInt(testGasPriceWei), + 100_000, + ) +} + +func signDynamicFeeTxWithFees(t *testing.T, key *ecdsa.PrivateKey, chainID *big.Int, nonce uint64, to *common.Address, value *big.Int, data []byte, gasTipCap *big.Int, gasFeeCap *big.Int, gas uint64) []byte { t.Helper() tx := ethtypes.NewTx(ðtypes.DynamicFeeTx{ ChainID: chainID, Nonce: nonce, - GasTipCap: big.NewInt(1_000_000_000), - GasFeeCap: big.NewInt(1_000_000_000), - Gas: 100_000, + GasTipCap: new(big.Int).Set(gasTipCap), + GasFeeCap: new(big.Int).Set(gasFeeCap), + Gas: gas, To: to, Value: value, Data: data, From 9c28df76b131dcd74670e5ce11ebd87d3e4844ab Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Wed, 24 Jun 2026 12:41:27 +0800 Subject: [PATCH 11/27] fix evmonly state finalization edge cases --- giga/evmonly/executor_test.go | 70 +++++++++++++++++++++++++++++++++++ giga/evmonly/state_db.go | 17 ++++++--- 2 files changed, 82 insertions(+), 5 deletions(-) diff --git a/giga/evmonly/executor_test.go b/giga/evmonly/executor_test.go index 9e04a4fdc0..440e9928f5 100644 --- a/giga/evmonly/executor_test.go +++ b/giga/evmonly/executor_test.go @@ -478,10 +478,44 @@ func TestExecutorCreateSelfDestructThenTransferSameAddress(t *testing.T) { state.ApplyChangeSet(result.ChangeSet) require.Empty(t, state.GetCode(contractAddr)) require.Equal(t, big.NewInt(9), state.GetBalance(contractAddr)) + require.Equal(t, uint64(0), state.GetNonce(contractAddr)) require.Equal(t, big.NewInt(0), state.GetBalance(beneficiary)) require.Equal(t, uint64(3), state.GetNonce(sender)) } +func TestExecutorEIP6780CreateFlagExpiresAfterTx(t *testing.T) { + chainID := big.NewInt(713715) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + beneficiary := testAddress(0xb3) + runtime := selfDestructCode(beneficiary) + contractAddr := crypto.CreateAddress(sender, 0) + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(2_000_000_000_000_000)) + + createContract := signLegacyTxWithGas(t, key, chainID, 0, nil, big.NewInt(0), initCode(runtime), 300_000) + selfDestructAfterCreateTx := signLegacyTx(t, key, chainID, 1, &contractAddr, big.NewInt(0), nil) + executor := NewExecutor(Config{}, WithState(state)) + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: blockContext(chainID), + Txs: [][]byte{createContract, selfDestructAfterCreateTx}, + }) + + require.NoError(t, err) + require.Len(t, result.Receipts, 2) + for _, txResult := range result.Txs { + require.Equal(t, ethtypes.ReceiptStatusSuccessful, txResult.Status) + } + + state.ApplyChangeSet(result.ChangeSet) + require.Equal(t, runtime, state.GetCode(contractAddr)) + require.Equal(t, uint64(1), state.GetNonce(contractAddr)) + require.Equal(t, big.NewInt(0), state.GetBalance(beneficiary)) +} + func TestExecutorFinalisesAfterEachTx(t *testing.T) { chainID := big.NewInt(713715) key, err := crypto.GenerateKey() @@ -545,6 +579,42 @@ func TestSnapshotRevertRestoresBaseState(t *testing.T) { require.Empty(t, stateDB.ChangeSet().Storage) } +func TestStateDBFirstStorageReadPreservesBase(t *testing.T) { + addr := testAddress(0xa9) + key := testHash(0x01) + value := testHash(0x02) + nextValue := testHash(0x03) + + t.Run("get state", func(t *testing.T) { + state := NewMemoryState() + state.SetState(addr, key, value) + stateDB := newNativeStateDB(state) + + require.Equal(t, value, stateDB.GetState(addr, key)) + require.Empty(t, stateDB.ChangeSet().Storage) + }) + + t.Run("get committed state", func(t *testing.T) { + state := NewMemoryState() + state.SetState(addr, key, value) + stateDB := newNativeStateDB(state) + + require.Equal(t, value, stateDB.GetCommittedState(addr, key)) + require.Empty(t, stateDB.ChangeSet().Storage) + }) + + t.Run("set state returns persisted previous value", func(t *testing.T) { + state := NewMemoryState() + state.SetState(addr, key, value) + stateDB := newNativeStateDB(state) + + require.Equal(t, value, stateDB.SetState(addr, key, nextValue)) + changes := stateDB.ChangeSet() + require.Len(t, changes.Storage, 1) + require.Equal(t, nextValue, changes.Storage[0].Value) + }) +} + func TestFinaliseClearsRefund(t *testing.T) { stateDB := newNativeStateDB(NewMemoryState()) stateDB.AddRefund(12) diff --git a/giga/evmonly/state_db.go b/giga/evmonly/state_db.go index 210a5ab7f7..0c2214a3fd 100644 --- a/giga/evmonly/state_db.go +++ b/giga/evmonly/state_db.go @@ -292,8 +292,9 @@ func (s *nativeStateDB) SelfDestruct(addr common.Address) uint256.Int { } func (s *nativeStateDB) SelfDestruct6780(addr common.Address) (uint256.Int, bool) { - if !s.account(addr).Created { - return *uint256.NewInt(0), false + acct := s.account(addr) + if !acct.Created { + return *acct.Balance.Clone(), false } return s.SelfDestruct(addr), true } @@ -422,7 +423,10 @@ func (s *nativeStateDB) Finalise(bool) { if acct.SelfDestructed { acct.Code = nil acct.Storage = map[common.Hash]common.Hash{} + acct.Nonce = 0 + acct.SelfDestructed = false } + acct.Created = false } s.refund = 0 } @@ -494,9 +498,12 @@ func (s *nativeStateDB) account(addr common.Address) *nativeAccount { if acct, ok := s.accounts[addr]; ok { return acct } - acct := s.loadAccount(addr) - s.accounts[addr] = acct.clone() - s.base[addr] = acct.clone() + base, ok := s.base[addr] + if !ok { + base = s.loadAccount(addr) + s.base[addr] = base.clone() + } + s.accounts[addr] = base.clone() return s.accounts[addr] } From 10c3108f790ebcb48f9a09916f36d2c77274a673 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Mon, 29 Jun 2026 20:32:15 +0800 Subject: [PATCH 12/27] add evmonly executor OCC --- giga/evmonly/README.md | 1 + giga/evmonly/config.go | 1 + giga/evmonly/executor.go | 18 ++ giga/evmonly/executor_test.go | 79 +++++++++ giga/evmonly/occ.go | 308 ++++++++++++++++++++++++++++++++++ giga/evmonly/state_db.go | 200 ++++++++++++++++++++-- 6 files changed, 591 insertions(+), 16 deletions(-) create mode 100644 giga/evmonly/occ.go diff --git a/giga/evmonly/README.md b/giga/evmonly/README.md index 928a203ce3..9c2f9a9a5d 100644 --- a/giga/evmonly/README.md +++ b/giga/evmonly/README.md @@ -28,6 +28,7 @@ The `evmonly` package currently provides: - go-ethereum `core.ApplyMessage` execution against an SDK-free `vm.StateDB` - key-addressable state reads for balance, nonce, code, and storage - deterministic post-block `StateChangeSet` construction +- optional executor-internal OCC for non-conflicting transaction sets - Ethereum receipt construction with logs, bloom, gas, tx hash, block metadata, contract address, and effective gas price - a map-backed `MemoryState` for tests and early integration diff --git a/giga/evmonly/config.go b/giga/evmonly/config.go index 8371b4c845..e376dfa2d3 100644 --- a/giga/evmonly/config.go +++ b/giga/evmonly/config.go @@ -15,6 +15,7 @@ type Config struct { MinGasPrice *big.Int ChainConfig *params.ChainConfig CustomPrecompiles precompiles.Registry + OCCWorkers int } func DefaultConfig() Config { diff --git a/giga/evmonly/executor.go b/giga/evmonly/executor.go index ad49d485f7..1d3599b8f8 100644 --- a/giga/evmonly/executor.go +++ b/giga/evmonly/executor.go @@ -51,7 +51,23 @@ func (e *Executor) ExecuteBlock(ctx context.Context, req BlockRequest) (*BlockRe if len(req.Txs) == 0 { return &BlockResult{}, nil } + if e.useOCC(req) { + return e.executeBlockOCC(ctx, req) + } + return e.executeBlockSequential(ctx, req) +} + +func (e *Executor) useOCC(req BlockRequest) bool { + if e.cfg.OCCWorkers <= 1 || len(req.Txs) <= 1 { + return false + } + if e.cfg.CustomPrecompiles == nil { + return true + } + return len(e.cfg.CustomPrecompiles.Addresses()) == 0 +} +func (e *Executor) executeBlockSequential(ctx context.Context, req BlockRequest) (*BlockResult, error) { chainConfig := e.chainConfig(req.Context) signer := ethtypes.MakeSigner(chainConfig, new(big.Int).SetUint64(req.Context.Number), req.Context.Time) parsed, err := parseBlockTxs(ctx, req.Txs, signer) @@ -93,6 +109,7 @@ func (e *Executor) ExecuteBlock(ctx context.Context, req BlockRequest) (*BlockRe result.GasUsed += txResult.GasUsed txIndexUint++ } + stateDB.clearSnapshots() stateDB.Finalise(true) result.ChangeSet = stateDB.ChangeSet() return result, nil @@ -133,6 +150,7 @@ func (e *Executor) executeTx( if err != nil { return TxResult{Hash: tx.Hash(), Sender: p.sender, To: tx.To(), Err: err}, nil, err } + stateDB.clearSnapshots() stateDB.Finalise(true) txLogs := append([]*ethtypes.Log(nil), stateDB.logs[logStart:]...) diff --git a/giga/evmonly/executor_test.go b/giga/evmonly/executor_test.go index 440e9928f5..04ef979cdf 100644 --- a/giga/evmonly/executor_test.go +++ b/giga/evmonly/executor_test.go @@ -85,6 +85,85 @@ func TestExecutorDynamicFeeTx(t *testing.T) { require.Equal(t, big.NewInt(11), state.GetBalance(recipient)) } +func TestExecutorOCCNonConflictingTransfersMatchSequential(t *testing.T) { + chainID := big.NewInt(713715) + txCount := 12 + rawTxs := make([][]byte, 0, txCount) + senders := make([]common.Address, 0, txCount) + recipients := make([]common.Address, 0, txCount) + seqState := NewMemoryState() + occState := NewMemoryState() + + for i := 0; i < txCount; i++ { + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + recipient := common.BigToAddress(big.NewInt(int64(10_000 + i))) + senders = append(senders, sender) + recipients = append(recipients, recipient) + seqState.SetBalance(sender, big.NewInt(1_000_000)) + occState.SetBalance(sender, big.NewInt(1_000_000)) + rawTxs = append(rawTxs, signLegacyTxWithGasPrice(t, key, chainID, 0, &recipient, big.NewInt(7), nil, 100_000, big.NewInt(0))) + } + + cfg := Config{MinGasPrice: big.NewInt(0)} + seqExecutor := NewExecutor(cfg, WithState(seqState)) + occExecutor := NewExecutor(Config{MinGasPrice: big.NewInt(0), OCCWorkers: 4}, WithState(occState)) + req := BlockRequest{Context: blockContext(chainID), Txs: rawTxs} + + seqResult, err := seqExecutor.ExecuteBlock(context.Background(), req) + require.NoError(t, err) + occResult, err := occExecutor.ExecuteBlock(context.Background(), req) + require.NoError(t, err) + + require.Equal(t, seqResult.GasUsed, occResult.GasUsed) + require.Len(t, occResult.Txs, txCount) + require.Len(t, occResult.Receipts, txCount) + for i := range txCount { + require.Equal(t, seqResult.Txs[i].Hash, occResult.Txs[i].Hash) + require.Equal(t, seqResult.Txs[i].Status, occResult.Txs[i].Status) + require.Equal(t, seqResult.Receipts[i].CumulativeGasUsed, occResult.Receipts[i].CumulativeGasUsed) + } + + seqState.ApplyChangeSet(seqResult.ChangeSet) + occState.ApplyChangeSet(occResult.ChangeSet) + for i := range txCount { + require.Equal(t, seqState.GetBalance(senders[i]), occState.GetBalance(senders[i])) + require.Equal(t, seqState.GetNonce(senders[i]), occState.GetNonce(senders[i])) + require.Equal(t, seqState.GetBalance(recipients[i]), occState.GetBalance(recipients[i])) + } +} + +func TestExecutorOCCConflictingTransfersMatchSequential(t *testing.T) { + chainID := big.NewInt(713715) + txCount := 8 + recipient := testAddress(0xdd) + rawTxs := make([][]byte, 0, txCount) + seqState := NewMemoryState() + occState := NewMemoryState() + + for i := 0; i < txCount; i++ { + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + seqState.SetBalance(sender, big.NewInt(1_000_000)) + occState.SetBalance(sender, big.NewInt(1_000_000)) + rawTxs = append(rawTxs, signLegacyTxWithGasPrice(t, key, chainID, 0, &recipient, big.NewInt(3), nil, 100_000, big.NewInt(0))) + } + + req := BlockRequest{Context: blockContext(chainID), Txs: rawTxs} + seqResult, err := NewExecutor(Config{MinGasPrice: big.NewInt(0)}, WithState(seqState)).ExecuteBlock(context.Background(), req) + require.NoError(t, err) + occResult, err := NewExecutor(Config{MinGasPrice: big.NewInt(0), OCCWorkers: 4}, WithState(occState)).ExecuteBlock(context.Background(), req) + require.NoError(t, err) + + seqState.ApplyChangeSet(seqResult.ChangeSet) + occState.ApplyChangeSet(occResult.ChangeSet) + require.Equal(t, seqResult.GasUsed, occResult.GasUsed) + require.Equal(t, seqState.GetBalance(recipient), occState.GetBalance(recipient)) + require.Equal(t, big.NewInt(int64(txCount*3)), occState.GetBalance(recipient)) +} + func TestExecutorReceiptAndLogMetadata(t *testing.T) { chainID := big.NewInt(713715) key, err := crypto.GenerateKey() diff --git a/giga/evmonly/occ.go b/giga/evmonly/occ.go new file mode 100644 index 0000000000..6edf0d9b87 --- /dev/null +++ b/giga/evmonly/occ.go @@ -0,0 +1,308 @@ +package evmonly + +import ( + "bytes" + "context" + "fmt" + "math" + "math/big" + "sort" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/params" + "golang.org/x/sync/errgroup" +) + +type occTxExecution struct { + txResult TxResult + receipt *ethtypes.Receipt + changeSet StateChangeSet + readSet map[stateAccessKey]struct{} + writeSet map[stateAccessKey]struct{} + gasUsed uint64 +} + +func (e *Executor) executeBlockOCC(ctx context.Context, req BlockRequest) (*BlockResult, error) { + chainConfig := e.chainConfig(req.Context) + signer := ethtypes.MakeSigner(chainConfig, new(big.Int).SetUint64(req.Context.Number), req.Context.Time) + blockCtx := buildBlockContext(req.Context) + baseFee := cloneBig(req.Context.BaseFee) + gasLimit := req.Context.GasLimit + if gasLimit == 0 { + gasLimit = math.MaxUint64 + } + + workers := e.cfg.OCCWorkers + if workers > len(req.Txs) { + workers = len(req.Txs) + } + results := make([]occTxExecution, len(req.Txs)) + jobs := make(chan int) + group, groupCtx := errgroup.WithContext(ctx) + + group.Go(func() error { + defer close(jobs) + for i := range req.Txs { + select { + case jobs <- i: + case <-groupCtx.Done(): + return groupCtx.Err() + } + } + return nil + }) + for range workers { + group.Go(func() error { + for { + select { + case <-groupCtx.Done(): + return groupCtx.Err() + case idx, ok := <-jobs: + if !ok { + return nil + } + result, err := e.executeTxSpeculative(groupCtx, req, idx, signer, chainConfig, blockCtx, baseFee, gasLimit) + if err != nil { + return err + } + results[idx] = result + } + } + }) + } + if err := group.Wait(); err != nil { + return nil, err + } + if !validateOCCResults(results, gasLimit) { + return e.executeBlockSequential(ctx, req) + } + return mergeOCCResults(results), nil +} + +func (e *Executor) executeTxSpeculative( + ctx context.Context, + req BlockRequest, + txIndex int, + signer ethtypes.Signer, + chainConfig *params.ChainConfig, + blockCtx vm.BlockContext, + baseFee *big.Int, + gasLimit uint64, +) (occTxExecution, error) { + tx, sender, err := parseTx(req.Txs[txIndex], signer) + if err != nil { + return occTxExecution{}, fmt.Errorf("parse tx %d: %w", txIndex, err) + } + stateDB := newNativeStateDB(e.state) + stateDB.enableAccessTracking() + evm := vm.NewEVM(blockCtx, stateDB, chainConfig, vm.Config{}, nil) + stateDB.SetEVM(evm) + gasPool := new(core.GasPool).AddGas(gasLimit) + txResult, receipt, err := e.executeTx( + evm, + stateDB, + gasPool, + req.Context, + parsedTx{tx: tx, sender: sender}, + txIndex, + uint(txIndex), + baseFee, + signer, + ) + if err != nil { + return occTxExecution{}, fmt.Errorf("execute tx %d %s: %w", txIndex, tx.Hash(), err) + } + readSet, writeSet := stateDB.accessSets() + return occTxExecution{ + txResult: txResult, + receipt: receipt, + changeSet: stateDB.ChangeSet(), + readSet: readSet, + writeSet: writeSet, + gasUsed: txResult.GasUsed, + }, nil +} + +func validateOCCResults(results []occTxExecution, gasLimit uint64) bool { + writes := newStateAccessIndex() + var totalGas uint64 + for _, result := range results { + if result.gasUsed > math.MaxUint64-totalGas { + return false + } + totalGas += result.gasUsed + if totalGas > gasLimit { + return false + } + if writes.conflictsWithAny(result.readSet) || writes.conflictsWithAny(result.writeSet) { + return false + } + writes.addAll(result.writeSet) + } + return true +} + +func mergeOCCResults(results []occTxExecution) *BlockResult { + blockResult := &BlockResult{ + Txs: make([]TxResult, len(results)), + Receipts: make(ethtypes.Receipts, len(results)), + } + var logIndex uint + for i, result := range results { + blockResult.GasUsed += result.gasUsed + result.txResult.CumulativeGasUsed = blockResult.GasUsed + result.receipt.CumulativeGasUsed = blockResult.GasUsed + for _, log := range result.receipt.Logs { + log.Index = logIndex + logIndex++ + } + blockResult.Txs[i] = result.txResult + blockResult.Receipts[i] = result.receipt + } + blockResult.ChangeSet = mergeChangeSets(results) + return blockResult +} + +type stateAccessIndex struct { + exact map[stateAccessKey]struct{} + account map[common.Address]struct{} + touched map[common.Address]struct{} +} + +func newStateAccessIndex() *stateAccessIndex { + return &stateAccessIndex{ + exact: map[stateAccessKey]struct{}{}, + account: map[common.Address]struct{}{}, + touched: map[common.Address]struct{}{}, + } +} + +func (i *stateAccessIndex) conflictsWithAny(set map[stateAccessKey]struct{}) bool { + for key := range set { + if i.conflictsWith(key) { + return true + } + } + return false +} + +func (i *stateAccessIndex) conflictsWith(key stateAccessKey) bool { + if _, ok := i.exact[key]; ok { + return true + } + if _, ok := i.account[key.address]; ok { + return true + } + if key.kind == stateAccessAccount { + _, ok := i.touched[key.address] + return ok + } + return false +} + +func (i *stateAccessIndex) addAll(set map[stateAccessKey]struct{}) { + for key := range set { + i.exact[key] = struct{}{} + i.touched[key.address] = struct{}{} + if key.kind == stateAccessAccount { + i.account[key.address] = struct{}{} + } + } +} + +type storageChangeKey struct { + address common.Address + key common.Hash +} + +func mergeChangeSets(results []occTxExecution) StateChangeSet { + balances := map[common.Address]*big.Int{} + nonces := map[common.Address]uint64{} + code := map[common.Address]CodeChange{} + storage := map[storageChangeKey]StorageChange{} + + for _, result := range results { + for _, change := range result.changeSet.Balances { + balances[change.Address] = cloneBig(change.Balance) + } + for _, change := range result.changeSet.Nonces { + nonces[change.Address] = change.Nonce + } + for _, change := range result.changeSet.Code { + code[change.Address] = CodeChange{ + Address: change.Address, + Code: cloneBytes(change.Code), + Delete: change.Delete, + } + } + for _, change := range result.changeSet.Storage { + storage[storageChangeKey{address: change.Address, key: change.Key}] = change + } + } + + var merged StateChangeSet + balanceAddrs := sortedAddressesFromBigMap(balances) + for _, addr := range balanceAddrs { + merged.Balances = append(merged.Balances, BalanceChange{Address: addr, Balance: cloneBig(balances[addr])}) + } + nonceAddrs := sortedAddressesFromUint64Map(nonces) + for _, addr := range nonceAddrs { + merged.Nonces = append(merged.Nonces, NonceChange{Address: addr, Nonce: nonces[addr]}) + } + codeAddrs := sortedAddressesFromCodeMap(code) + for _, addr := range codeAddrs { + change := code[addr] + change.Code = cloneBytes(change.Code) + merged.Code = append(merged.Code, change) + } + storageKeys := make([]storageChangeKey, 0, len(storage)) + for key := range storage { + storageKeys = append(storageKeys, key) + } + sort.Slice(storageKeys, func(i, j int) bool { + if cmp := bytes.Compare(storageKeys[i].address[:], storageKeys[j].address[:]); cmp != 0 { + return cmp < 0 + } + return bytes.Compare(storageKeys[i].key[:], storageKeys[j].key[:]) < 0 + }) + for _, key := range storageKeys { + merged.Storage = append(merged.Storage, storage[key]) + } + return merged +} + +func sortedAddressesFromBigMap(values map[common.Address]*big.Int) []common.Address { + addrs := make([]common.Address, 0, len(values)) + for addr := range values { + addrs = append(addrs, addr) + } + sort.Slice(addrs, func(i, j int) bool { + return bytes.Compare(addrs[i][:], addrs[j][:]) < 0 + }) + return addrs +} + +func sortedAddressesFromUint64Map(values map[common.Address]uint64) []common.Address { + addrs := make([]common.Address, 0, len(values)) + for addr := range values { + addrs = append(addrs, addr) + } + sort.Slice(addrs, func(i, j int) bool { + return bytes.Compare(addrs[i][:], addrs[j][:]) < 0 + }) + return addrs +} + +func sortedAddressesFromCodeMap(values map[common.Address]CodeChange) []common.Address { + addrs := make([]common.Address, 0, len(values)) + for addr := range values { + addrs = append(addrs, addr) + } + sort.Slice(addrs, func(i, j int) bool { + return bytes.Compare(addrs[i][:], addrs[j][:]) < 0 + }) + return addrs +} diff --git a/giga/evmonly/state_db.go b/giga/evmonly/state_db.go index 0c2214a3fd..f3a929042a 100644 --- a/giga/evmonly/state_db.go +++ b/giga/evmonly/state_db.go @@ -31,7 +31,11 @@ type nativeStateDB struct { accessList accessList transientStates map[common.Address]map[common.Hash]common.Hash + finaliseAddrs map[common.Address]struct{} + journal []nativeJournalEntry snapshots []nativeSnapshot + readSet map[stateAccessKey]struct{} + writeSet map[stateAccessKey]struct{} txHash common.Hash txIndex int @@ -50,21 +54,50 @@ type nativeAccount struct { } type nativeSnapshot struct { - accounts map[common.Address]*nativeAccount - base map[common.Address]*nativeAccount + journalLen int refund uint64 - logs []*ethtypes.Log + logsLen int accessList accessList transientStates map[common.Address]map[common.Hash]common.Hash + finaliseAddrs map[common.Address]struct{} preimages map[common.Hash][]byte + journaledAddrs map[common.Address]struct{} err error } +type nativeJournalKind uint8 + +const ( + nativeJournalAccount nativeJournalKind = iota +) + +type nativeJournalEntry struct { + kind nativeJournalKind + address common.Address + account *nativeAccount +} + type accessList struct { addresses map[common.Address]struct{} slots map[common.Address]map[common.Hash]struct{} } +type stateAccessKind uint8 + +const ( + stateAccessAccount stateAccessKind = iota + stateAccessBalance + stateAccessNonce + stateAccessCode + stateAccessStorage +) + +type stateAccessKey struct { + kind stateAccessKind + address common.Address + slot common.Hash +} + func newNativeStateDB(source StateReader) *nativeStateDB { if source == nil { source = NewMemoryState() @@ -76,6 +109,7 @@ func newNativeStateDB(source StateReader) *nativeStateDB { preimages: map[common.Hash][]byte{}, accessList: newAccessList(), transientStates: map[common.Address]map[common.Hash]common.Hash{}, + finaliseAddrs: map[common.Address]struct{}{}, } } @@ -132,16 +166,23 @@ func (s *nativeStateDB) ChangeSet() StateChangeSet { func (s *nativeStateDB) CreateAccount(addr common.Address) { acct := s.account(addr) + s.recordAccount(addr) + s.markWrite(stateAccessKey{kind: stateAccessAccount, address: addr}) balance := acct.Balance.Clone() *acct = nativeAccount{ Balance: balance, Storage: map[common.Hash]common.Hash{}, Created: true, } + s.markForFinalise(addr) } func (s *nativeStateDB) CreateContract(addr common.Address) { - s.account(addr).Created = true + acct := s.account(addr) + s.recordAccount(addr) + s.markWrite(stateAccessKey{kind: stateAccessAccount, address: addr}) + acct.Created = true + s.markForFinalise(addr) } func (s *nativeStateDB) SubBalance(addr common.Address, amount *uint256.Int, _ tracing.BalanceChangeReason) uint256.Int { @@ -154,6 +195,8 @@ func (s *nativeStateDB) SubBalance(addr common.Address, amount *uint256.Int, _ t s.err = errInsufficientBalance return prev } + s.recordAccount(addr) + s.markWrite(stateAccessKey{kind: stateAccessBalance, address: addr}) acct.Balance.Sub(acct.Balance, amount) return prev } @@ -164,16 +207,21 @@ func (s *nativeStateDB) AddBalance(addr common.Address, amount *uint256.Int, _ t return prev } acct := s.account(addr) + s.recordAccount(addr) + s.markWrite(stateAccessKey{kind: stateAccessBalance, address: addr}) acct.Balance.Add(acct.Balance, amount) return prev } func (s *nativeStateDB) GetBalance(addr common.Address) *uint256.Int { + s.markRead(stateAccessKey{kind: stateAccessBalance, address: addr}) return s.account(addr).Balance.Clone() } func (s *nativeStateDB) SetBalance(addr common.Address, balance *uint256.Int, _ tracing.BalanceChangeReason) { acct := s.account(addr) + s.recordAccount(addr) + s.markWrite(stateAccessKey{kind: stateAccessBalance, address: addr}) if balance == nil { acct.Balance = uint256.NewInt(0) return @@ -182,11 +230,15 @@ func (s *nativeStateDB) SetBalance(addr common.Address, balance *uint256.Int, _ } func (s *nativeStateDB) GetNonce(addr common.Address) uint64 { + s.markRead(stateAccessKey{kind: stateAccessNonce, address: addr}) return s.account(addr).Nonce } func (s *nativeStateDB) SetNonce(addr common.Address, nonce uint64, _ tracing.NonceChangeReason) { - s.account(addr).Nonce = nonce + acct := s.account(addr) + s.recordAccount(addr) + s.markWrite(stateAccessKey{kind: stateAccessNonce, address: addr}) + acct.Nonce = nonce } func (s *nativeStateDB) GetCodeHash(addr common.Address) common.Hash { @@ -198,17 +250,21 @@ func (s *nativeStateDB) GetCodeHash(addr common.Address) common.Hash { } func (s *nativeStateDB) GetCode(addr common.Address) []byte { + s.markRead(stateAccessKey{kind: stateAccessCode, address: addr}) return cloneBytes(s.account(addr).Code) } func (s *nativeStateDB) SetCode(addr common.Address, code []byte) []byte { acct := s.account(addr) prev := cloneBytes(acct.Code) + s.recordAccount(addr) + s.markWrite(stateAccessKey{kind: stateAccessCode, address: addr}) acct.Code = cloneBytes(code) return prev } func (s *nativeStateDB) GetCodeSize(addr common.Address) int { + s.markRead(stateAccessKey{kind: stateAccessCode, address: addr}) return len(s.account(addr).Code) } @@ -228,11 +284,13 @@ func (s *nativeStateDB) GetRefund() uint64 { } func (s *nativeStateDB) GetCommittedState(addr common.Address, key common.Hash) common.Hash { + s.markRead(stateAccessKey{kind: stateAccessStorage, address: addr, slot: key}) s.ensureStorage(addr, key) return s.baseAccount(addr).Storage[key] } func (s *nativeStateDB) GetState(addr common.Address, key common.Hash) common.Hash { + s.markRead(stateAccessKey{kind: stateAccessStorage, address: addr, slot: key}) s.ensureStorage(addr, key) return s.account(addr).Storage[key] } @@ -241,6 +299,8 @@ func (s *nativeStateDB) SetState(addr common.Address, key common.Hash, value com s.ensureStorage(addr, key) acct := s.account(addr) prev := acct.Storage[key] + s.recordAccount(addr) + s.markWrite(stateAccessKey{kind: stateAccessStorage, address: addr, slot: key}) if value == (common.Hash{}) { delete(acct.Storage, key) } else { @@ -251,6 +311,8 @@ func (s *nativeStateDB) SetState(addr common.Address, key common.Hash, value com func (s *nativeStateDB) SetStorage(addr common.Address, states map[common.Hash]common.Hash) { acct := s.account(addr) + s.recordAccount(addr) + s.markWrite(stateAccessKey{kind: stateAccessAccount, address: addr}) acct.Storage = map[common.Hash]common.Hash{} for key, value := range states { if value != (common.Hash{}) { @@ -286,8 +348,11 @@ func (s *nativeStateDB) SetTransientState(addr common.Address, key, value common func (s *nativeStateDB) SelfDestruct(addr common.Address) uint256.Int { acct := s.account(addr) prev := *acct.Balance.Clone() + s.recordAccount(addr) + s.markWrite(stateAccessKey{kind: stateAccessAccount, address: addr}) acct.Balance.Clear() acct.SelfDestructed = true + s.markForFinalise(addr) return prev } @@ -304,11 +369,13 @@ func (s *nativeStateDB) HasSelfDestructed(addr common.Address) bool { } func (s *nativeStateDB) Exist(addr common.Address) bool { + s.markRead(stateAccessKey{kind: stateAccessAccount, address: addr}) acct := s.account(addr) return acct.SelfDestructed || acct.Nonce != 0 || !acct.Balance.IsZero() || len(acct.Code) != 0 } func (s *nativeStateDB) Empty(addr common.Address) bool { + s.markRead(stateAccessKey{kind: stateAccessAccount, address: addr}) acct := s.account(addr) return acct.Nonce == 0 && acct.Balance.IsZero() && len(acct.Code) == 0 } @@ -371,13 +438,14 @@ func (s *nativeStateDB) PointCache() *ethutils.PointCache { func (s *nativeStateDB) Snapshot() int { id := len(s.snapshots) s.snapshots = append(s.snapshots, nativeSnapshot{ - accounts: cloneAccounts(s.accounts), - base: cloneAccounts(s.base), + journalLen: len(s.journal), refund: s.refund, - logs: append([]*ethtypes.Log(nil), s.logs...), + logsLen: len(s.logs), accessList: cloneAccessList(s.accessList), transientStates: cloneTransientStates(s.transientStates), + finaliseAddrs: cloneAddressSet(s.finaliseAddrs), preimages: clonePreimages(s.preimages), + journaledAddrs: map[common.Address]struct{}{}, err: s.err, }) return id @@ -388,12 +456,15 @@ func (s *nativeStateDB) RevertToSnapshot(id int) { panic("invalid state snapshot") } snapshot := s.snapshots[id] - s.accounts = cloneAccounts(snapshot.accounts) - s.base = cloneAccounts(snapshot.base) + for i := len(s.journal) - 1; i >= snapshot.journalLen; i-- { + s.journal[i].revert(s) + } + s.journal = s.journal[:snapshot.journalLen] s.refund = snapshot.refund - s.logs = append([]*ethtypes.Log(nil), snapshot.logs...) + s.logs = s.logs[:snapshot.logsLen] s.accessList = cloneAccessList(snapshot.accessList) s.transientStates = cloneTransientStates(snapshot.transientStates) + s.finaliseAddrs = cloneAddressSet(snapshot.finaliseAddrs) s.preimages = clonePreimages(snapshot.preimages) s.err = snapshot.err s.snapshots = s.snapshots[:id] @@ -419,15 +490,21 @@ func (s *nativeStateDB) AccessEvents() *vm.AccessEvents { } func (s *nativeStateDB) Finalise(bool) { - for _, acct := range s.accounts { + for addr := range s.finaliseAddrs { + acct := s.account(addr) if acct.SelfDestructed { + s.recordAccount(addr) acct.Code = nil acct.Storage = map[common.Hash]common.Hash{} acct.Nonce = 0 acct.SelfDestructed = false } - acct.Created = false + if acct.Created { + s.recordAccount(addr) + acct.Created = false + } } + clear(s.finaliseAddrs) s.refund = 0 } @@ -460,7 +537,11 @@ func (s *nativeStateDB) Copy() vm.StateDB { preimages: clonePreimages(s.preimages), accessList: cloneAccessList(s.accessList), transientStates: cloneTransientStates(s.transientStates), + finaliseAddrs: cloneAddressSet(s.finaliseAddrs), + journal: cloneJournal(s.journal), snapshots: cloneSnapshots(s.snapshots), + readSet: cloneAccessSet(s.readSet), + writeSet: cloneAccessSet(s.writeSet), txHash: s.txHash, txIndex: s.txIndex, txIndexUint: s.txIndexUint, @@ -494,6 +575,61 @@ func (s *nativeStateDB) SetEVM(evm *vm.EVM) { s.evm = evm } +func (s *nativeStateDB) enableAccessTracking() { + s.readSet = map[stateAccessKey]struct{}{} + s.writeSet = map[stateAccessKey]struct{}{} +} + +func (s *nativeStateDB) accessSets() (map[stateAccessKey]struct{}, map[stateAccessKey]struct{}) { + return cloneAccessSet(s.readSet), cloneAccessSet(s.writeSet) +} + +func (s *nativeStateDB) markRead(key stateAccessKey) { + if s.readSet != nil { + s.readSet[key] = struct{}{} + } +} + +func (s *nativeStateDB) markWrite(key stateAccessKey) { + if s.writeSet != nil { + s.writeSet[key] = struct{}{} + } +} + +func (s *nativeStateDB) recordAccount(addr common.Address) { + if len(s.snapshots) == 0 { + return + } + snapshot := &s.snapshots[len(s.snapshots)-1] + if _, ok := snapshot.journaledAddrs[addr]; ok { + return + } + snapshot.journaledAddrs[addr] = struct{}{} + s.journal = append(s.journal, nativeJournalEntry{ + kind: nativeJournalAccount, + address: addr, + account: s.account(addr).clone(), + }) +} + +func (e nativeJournalEntry) revert(s *nativeStateDB) { + switch e.kind { + case nativeJournalAccount: + s.accounts[e.address] = e.account.clone() + default: + panic("unknown native state journal entry") + } +} + +func (s *nativeStateDB) markForFinalise(addr common.Address) { + s.finaliseAddrs[addr] = struct{}{} +} + +func (s *nativeStateDB) clearSnapshots() { + s.journal = s.journal[:0] + s.snapshots = s.snapshots[:0] +} + func (s *nativeStateDB) account(addr common.Address) *nativeAccount { if acct, ok := s.accounts[addr]; ok { return acct @@ -602,6 +738,25 @@ func cloneTransientStates(states map[common.Address]map[common.Hash]common.Hash) return cp } +func cloneAddressSet(addrs map[common.Address]struct{}) map[common.Address]struct{} { + cp := make(map[common.Address]struct{}, len(addrs)) + for addr := range addrs { + cp[addr] = struct{}{} + } + return cp +} + +func cloneAccessSet(set map[stateAccessKey]struct{}) map[stateAccessKey]struct{} { + if set == nil { + return nil + } + cp := make(map[stateAccessKey]struct{}, len(set)) + for key := range set { + cp[key] = struct{}{} + } + return cp +} + func clonePreimages(preimages map[common.Hash][]byte) map[common.Hash][]byte { cp := make(map[common.Hash][]byte, len(preimages)) for hash, preimage := range preimages { @@ -610,17 +765,30 @@ func clonePreimages(preimages map[common.Hash][]byte) map[common.Hash][]byte { return cp } +func cloneJournal(journal []nativeJournalEntry) []nativeJournalEntry { + cp := make([]nativeJournalEntry, len(journal)) + for i, entry := range journal { + cp[i] = nativeJournalEntry{ + kind: entry.kind, + address: entry.address, + account: entry.account.clone(), + } + } + return cp +} + func cloneSnapshots(snapshots []nativeSnapshot) []nativeSnapshot { cp := make([]nativeSnapshot, len(snapshots)) for i, snapshot := range snapshots { cp[i] = nativeSnapshot{ - accounts: cloneAccounts(snapshot.accounts), - base: cloneAccounts(snapshot.base), + journalLen: snapshot.journalLen, refund: snapshot.refund, - logs: append([]*ethtypes.Log(nil), snapshot.logs...), + logsLen: snapshot.logsLen, accessList: cloneAccessList(snapshot.accessList), transientStates: cloneTransientStates(snapshot.transientStates), + finaliseAddrs: cloneAddressSet(snapshot.finaliseAddrs), preimages: clonePreimages(snapshot.preimages), + journaledAddrs: cloneAddressSet(snapshot.journaledAddrs), err: snapshot.err, } } From f1fa418bffdea98376e05a061cc19f5b959eed2e Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Tue, 30 Jun 2026 14:10:29 +0800 Subject: [PATCH 13/27] optimize evmonly OCC scheduling --- giga/evmonly/occ.go | 55 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 42 insertions(+), 13 deletions(-) diff --git a/giga/evmonly/occ.go b/giga/evmonly/occ.go index 6edf0d9b87..f37e6b71b7 100644 --- a/giga/evmonly/occ.go +++ b/giga/evmonly/occ.go @@ -25,6 +25,11 @@ type occTxExecution struct { gasUsed uint64 } +type occTxRange struct { + start int + end int +} + func (e *Executor) executeBlockOCC(ctx context.Context, req BlockRequest) (*BlockResult, error) { chainConfig := e.chainConfig(req.Context) signer := ethtypes.MakeSigner(chainConfig, new(big.Int).SetUint64(req.Context.Number), req.Context.Time) @@ -36,18 +41,24 @@ func (e *Executor) executeBlockOCC(ctx context.Context, req BlockRequest) (*Bloc } workers := e.cfg.OCCWorkers - if workers > len(req.Txs) { - workers = len(req.Txs) + txCount := len(req.Txs) + if workers > txCount { + workers = txCount } - results := make([]occTxExecution, len(req.Txs)) - jobs := make(chan int) + results := make([]occTxExecution, txCount) + chunkSize := occChunkSize(txCount, workers) + jobs := make(chan occTxRange) group, groupCtx := errgroup.WithContext(ctx) group.Go(func() error { defer close(jobs) - for i := range req.Txs { + for start := 0; start < txCount; start += chunkSize { + end := start + chunkSize + if end > txCount { + end = txCount + } select { - case jobs <- i: + case jobs <- occTxRange{start: start, end: end}: case <-groupCtx.Done(): return groupCtx.Err() } @@ -60,15 +71,17 @@ func (e *Executor) executeBlockOCC(ctx context.Context, req BlockRequest) (*Bloc select { case <-groupCtx.Done(): return groupCtx.Err() - case idx, ok := <-jobs: + case txRange, ok := <-jobs: if !ok { return nil } - result, err := e.executeTxSpeculative(groupCtx, req, idx, signer, chainConfig, blockCtx, baseFee, gasLimit) - if err != nil { - return err + for idx := txRange.start; idx < txRange.end; idx++ { + result, err := e.executeTxSpeculative(groupCtx, req, idx, signer, chainConfig, blockCtx, baseFee, gasLimit) + if err != nil { + return err + } + results[idx] = result } - results[idx] = result } } }) @@ -82,6 +95,21 @@ func (e *Executor) executeBlockOCC(ctx context.Context, req BlockRequest) (*Bloc return mergeOCCResults(results), nil } +func occChunkSize(txCount int, workers int) int { + if txCount <= 0 || workers <= 0 { + return 1 + } + targetChunks := workers * 8 + chunkSize := (txCount + targetChunks - 1) / targetChunks + if chunkSize < 16 { + return 16 + } + if chunkSize > 256 { + return 256 + } + return chunkSize +} + func (e *Executor) executeTxSpeculative( ctx context.Context, req BlockRequest, @@ -96,6 +124,7 @@ func (e *Executor) executeTxSpeculative( if err != nil { return occTxExecution{}, fmt.Errorf("parse tx %d: %w", txIndex, err) } + p := parsedTx{tx: tx, sender: sender} stateDB := newNativeStateDB(e.state) stateDB.enableAccessTracking() evm := vm.NewEVM(blockCtx, stateDB, chainConfig, vm.Config{}, nil) @@ -106,14 +135,14 @@ func (e *Executor) executeTxSpeculative( stateDB, gasPool, req.Context, - parsedTx{tx: tx, sender: sender}, + p, txIndex, uint(txIndex), baseFee, signer, ) if err != nil { - return occTxExecution{}, fmt.Errorf("execute tx %d %s: %w", txIndex, tx.Hash(), err) + return occTxExecution{}, fmt.Errorf("execute tx %d %s: %w", txIndex, p.tx.Hash(), err) } readSet, writeSet := stateDB.accessSets() return occTxExecution{ From dd519f70d6fa217eff661ea7963cbf91639f93a2 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Thu, 2 Jul 2026 13:51:06 +0800 Subject: [PATCH 14/27] add evmonly executor result sink --- giga/evmonly/executor.go | 41 ++++++++++++++++++++++++----- giga/evmonly/executor_test.go | 49 +++++++++++++++++++++++++++++++++++ giga/evmonly/types.go | 6 +++++ 3 files changed, 90 insertions(+), 6 deletions(-) diff --git a/giga/evmonly/executor.go b/giga/evmonly/executor.go index 1d3599b8f8..8e8183fb42 100644 --- a/giga/evmonly/executor.go +++ b/giga/evmonly/executor.go @@ -18,8 +18,9 @@ import ( // Executor runs raw EVM transactions against an EVM-native state backend. type Executor struct { - cfg Config - state StateReader + cfg Config + state StateReader + resultSink ResultSink } type Option func(*Executor) @@ -32,6 +33,12 @@ func WithState(state StateReader) Option { } } +func WithResultSink(sink ResultSink) Option { + return func(e *Executor) { + e.resultSink = sink + } +} + func NewExecutor(cfg Config, opts ...Option) *Executor { e := &Executor{ cfg: cfg.WithDefaults(), @@ -48,13 +55,35 @@ func (e *Executor) Config() Config { } func (e *Executor) ExecuteBlock(ctx context.Context, req BlockRequest) (*BlockResult, error) { + var result *BlockResult + var err error if len(req.Txs) == 0 { - return &BlockResult{}, nil + result = &BlockResult{} + } else if e.useOCC(req) { + result, err = e.executeBlockOCC(ctx, req) + } else { + result, err = e.executeBlockSequential(ctx, req) + } + if err != nil { + return nil, err + } + if err := e.sinkBlockResult(ctx, req.Context.Number, result); err != nil { + return nil, err + } + return result, nil +} + +func (e *Executor) sinkBlockResult(ctx context.Context, height uint64, result *BlockResult) error { + if e.resultSink == nil || result == nil { + return nil + } + if err := e.resultSink.StoreChangeSet(ctx, height, result.ChangeSet); err != nil { + return fmt.Errorf("store changeset for block %d: %w", height, err) } - if e.useOCC(req) { - return e.executeBlockOCC(ctx, req) + if err := e.resultSink.StoreReceipts(ctx, height, result.Receipts); err != nil { + return fmt.Errorf("store receipts for block %d: %w", height, err) } - return e.executeBlockSequential(ctx, req) + return nil } func (e *Executor) useOCC(req BlockRequest) bool { diff --git a/giga/evmonly/executor_test.go b/giga/evmonly/executor_test.go index 04ef979cdf..998a4783f0 100644 --- a/giga/evmonly/executor_test.go +++ b/giga/evmonly/executor_test.go @@ -20,6 +20,25 @@ import ( const testGasPriceWei = 1_000_000_000 +type recordingResultSink struct { + changeSetHeights []uint64 + receiptHeights []uint64 + changeSets []StateChangeSet + receipts []ethtypes.Receipts +} + +func (s *recordingResultSink) StoreChangeSet(_ context.Context, height uint64, changeSet StateChangeSet) error { + s.changeSetHeights = append(s.changeSetHeights, height) + s.changeSets = append(s.changeSets, changeSet) + return nil +} + +func (s *recordingResultSink) StoreReceipts(_ context.Context, height uint64, receipts ethtypes.Receipts) error { + s.receiptHeights = append(s.receiptHeights, height) + s.receipts = append(s.receipts, receipts) + return nil +} + func TestExecutorEmptyBlock(t *testing.T) { executor := NewExecutor(Config{}) @@ -59,6 +78,36 @@ func TestExecutorTransferTx(t *testing.T) { require.Equal(t, uint64(1), state.GetNonce(sender)) } +func TestExecutorInvokesResultSink(t *testing.T) { + chainID := big.NewInt(713715) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + recipient := common.HexToAddress("0x00000000000000000000000000000000000000a7") + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(200_000_000_000_000)) + sink := &recordingResultSink{} + + rawTx := signLegacyTx(t, key, chainID, 0, &recipient, big.NewInt(7), nil) + executor := NewExecutor(Config{}, WithState(state), WithResultSink(sink)) + ctx := blockContext(chainID) + ctx.Number = 77 + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: ctx, + Txs: [][]byte{rawTx}, + }) + + require.NoError(t, err) + require.Len(t, sink.changeSets, 1) + require.Len(t, sink.receipts, 1) + require.Equal(t, []uint64{ctx.Number}, sink.changeSetHeights) + require.Equal(t, []uint64{ctx.Number}, sink.receiptHeights) + require.Equal(t, result.ChangeSet, sink.changeSets[0]) + require.Equal(t, result.Receipts, sink.receipts[0]) +} + func TestExecutorDynamicFeeTx(t *testing.T) { chainID := big.NewInt(713715) key, err := crypto.GenerateKey() diff --git a/giga/evmonly/types.go b/giga/evmonly/types.go index 708c10e91f..f081c71a44 100644 --- a/giga/evmonly/types.go +++ b/giga/evmonly/types.go @@ -13,6 +13,12 @@ type BlockExecutor interface { ExecuteBlock(context.Context, BlockRequest) (*BlockResult, error) } +// ResultSink persists executor-produced block outputs. +type ResultSink interface { + StoreChangeSet(ctx context.Context, height uint64, changeSet StateChangeSet) error + StoreReceipts(ctx context.Context, height uint64, receipts ethtypes.Receipts) error +} + // BlockRequest contains all consensus/runtime inputs needed to execute a block. // Txs must be raw Ethereum transaction RLP bytes. type BlockRequest struct { From da985fc25d02f81e5ccdaf18981a0dcf08c72021 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Thu, 2 Jul 2026 16:37:25 +0800 Subject: [PATCH 15/27] pipeline evmonly sender recovery --- giga/evmonly/README.md | 13 ++++++ giga/evmonly/executor.go | 90 +++++++++++++++++++++++++++++----------- giga/evmonly/occ.go | 17 +++----- giga/evmonly/parser.go | 11 ++--- giga/evmonly/types.go | 22 ++++++++++ 5 files changed, 108 insertions(+), 45 deletions(-) diff --git a/giga/evmonly/README.md b/giga/evmonly/README.md index 9c2f9a9a5d..b748f13247 100644 --- a/giga/evmonly/README.md +++ b/giga/evmonly/README.md @@ -45,6 +45,19 @@ The boundary is: ExecuteBlock(context.Context, BlockRequest) (*BlockResult, error) ``` +For callers that can pipeline stateless work across blocks, the concrete +executor also supports: + +```go +PrepareBlock(context.Context, BlockRequest) (PreparedBlock, error) +ExecutePreparedBlock(context.Context, PreparedBlock) (*BlockResult, error) +``` + +`PrepareBlock` decodes transaction RLP and recovers senders. This work does not +touch state, so block N+1 can be prepared while block N is still executing. +`ExecuteBlock` remains the convenience path and performs prepare then execute in +one call. + The executor should be commit-neutral. It executes an ordered EVM block and returns the state writes and receipts produced by that block. The caller owns durable persistence, state commitment, block indexing, and receipt publication. diff --git a/giga/evmonly/executor.go b/giga/evmonly/executor.go index 8e8183fb42..13d0204960 100644 --- a/giga/evmonly/executor.go +++ b/giga/evmonly/executor.go @@ -55,11 +55,32 @@ func (e *Executor) Config() Config { } func (e *Executor) ExecuteBlock(ctx context.Context, req BlockRequest) (*BlockResult, error) { + prepared, err := e.PrepareBlock(ctx, req) + if err != nil { + return nil, err + } + return e.ExecutePreparedBlock(ctx, prepared) +} + +func (e *Executor) PrepareBlock(ctx context.Context, req BlockRequest) (PreparedBlock, error) { + chainConfig := e.chainConfig(req.Context) + signer := ethtypes.MakeSigner(chainConfig, new(big.Int).SetUint64(req.Context.Number), req.Context.Time) + parsed, err := parseBlockTxs(ctx, req.Txs, signer) + if err != nil { + return PreparedBlock{}, err + } + return PreparedBlock{ + Context: req.Context, + Txs: parsed, + }, nil +} + +func (e *Executor) ExecutePreparedBlock(ctx context.Context, req PreparedBlock) (*BlockResult, error) { var result *BlockResult var err error if len(req.Txs) == 0 { result = &BlockResult{} - } else if e.useOCC(req) { + } else if e.useOCC(len(req.Txs)) { result, err = e.executeBlockOCC(ctx, req) } else { result, err = e.executeBlockSequential(ctx, req) @@ -86,8 +107,8 @@ func (e *Executor) sinkBlockResult(ctx context.Context, height uint64, result *B return nil } -func (e *Executor) useOCC(req BlockRequest) bool { - if e.cfg.OCCWorkers <= 1 || len(req.Txs) <= 1 { +func (e *Executor) useOCC(txCount int) bool { + if e.cfg.OCCWorkers <= 1 || txCount <= 1 { return false } if e.cfg.CustomPrecompiles == nil { @@ -96,13 +117,8 @@ func (e *Executor) useOCC(req BlockRequest) bool { return len(e.cfg.CustomPrecompiles.Addresses()) == 0 } -func (e *Executor) executeBlockSequential(ctx context.Context, req BlockRequest) (*BlockResult, error) { +func (e *Executor) executeBlockSequential(ctx context.Context, req PreparedBlock) (*BlockResult, error) { chainConfig := e.chainConfig(req.Context) - signer := ethtypes.MakeSigner(chainConfig, new(big.Int).SetUint64(req.Context.Number), req.Context.Time) - parsed, err := parseBlockTxs(ctx, req.Txs, signer) - if err != nil { - return nil, err - } stateDB := newNativeStateDB(e.state) blockCtx := buildBlockContext(req.Context) @@ -117,19 +133,19 @@ func (e *Executor) executeBlockSequential(ctx context.Context, req BlockRequest) baseFee := cloneBig(req.Context.BaseFee) result := &BlockResult{ - Txs: make([]TxResult, 0, len(parsed)), - Receipts: make(ethtypes.Receipts, 0, len(parsed)), + Txs: make([]TxResult, 0, len(req.Txs)), + Receipts: make(ethtypes.Receipts, 0, len(req.Txs)), } var txIndexUint uint - for txIndex, p := range parsed { + for txIndex, p := range req.Txs { select { case <-ctx.Done(): return nil, ctx.Err() default: } - txResult, receipt, err := e.executeTx(evm, stateDB, gasPool, req.Context, p, txIndex, txIndexUint, baseFee, signer) + txResult, receipt, err := e.executeTx(evm, stateDB, gasPool, req.Context, p, txIndex, txIndexUint, baseFee) if err != nil { - return nil, fmt.Errorf("execute tx %d %s: %w", txIndex, p.tx.Hash(), err) + return nil, fmt.Errorf("execute tx %d %s: %w", txIndex, p.Tx.Hash(), err) } txResult.CumulativeGasUsed = result.GasUsed + txResult.GasUsed receipt.CumulativeGasUsed = txResult.CumulativeGasUsed @@ -149,27 +165,23 @@ func (e *Executor) executeTx( stateDB *nativeStateDB, gasPool *core.GasPool, block BlockContext, - p parsedTx, + p PreparedTx, txIndex int, txIndexUint uint, baseFee *big.Int, - signer ethtypes.Signer, ) (TxResult, *ethtypes.Receipt, error) { - tx := p.tx + tx := p.Tx if !e.cfg.DisableGasPriceCheck && e.cfg.MinGasPrice != nil { // MinGasPrice is block-validity policy; unlike EVM call failures, it // does not produce a receipt for an otherwise invalid block. if effectiveGasPrice(tx, baseFee).Cmp(e.cfg.MinGasPrice) < 0 { - return TxResult{Hash: tx.Hash(), Sender: p.sender, To: tx.To(), Err: errInsufficientGasPrice}, + return TxResult{Hash: tx.Hash(), Sender: p.Sender, To: tx.To(), Err: errInsufficientGasPrice}, nil, errInsufficientGasPrice } } - msg, err := core.TransactionToMessage(tx, signer, baseFee) - if err != nil { - return TxResult{Hash: tx.Hash(), Sender: p.sender, To: tx.To(), Err: err}, nil, err - } + msg := transactionToPreparedMessage(p, baseFee) msg.SkipNonceChecks = e.cfg.DisableNonceCheck stateDB.setTxContext(tx.Hash(), txIndex, txIndexUint) @@ -177,7 +189,7 @@ func (e *Executor) executeTx( evm.SetTxContext(core.NewEVMTxContext(msg)) execResult, err := core.ApplyMessage(evm, msg, gasPool) if err != nil { - return TxResult{Hash: tx.Hash(), Sender: p.sender, To: tx.To(), Err: err}, nil, err + return TxResult{Hash: tx.Hash(), Sender: p.Sender, To: tx.To(), Err: err}, nil, err } stateDB.clearSnapshots() stateDB.Finalise(true) @@ -206,13 +218,13 @@ func (e *Executor) executeTx( TransactionIndex: txIndexUint, } if tx.To() == nil { - receipt.ContractAddress = crypto.CreateAddress(p.sender, tx.Nonce()) + receipt.ContractAddress = crypto.CreateAddress(p.Sender, tx.Nonce()) } receipt.Bloom = ethtypes.CreateBloom(receipt) txResult := TxResult{ Hash: tx.Hash(), - Sender: p.sender, + Sender: p.Sender, To: tx.To(), ContractAddress: receipt.ContractAddress, Status: status, @@ -224,6 +236,34 @@ func (e *Executor) executeTx( return txResult, receipt, nil } +func transactionToPreparedMessage(p PreparedTx, baseFee *big.Int) *core.Message { + tx := p.Tx + msg := &core.Message{ + From: p.Sender, + Nonce: tx.Nonce(), + GasLimit: tx.Gas(), + GasPrice: new(big.Int).Set(tx.GasPrice()), + GasFeeCap: new(big.Int).Set(tx.GasFeeCap()), + GasTipCap: new(big.Int).Set(tx.GasTipCap()), + To: tx.To(), + Value: tx.Value(), + Data: tx.Data(), + AccessList: tx.AccessList(), + SetCodeAuthorizations: tx.SetCodeAuthorizations(), + SkipNonceChecks: false, + SkipFromEOACheck: false, + BlobHashes: tx.BlobHashes(), + BlobGasFeeCap: tx.BlobGasFeeCap(), + } + if baseFee != nil { + msg.GasPrice = msg.GasPrice.Add(msg.GasTipCap, baseFee) + if msg.GasPrice.Cmp(msg.GasFeeCap) > 0 { + msg.GasPrice = msg.GasFeeCap + } + } + return msg +} + func buildBlockContext(ctx BlockContext) vm.BlockContext { prevRandao := ctx.PrevRandao baseFee := cloneBig(ctx.BaseFee) diff --git a/giga/evmonly/occ.go b/giga/evmonly/occ.go index f37e6b71b7..4578cf6c4f 100644 --- a/giga/evmonly/occ.go +++ b/giga/evmonly/occ.go @@ -30,9 +30,8 @@ type occTxRange struct { end int } -func (e *Executor) executeBlockOCC(ctx context.Context, req BlockRequest) (*BlockResult, error) { +func (e *Executor) executeBlockOCC(ctx context.Context, req PreparedBlock) (*BlockResult, error) { chainConfig := e.chainConfig(req.Context) - signer := ethtypes.MakeSigner(chainConfig, new(big.Int).SetUint64(req.Context.Number), req.Context.Time) blockCtx := buildBlockContext(req.Context) baseFee := cloneBig(req.Context.BaseFee) gasLimit := req.Context.GasLimit @@ -76,7 +75,7 @@ func (e *Executor) executeBlockOCC(ctx context.Context, req BlockRequest) (*Bloc return nil } for idx := txRange.start; idx < txRange.end; idx++ { - result, err := e.executeTxSpeculative(groupCtx, req, idx, signer, chainConfig, blockCtx, baseFee, gasLimit) + result, err := e.executeTxSpeculative(groupCtx, req, idx, chainConfig, blockCtx, baseFee, gasLimit) if err != nil { return err } @@ -112,19 +111,14 @@ func occChunkSize(txCount int, workers int) int { func (e *Executor) executeTxSpeculative( ctx context.Context, - req BlockRequest, + req PreparedBlock, txIndex int, - signer ethtypes.Signer, chainConfig *params.ChainConfig, blockCtx vm.BlockContext, baseFee *big.Int, gasLimit uint64, ) (occTxExecution, error) { - tx, sender, err := parseTx(req.Txs[txIndex], signer) - if err != nil { - return occTxExecution{}, fmt.Errorf("parse tx %d: %w", txIndex, err) - } - p := parsedTx{tx: tx, sender: sender} + p := req.Txs[txIndex] stateDB := newNativeStateDB(e.state) stateDB.enableAccessTracking() evm := vm.NewEVM(blockCtx, stateDB, chainConfig, vm.Config{}, nil) @@ -139,10 +133,9 @@ func (e *Executor) executeTxSpeculative( txIndex, uint(txIndex), baseFee, - signer, ) if err != nil { - return occTxExecution{}, fmt.Errorf("execute tx %d %s: %w", txIndex, p.tx.Hash(), err) + return occTxExecution{}, fmt.Errorf("execute tx %d %s: %w", txIndex, p.Tx.Hash(), err) } readSet, writeSet := stateDB.accessSets() return occTxExecution{ diff --git a/giga/evmonly/parser.go b/giga/evmonly/parser.go index d5a47fffb6..99b366fc61 100644 --- a/giga/evmonly/parser.go +++ b/giga/evmonly/parser.go @@ -8,13 +8,8 @@ import ( ethtypes "github.com/ethereum/go-ethereum/core/types" ) -type parsedTx struct { - tx *ethtypes.Transaction - sender common.Address -} - -func parseBlockTxs(ctx context.Context, txs [][]byte, signer ethtypes.Signer) ([]parsedTx, error) { - parsed := make([]parsedTx, len(txs)) +func parseBlockTxs(ctx context.Context, txs [][]byte, signer ethtypes.Signer) ([]PreparedTx, error) { + parsed := make([]PreparedTx, len(txs)) for i, raw := range txs { select { case <-ctx.Done(): @@ -25,7 +20,7 @@ func parseBlockTxs(ctx context.Context, txs [][]byte, signer ethtypes.Signer) ([ if err != nil { return nil, fmt.Errorf("parse tx %d: %w", i, err) } - parsed[i] = parsedTx{tx: tx, sender: sender} + parsed[i] = PreparedTx{Tx: tx, Sender: sender} } return parsed, nil } diff --git a/giga/evmonly/types.go b/giga/evmonly/types.go index f081c71a44..639d3f1694 100644 --- a/giga/evmonly/types.go +++ b/giga/evmonly/types.go @@ -13,6 +13,15 @@ type BlockExecutor interface { ExecuteBlock(context.Context, BlockRequest) (*BlockResult, error) } +// PreparedBlockExecutor exposes the split transaction preparation and block +// execution phases. Preparation is stateless, so callers may pipeline it ahead +// of ordered block execution. +type PreparedBlockExecutor interface { + BlockExecutor + PrepareBlock(context.Context, BlockRequest) (PreparedBlock, error) + ExecutePreparedBlock(context.Context, PreparedBlock) (*BlockResult, error) +} + // ResultSink persists executor-produced block outputs. type ResultSink interface { StoreChangeSet(ctx context.Context, height uint64, changeSet StateChangeSet) error @@ -26,6 +35,19 @@ type BlockRequest struct { Txs [][]byte } +// PreparedBlock contains decoded transactions with recovered senders. The +// executor treats prepared transactions as immutable. +type PreparedBlock struct { + Context BlockContext + Txs []PreparedTx +} + +// PreparedTx is the stateless per-transaction work needed before EVM execution. +type PreparedTx struct { + Tx *ethtypes.Transaction + Sender common.Address +} + // BlockContext contains block-constant EVM execution data. type BlockContext struct { Number uint64 From 06e18a932c3871f12827d173f465df56567160f8 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Thu, 2 Jul 2026 17:50:56 +0800 Subject: [PATCH 16/27] reuse evmonly OCC workers --- giga/evmonly/affinity_linux.go | 32 +++++++ giga/evmonly/affinity_other.go | 9 ++ giga/evmonly/config.go | 2 + giga/evmonly/executor.go | 15 ++++ giga/evmonly/occ.go | 65 ++++++--------- giga/evmonly/occ_pool.go | 147 +++++++++++++++++++++++++++++++++ 6 files changed, 232 insertions(+), 38 deletions(-) create mode 100644 giga/evmonly/affinity_linux.go create mode 100644 giga/evmonly/affinity_other.go create mode 100644 giga/evmonly/occ_pool.go diff --git a/giga/evmonly/affinity_linux.go b/giga/evmonly/affinity_linux.go new file mode 100644 index 0000000000..d0753dfa3b --- /dev/null +++ b/giga/evmonly/affinity_linux.go @@ -0,0 +1,32 @@ +//go:build linux + +package evmonly + +import ( + "fmt" + "runtime" + + "golang.org/x/sys/unix" +) + +func pinCurrentWorkerThread(cpu int) (func(), error) { + runtime.LockOSThread() + unlock := runtime.UnlockOSThread + numCPU := runtime.NumCPU() + if numCPU <= 0 { + unlock() + return func() {}, fmt.Errorf("runtime reported no CPUs") + } + if cpu < 0 { + unlock() + return func() {}, fmt.Errorf("CPU index must be non-negative") + } + cpu %= numCPU + var set unix.CPUSet + set.Set(cpu) + if err := unix.SchedSetaffinity(0, &set); err != nil { + unlock() + return func() {}, err + } + return unlock, nil +} diff --git a/giga/evmonly/affinity_other.go b/giga/evmonly/affinity_other.go new file mode 100644 index 0000000000..88c140fe26 --- /dev/null +++ b/giga/evmonly/affinity_other.go @@ -0,0 +1,9 @@ +//go:build !linux + +package evmonly + +import "fmt" + +func pinCurrentWorkerThread(int) (func(), error) { + return func() {}, fmt.Errorf("worker CPU pinning is only supported on linux") +} diff --git a/giga/evmonly/config.go b/giga/evmonly/config.go index e376dfa2d3..0f5ebc093e 100644 --- a/giga/evmonly/config.go +++ b/giga/evmonly/config.go @@ -16,6 +16,8 @@ type Config struct { ChainConfig *params.ChainConfig CustomPrecompiles precompiles.Registry OCCWorkers int + PinOCCWorkers bool + OCCWorkerCPUOffset int } func DefaultConfig() Config { diff --git a/giga/evmonly/executor.go b/giga/evmonly/executor.go index 13d0204960..15367ea63e 100644 --- a/giga/evmonly/executor.go +++ b/giga/evmonly/executor.go @@ -5,6 +5,7 @@ import ( "fmt" "math" "math/big" + "runtime" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" @@ -21,6 +22,7 @@ type Executor struct { cfg Config state StateReader resultSink ResultSink + occPool *occWorkerPool } type Option func(*Executor) @@ -44,12 +46,25 @@ func NewExecutor(cfg Config, opts ...Option) *Executor { cfg: cfg.WithDefaults(), state: NewMemoryState(), } + if e.cfg.OCCWorkers > 1 { + e.occPool = newOCCWorkerPool(e.cfg.OCCWorkers, e.cfg.PinOCCWorkers, e.cfg.OCCWorkerCPUOffset) + runtime.SetFinalizer(e, (*Executor).Close) + } for _, opt := range opts { opt(e) } return e } +func (e *Executor) Close() { + if e == nil || e.occPool == nil { + return + } + runtime.SetFinalizer(e, nil) + e.occPool.Close() + e.occPool = nil +} + func (e *Executor) Config() Config { return e.cfg } diff --git a/giga/evmonly/occ.go b/giga/evmonly/occ.go index 4578cf6c4f..d212fe0f55 100644 --- a/giga/evmonly/occ.go +++ b/giga/evmonly/occ.go @@ -13,7 +13,6 @@ import ( ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/params" - "golang.org/x/sync/errgroup" ) type occTxExecution struct { @@ -46,46 +45,21 @@ func (e *Executor) executeBlockOCC(ctx context.Context, req PreparedBlock) (*Blo } results := make([]occTxExecution, txCount) chunkSize := occChunkSize(txCount, workers) - jobs := make(chan occTxRange) - group, groupCtx := errgroup.WithContext(ctx) - - group.Go(func() error { - defer close(jobs) - for start := 0; start < txCount; start += chunkSize { - end := start + chunkSize - if end > txCount { - end = txCount - } - select { - case jobs <- occTxRange{start: start, end: end}: - case <-groupCtx.Done(): - return groupCtx.Err() + pool := e.occPool + if pool == nil { + pool = newOCCWorkerPool(workers, e.cfg.PinOCCWorkers, e.cfg.OCCWorkerCPUOffset) + defer pool.Close() + } + if err := pool.Run(ctx, occRanges(txCount, chunkSize), func(workerCtx context.Context, txRange occTxRange) error { + for idx := txRange.start; idx < txRange.end; idx++ { + result, err := e.executeTxSpeculative(workerCtx, req, idx, chainConfig, blockCtx, baseFee, gasLimit) + if err != nil { + return err } + results[idx] = result } return nil - }) - for range workers { - group.Go(func() error { - for { - select { - case <-groupCtx.Done(): - return groupCtx.Err() - case txRange, ok := <-jobs: - if !ok { - return nil - } - for idx := txRange.start; idx < txRange.end; idx++ { - result, err := e.executeTxSpeculative(groupCtx, req, idx, chainConfig, blockCtx, baseFee, gasLimit) - if err != nil { - return err - } - results[idx] = result - } - } - } - }) - } - if err := group.Wait(); err != nil { + }); err != nil { return nil, err } if !validateOCCResults(results, gasLimit) { @@ -94,6 +68,21 @@ func (e *Executor) executeBlockOCC(ctx context.Context, req PreparedBlock) (*Blo return mergeOCCResults(results), nil } +func occRanges(txCount int, chunkSize int) []occTxRange { + if chunkSize <= 0 { + chunkSize = 1 + } + ranges := make([]occTxRange, 0, (txCount+chunkSize-1)/chunkSize) + for start := 0; start < txCount; start += chunkSize { + end := start + chunkSize + if end > txCount { + end = txCount + } + ranges = append(ranges, occTxRange{start: start, end: end}) + } + return ranges +} + func occChunkSize(txCount int, workers int) int { if txCount <= 0 || workers <= 0 { return 1 diff --git a/giga/evmonly/occ_pool.go b/giga/evmonly/occ_pool.go new file mode 100644 index 0000000000..54ed448c1f --- /dev/null +++ b/giga/evmonly/occ_pool.go @@ -0,0 +1,147 @@ +package evmonly + +import ( + "context" + "fmt" + "sync" +) + +type occWorkerPool struct { + jobs chan occPoolJob + stop chan struct{} + closed chan struct{} + once sync.Once + + pinErrMu sync.Mutex + pinErr error +} + +type occPoolJob struct { + ctx context.Context + txRange occTxRange + run func(context.Context, occTxRange) error + + done *sync.WaitGroup + cancel context.CancelFunc + errOnce *sync.Once + err *error +} + +func newOCCWorkerPool(workers int, pinWorkers bool, cpuOffset int) *occWorkerPool { + p := &occWorkerPool{ + jobs: make(chan occPoolJob, workers*2), + stop: make(chan struct{}), + closed: make(chan struct{}), + } + var workerWG sync.WaitGroup + workerWG.Add(workers) + for workerID := 0; workerID < workers; workerID++ { + workerID := workerID + go func() { + defer workerWG.Done() + p.runWorker(workerID, pinWorkers, cpuOffset) + }() + } + go func() { + workerWG.Wait() + close(p.closed) + }() + return p +} + +func (p *occWorkerPool) runWorker(workerID int, pinWorkers bool, cpuOffset int) { + if pinWorkers { + unlock, err := pinCurrentWorkerThread(cpuOffset + workerID) + if err != nil { + p.setPinErr(fmt.Errorf("pin OCC worker %d: %w", workerID, err)) + } + defer unlock() + } + for { + select { + case <-p.stop: + return + case job := <-p.jobs: + p.runJob(job) + } + } +} + +func (p *occWorkerPool) runJob(job occPoolJob) { + defer job.done.Done() + if err := job.ctx.Err(); err != nil { + return + } + if err := job.run(job.ctx, job.txRange); err != nil { + job.errOnce.Do(func() { + *job.err = err + job.cancel() + }) + } +} + +func (p *occWorkerPool) Run(ctx context.Context, ranges []occTxRange, run func(context.Context, occTxRange) error) error { + if err := p.getPinErr(); err != nil { + return err + } + jobCtx, cancel := context.WithCancel(ctx) + defer cancel() + + var done sync.WaitGroup + var err error + var errOnce sync.Once +dispatch: + for _, txRange := range ranges { + done.Add(1) + job := occPoolJob{ + ctx: jobCtx, + txRange: txRange, + run: run, + done: &done, + cancel: cancel, + errOnce: &errOnce, + err: &err, + } + select { + case p.jobs <- job: + case <-jobCtx.Done(): + done.Done() + break dispatch + case <-p.stop: + done.Done() + return fmt.Errorf("OCC worker pool is closed") + } + } + done.Wait() + if err != nil { + return err + } + if ctxErr := ctx.Err(); ctxErr != nil { + return ctxErr + } + return nil +} + +func (p *occWorkerPool) Close() { + if p == nil { + return + } + p.once.Do(func() { + close(p.stop) + <-p.closed + }) +} + +func (p *occWorkerPool) setPinErr(err error) { + p.pinErrMu.Lock() + defer p.pinErrMu.Unlock() + if p.pinErr == nil { + p.pinErr = err + } +} + +func (p *occWorkerPool) getPinErr() error { + p.pinErrMu.Lock() + defer p.pinErrMu.Unlock() + return p.pinErr +} From 12e1d83b829be54f35eecb78dddb523fec3580b7 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Thu, 2 Jul 2026 20:35:07 +0800 Subject: [PATCH 17/27] remove evmonly worker pinning --- giga/evmonly/affinity_linux.go | 32 ------------------------------ giga/evmonly/affinity_other.go | 9 --------- giga/evmonly/config.go | 2 -- giga/evmonly/executor.go | 2 +- giga/evmonly/occ.go | 2 +- giga/evmonly/occ_pool.go | 36 ++++------------------------------ 6 files changed, 6 insertions(+), 77 deletions(-) delete mode 100644 giga/evmonly/affinity_linux.go delete mode 100644 giga/evmonly/affinity_other.go diff --git a/giga/evmonly/affinity_linux.go b/giga/evmonly/affinity_linux.go deleted file mode 100644 index d0753dfa3b..0000000000 --- a/giga/evmonly/affinity_linux.go +++ /dev/null @@ -1,32 +0,0 @@ -//go:build linux - -package evmonly - -import ( - "fmt" - "runtime" - - "golang.org/x/sys/unix" -) - -func pinCurrentWorkerThread(cpu int) (func(), error) { - runtime.LockOSThread() - unlock := runtime.UnlockOSThread - numCPU := runtime.NumCPU() - if numCPU <= 0 { - unlock() - return func() {}, fmt.Errorf("runtime reported no CPUs") - } - if cpu < 0 { - unlock() - return func() {}, fmt.Errorf("CPU index must be non-negative") - } - cpu %= numCPU - var set unix.CPUSet - set.Set(cpu) - if err := unix.SchedSetaffinity(0, &set); err != nil { - unlock() - return func() {}, err - } - return unlock, nil -} diff --git a/giga/evmonly/affinity_other.go b/giga/evmonly/affinity_other.go deleted file mode 100644 index 88c140fe26..0000000000 --- a/giga/evmonly/affinity_other.go +++ /dev/null @@ -1,9 +0,0 @@ -//go:build !linux - -package evmonly - -import "fmt" - -func pinCurrentWorkerThread(int) (func(), error) { - return func() {}, fmt.Errorf("worker CPU pinning is only supported on linux") -} diff --git a/giga/evmonly/config.go b/giga/evmonly/config.go index 0f5ebc093e..e376dfa2d3 100644 --- a/giga/evmonly/config.go +++ b/giga/evmonly/config.go @@ -16,8 +16,6 @@ type Config struct { ChainConfig *params.ChainConfig CustomPrecompiles precompiles.Registry OCCWorkers int - PinOCCWorkers bool - OCCWorkerCPUOffset int } func DefaultConfig() Config { diff --git a/giga/evmonly/executor.go b/giga/evmonly/executor.go index 15367ea63e..3455c8eac6 100644 --- a/giga/evmonly/executor.go +++ b/giga/evmonly/executor.go @@ -47,7 +47,7 @@ func NewExecutor(cfg Config, opts ...Option) *Executor { state: NewMemoryState(), } if e.cfg.OCCWorkers > 1 { - e.occPool = newOCCWorkerPool(e.cfg.OCCWorkers, e.cfg.PinOCCWorkers, e.cfg.OCCWorkerCPUOffset) + e.occPool = newOCCWorkerPool(e.cfg.OCCWorkers) runtime.SetFinalizer(e, (*Executor).Close) } for _, opt := range opts { diff --git a/giga/evmonly/occ.go b/giga/evmonly/occ.go index d212fe0f55..533adfc494 100644 --- a/giga/evmonly/occ.go +++ b/giga/evmonly/occ.go @@ -47,7 +47,7 @@ func (e *Executor) executeBlockOCC(ctx context.Context, req PreparedBlock) (*Blo chunkSize := occChunkSize(txCount, workers) pool := e.occPool if pool == nil { - pool = newOCCWorkerPool(workers, e.cfg.PinOCCWorkers, e.cfg.OCCWorkerCPUOffset) + pool = newOCCWorkerPool(workers) defer pool.Close() } if err := pool.Run(ctx, occRanges(txCount, chunkSize), func(workerCtx context.Context, txRange occTxRange) error { diff --git a/giga/evmonly/occ_pool.go b/giga/evmonly/occ_pool.go index 54ed448c1f..0678968a96 100644 --- a/giga/evmonly/occ_pool.go +++ b/giga/evmonly/occ_pool.go @@ -11,9 +11,6 @@ type occWorkerPool struct { stop chan struct{} closed chan struct{} once sync.Once - - pinErrMu sync.Mutex - pinErr error } type occPoolJob struct { @@ -27,7 +24,7 @@ type occPoolJob struct { err *error } -func newOCCWorkerPool(workers int, pinWorkers bool, cpuOffset int) *occWorkerPool { +func newOCCWorkerPool(workers int) *occWorkerPool { p := &occWorkerPool{ jobs: make(chan occPoolJob, workers*2), stop: make(chan struct{}), @@ -35,11 +32,10 @@ func newOCCWorkerPool(workers int, pinWorkers bool, cpuOffset int) *occWorkerPoo } var workerWG sync.WaitGroup workerWG.Add(workers) - for workerID := 0; workerID < workers; workerID++ { - workerID := workerID + for range workers { go func() { defer workerWG.Done() - p.runWorker(workerID, pinWorkers, cpuOffset) + p.runWorker() }() } go func() { @@ -49,14 +45,7 @@ func newOCCWorkerPool(workers int, pinWorkers bool, cpuOffset int) *occWorkerPoo return p } -func (p *occWorkerPool) runWorker(workerID int, pinWorkers bool, cpuOffset int) { - if pinWorkers { - unlock, err := pinCurrentWorkerThread(cpuOffset + workerID) - if err != nil { - p.setPinErr(fmt.Errorf("pin OCC worker %d: %w", workerID, err)) - } - defer unlock() - } +func (p *occWorkerPool) runWorker() { for { select { case <-p.stop: @@ -81,9 +70,6 @@ func (p *occWorkerPool) runJob(job occPoolJob) { } func (p *occWorkerPool) Run(ctx context.Context, ranges []occTxRange, run func(context.Context, occTxRange) error) error { - if err := p.getPinErr(); err != nil { - return err - } jobCtx, cancel := context.WithCancel(ctx) defer cancel() @@ -131,17 +117,3 @@ func (p *occWorkerPool) Close() { <-p.closed }) } - -func (p *occWorkerPool) setPinErr(err error) { - p.pinErrMu.Lock() - defer p.pinErrMu.Unlock() - if p.pinErr == nil { - p.pinErr = err - } -} - -func (p *occWorkerPool) getPinErr() error { - p.pinErrMu.Lock() - defer p.pinErrMu.Unlock() - return p.pinErr -} From 1bccbe46ca2aa69951883b5c62c9f3789841cec6 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Thu, 2 Jul 2026 20:55:29 +0800 Subject: [PATCH 18/27] pool evmonly execution outputs --- giga/evmonly/config.go | 5 ++ giga/evmonly/executor.go | 49 ++++++++++++--- giga/evmonly/executor_test.go | 47 ++++++++++++++ giga/evmonly/occ.go | 51 +++++++++++---- giga/evmonly/occ_pool.go | 11 ++-- giga/evmonly/result_pool.go | 115 ++++++++++++++++++++++++++++++++++ giga/evmonly/state_db.go | 87 +++++++++++++++++++++++-- giga/evmonly/types.go | 22 +++++++ 8 files changed, 356 insertions(+), 31 deletions(-) create mode 100644 giga/evmonly/result_pool.go diff --git a/giga/evmonly/config.go b/giga/evmonly/config.go index e376dfa2d3..c79dbef842 100644 --- a/giga/evmonly/config.go +++ b/giga/evmonly/config.go @@ -16,6 +16,11 @@ type Config struct { ChainConfig *params.ChainConfig CustomPrecompiles precompiles.Registry OCCWorkers int + // BlockResultPoolSize enables a bounded reusable output pool. Callers that + // enable it must call BlockResult.Release when they are done with returned + // results. Async sinks should implement BlockResultSink so the executor can + // retain results until the sink releases them. + BlockResultPoolSize int } func DefaultConfig() Config { diff --git a/giga/evmonly/executor.go b/giga/evmonly/executor.go index 3455c8eac6..92bfd05561 100644 --- a/giga/evmonly/executor.go +++ b/giga/evmonly/executor.go @@ -23,6 +23,7 @@ type Executor struct { state StateReader resultSink ResultSink occPool *occWorkerPool + resultPool *blockResultPool } type Option func(*Executor) @@ -43,8 +44,9 @@ func WithResultSink(sink ResultSink) Option { func NewExecutor(cfg Config, opts ...Option) *Executor { e := &Executor{ - cfg: cfg.WithDefaults(), - state: NewMemoryState(), + cfg: cfg.WithDefaults(), + state: NewMemoryState(), + resultPool: newBlockResultPool(cfg.BlockResultPoolSize), } if e.cfg.OCCWorkers > 1 { e.occPool = newOCCWorkerPool(e.cfg.OCCWorkers) @@ -94,7 +96,7 @@ func (e *Executor) ExecutePreparedBlock(ctx context.Context, req PreparedBlock) var result *BlockResult var err error if len(req.Txs) == 0 { - result = &BlockResult{} + result, err = e.acquireBlockResult(ctx, 0) } else if e.useOCC(len(req.Txs)) { result, err = e.executeBlockOCC(ctx, req) } else { @@ -104,15 +106,41 @@ func (e *Executor) ExecutePreparedBlock(ctx context.Context, req PreparedBlock) return nil, err } if err := e.sinkBlockResult(ctx, req.Context.Number, result); err != nil { + result.Release() return nil, err } return result, nil } +func (e *Executor) acquireBlockResult(ctx context.Context, txCapacity int) (*BlockResult, error) { + if e.resultPool == nil || !e.canPoolBlockResults() { + result := &BlockResult{} + result.prepareForBlock(txCapacity) + return result, nil + } + return e.resultPool.acquire(ctx, txCapacity) +} + +func (e *Executor) canPoolBlockResults() bool { + if e.resultSink == nil { + return true + } + _, ok := e.resultSink.(BlockResultSink) + return ok +} + func (e *Executor) sinkBlockResult(ctx context.Context, height uint64, result *BlockResult) error { if e.resultSink == nil || result == nil { return nil } + if sink, ok := e.resultSink.(BlockResultSink); ok { + release := result.retain() + if err := sink.StoreBlockResult(ctx, height, result, release); err != nil { + release() + return fmt.Errorf("store block result for block %d: %w", height, err) + } + return nil + } if err := e.resultSink.StoreChangeSet(ctx, height, result.ChangeSet); err != nil { return fmt.Errorf("store changeset for block %d: %w", height, err) } @@ -147,10 +175,16 @@ func (e *Executor) executeBlockSequential(ctx context.Context, req PreparedBlock gasPool := new(core.GasPool).AddGas(gasLimit) baseFee := cloneBig(req.Context.BaseFee) - result := &BlockResult{ - Txs: make([]TxResult, 0, len(req.Txs)), - Receipts: make(ethtypes.Receipts, 0, len(req.Txs)), + result, err := e.acquireBlockResult(ctx, len(req.Txs)) + if err != nil { + return nil, err } + ok := false + defer func() { + if !ok { + result.Release() + } + }() var txIndexUint uint for txIndex, p := range req.Txs { select { @@ -171,7 +205,8 @@ func (e *Executor) executeBlockSequential(ctx context.Context, req PreparedBlock } stateDB.clearSnapshots() stateDB.Finalise(true) - result.ChangeSet = stateDB.ChangeSet() + stateDB.ChangeSetInto(&result.ChangeSet) + ok = true return result, nil } diff --git a/giga/evmonly/executor_test.go b/giga/evmonly/executor_test.go index 998a4783f0..911d184b41 100644 --- a/giga/evmonly/executor_test.go +++ b/giga/evmonly/executor_test.go @@ -39,6 +39,25 @@ func (s *recordingResultSink) StoreReceipts(_ context.Context, height uint64, re return nil } +type recordingBlockResultSink struct { + result *BlockResult + release func() +} + +func (s *recordingBlockResultSink) StoreChangeSet(context.Context, uint64, StateChangeSet) error { + return nil +} + +func (s *recordingBlockResultSink) StoreReceipts(context.Context, uint64, ethtypes.Receipts) error { + return nil +} + +func (s *recordingBlockResultSink) StoreBlockResult(_ context.Context, _ uint64, result *BlockResult, release func()) error { + s.result = result + s.release = release + return nil +} + func TestExecutorEmptyBlock(t *testing.T) { executor := NewExecutor(Config{}) @@ -108,6 +127,34 @@ func TestExecutorInvokesResultSink(t *testing.T) { require.Equal(t, result.Receipts, sink.receipts[0]) } +func TestExecutorPooledResultRelease(t *testing.T) { + chainID := big.NewInt(713715) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + recipient := common.HexToAddress("0x00000000000000000000000000000000000000a8") + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(200_000_000_000_000)) + sink := &recordingBlockResultSink{} + executor := NewExecutor(Config{BlockResultPoolSize: 1}, WithState(state), WithResultSink(sink)) + rawTx := signLegacyTx(t, key, chainID, 0, &recipient, big.NewInt(7), nil) + req := BlockRequest{Context: blockContext(chainID), Txs: [][]byte{rawTx}} + + first, err := executor.ExecuteBlock(context.Background(), req) + require.NoError(t, err) + require.Same(t, first, sink.result) + require.NotNil(t, sink.release) + sink.release() + first.Release() + + second, err := executor.ExecuteBlock(context.Background(), req) + require.NoError(t, err) + require.Same(t, first, second) + sink.release() + second.Release() +} + func TestExecutorDynamicFeeTx(t *testing.T) { chainID := big.NewInt(713715) key, err := crypto.GenerateKey() diff --git a/giga/evmonly/occ.go b/giga/evmonly/occ.go index 533adfc494..c9977a0628 100644 --- a/giga/evmonly/occ.go +++ b/giga/evmonly/occ.go @@ -29,6 +29,19 @@ type occTxRange struct { end int } +type occWorkerScratch struct { + stateDB *nativeStateDB +} + +func (s *occWorkerScratch) resetStateDB(source StateReader) *nativeStateDB { + if s.stateDB == nil { + s.stateDB = newNativeStateDB(source) + } else { + s.stateDB.reset(source) + } + return s.stateDB +} + func (e *Executor) executeBlockOCC(ctx context.Context, req PreparedBlock) (*BlockResult, error) { chainConfig := e.chainConfig(req.Context) blockCtx := buildBlockContext(req.Context) @@ -50,9 +63,9 @@ func (e *Executor) executeBlockOCC(ctx context.Context, req PreparedBlock) (*Blo pool = newOCCWorkerPool(workers) defer pool.Close() } - if err := pool.Run(ctx, occRanges(txCount, chunkSize), func(workerCtx context.Context, txRange occTxRange) error { + if err := pool.Run(ctx, occRanges(txCount, chunkSize), func(workerCtx context.Context, txRange occTxRange, scratch *occWorkerScratch) error { for idx := txRange.start; idx < txRange.end; idx++ { - result, err := e.executeTxSpeculative(workerCtx, req, idx, chainConfig, blockCtx, baseFee, gasLimit) + result, err := e.executeTxSpeculative(workerCtx, scratch, req, idx, chainConfig, blockCtx, baseFee, gasLimit) if err != nil { return err } @@ -65,7 +78,7 @@ func (e *Executor) executeBlockOCC(ctx context.Context, req PreparedBlock) (*Blo if !validateOCCResults(results, gasLimit) { return e.executeBlockSequential(ctx, req) } - return mergeOCCResults(results), nil + return e.mergeOCCResults(ctx, results) } func occRanges(txCount int, chunkSize int) []occTxRange { @@ -100,6 +113,7 @@ func occChunkSize(txCount int, workers int) int { func (e *Executor) executeTxSpeculative( ctx context.Context, + scratch *occWorkerScratch, req PreparedBlock, txIndex int, chainConfig *params.ChainConfig, @@ -107,8 +121,11 @@ func (e *Executor) executeTxSpeculative( baseFee *big.Int, gasLimit uint64, ) (occTxExecution, error) { + if scratch == nil { + scratch = &occWorkerScratch{} + } p := req.Txs[txIndex] - stateDB := newNativeStateDB(e.state) + stateDB := scratch.resetStateDB(e.state) stateDB.enableAccessTracking() evm := vm.NewEVM(blockCtx, stateDB, chainConfig, vm.Config{}, nil) stateDB.SetEVM(evm) @@ -127,10 +144,12 @@ func (e *Executor) executeTxSpeculative( return occTxExecution{}, fmt.Errorf("execute tx %d %s: %w", txIndex, p.Tx.Hash(), err) } readSet, writeSet := stateDB.accessSets() + var changeSet StateChangeSet + stateDB.ChangeSetInto(&changeSet) return occTxExecution{ txResult: txResult, receipt: receipt, - changeSet: stateDB.ChangeSet(), + changeSet: changeSet, readSet: readSet, writeSet: writeSet, gasUsed: txResult.GasUsed, @@ -156,11 +175,12 @@ func validateOCCResults(results []occTxExecution, gasLimit uint64) bool { return true } -func mergeOCCResults(results []occTxExecution) *BlockResult { - blockResult := &BlockResult{ - Txs: make([]TxResult, len(results)), - Receipts: make(ethtypes.Receipts, len(results)), +func (e *Executor) mergeOCCResults(ctx context.Context, results []occTxExecution) (*BlockResult, error) { + blockResult, err := e.acquireBlockResult(ctx, len(results)) + if err != nil { + return nil, err } + blockResult.prepareIndexedResults(len(results)) var logIndex uint for i, result := range results { blockResult.GasUsed += result.gasUsed @@ -173,8 +193,8 @@ func mergeOCCResults(results []occTxExecution) *BlockResult { blockResult.Txs[i] = result.txResult blockResult.Receipts[i] = result.receipt } - blockResult.ChangeSet = mergeChangeSets(results) - return blockResult + mergeChangeSetsInto(results, &blockResult.ChangeSet) + return blockResult, nil } type stateAccessIndex struct { @@ -230,6 +250,13 @@ type storageChangeKey struct { } func mergeChangeSets(results []occTxExecution) StateChangeSet { + var merged StateChangeSet + mergeChangeSetsInto(results, &merged) + return merged +} + +func mergeChangeSetsInto(results []occTxExecution, merged *StateChangeSet) { + merged.resetForReuse() balances := map[common.Address]*big.Int{} nonces := map[common.Address]uint64{} code := map[common.Address]CodeChange{} @@ -254,7 +281,6 @@ func mergeChangeSets(results []occTxExecution) StateChangeSet { } } - var merged StateChangeSet balanceAddrs := sortedAddressesFromBigMap(balances) for _, addr := range balanceAddrs { merged.Balances = append(merged.Balances, BalanceChange{Address: addr, Balance: cloneBig(balances[addr])}) @@ -282,7 +308,6 @@ func mergeChangeSets(results []occTxExecution) StateChangeSet { for _, key := range storageKeys { merged.Storage = append(merged.Storage, storage[key]) } - return merged } func sortedAddressesFromBigMap(values map[common.Address]*big.Int) []common.Address { diff --git a/giga/evmonly/occ_pool.go b/giga/evmonly/occ_pool.go index 0678968a96..900126bad1 100644 --- a/giga/evmonly/occ_pool.go +++ b/giga/evmonly/occ_pool.go @@ -16,7 +16,7 @@ type occWorkerPool struct { type occPoolJob struct { ctx context.Context txRange occTxRange - run func(context.Context, occTxRange) error + run func(context.Context, occTxRange, *occWorkerScratch) error done *sync.WaitGroup cancel context.CancelFunc @@ -46,22 +46,23 @@ func newOCCWorkerPool(workers int) *occWorkerPool { } func (p *occWorkerPool) runWorker() { + scratch := &occWorkerScratch{} for { select { case <-p.stop: return case job := <-p.jobs: - p.runJob(job) + p.runJob(job, scratch) } } } -func (p *occWorkerPool) runJob(job occPoolJob) { +func (p *occWorkerPool) runJob(job occPoolJob, scratch *occWorkerScratch) { defer job.done.Done() if err := job.ctx.Err(); err != nil { return } - if err := job.run(job.ctx, job.txRange); err != nil { + if err := job.run(job.ctx, job.txRange, scratch); err != nil { job.errOnce.Do(func() { *job.err = err job.cancel() @@ -69,7 +70,7 @@ func (p *occWorkerPool) runJob(job occPoolJob) { } } -func (p *occWorkerPool) Run(ctx context.Context, ranges []occTxRange, run func(context.Context, occTxRange) error) error { +func (p *occWorkerPool) Run(ctx context.Context, ranges []occTxRange, run func(context.Context, occTxRange, *occWorkerScratch) error) error { jobCtx, cancel := context.WithCancel(ctx) defer cancel() diff --git a/giga/evmonly/result_pool.go b/giga/evmonly/result_pool.go new file mode 100644 index 0000000000..31d2be5f83 --- /dev/null +++ b/giga/evmonly/result_pool.go @@ -0,0 +1,115 @@ +package evmonly + +import ( + "context" + "sync" + "sync/atomic" + + ethtypes "github.com/ethereum/go-ethereum/core/types" +) + +type blockResultPool struct { + free chan *BlockResult +} + +type blockResultLease struct { + pool *blockResultPool + result *BlockResult + refs atomic.Int32 +} + +func newBlockResultPool(size int) *blockResultPool { + if size <= 0 { + return nil + } + p := &blockResultPool{free: make(chan *BlockResult, size)} + for range size { + p.free <- &BlockResult{} + } + return p +} + +func (p *blockResultPool) acquire(ctx context.Context, txCapacity int) (*BlockResult, error) { + if p == nil { + result := &BlockResult{} + result.prepareForBlock(txCapacity) + return result, nil + } + select { + case result := <-p.free: + result.prepareForBlock(txCapacity) + lease := &blockResultLease{pool: p, result: result} + lease.refs.Store(1) + result.lease = lease + return result, nil + case <-ctx.Done(): + return nil, ctx.Err() + } +} + +func (l *blockResultLease) retain() func() { + if l == nil { + return func() {} + } + l.refs.Add(1) + var once sync.Once + return func() { + once.Do(l.release) + } +} + +func (l *blockResultLease) release() { + if l == nil { + return + } + if l.refs.Add(-1) != 0 { + return + } + result := l.result + result.resetForPool() + l.pool.free <- result +} + +func (r *BlockResult) retain() func() { + if r == nil || r.lease == nil { + return func() {} + } + return r.lease.retain() +} + +func (r *BlockResult) prepareForBlock(txCapacity int) { + r.resetForPool() + if cap(r.Txs) < txCapacity { + r.Txs = make([]TxResult, 0, txCapacity) + } + if cap(r.Receipts) < txCapacity { + r.Receipts = make(ethtypes.Receipts, 0, txCapacity) + } +} + +func (r *BlockResult) prepareIndexedResults(txCount int) { + r.prepareForBlock(txCount) + r.Txs = r.Txs[:txCount] + r.Receipts = r.Receipts[:txCount] +} + +func (r *BlockResult) resetForPool() { + r.ChangeSet.resetForReuse() + clear(r.Txs) + r.Txs = r.Txs[:0] + clear(r.Receipts) + r.Receipts = r.Receipts[:0] + r.GasUsed = 0 + r.lease = nil +} + +func (cs *StateChangeSet) resetForReuse() { + clear(cs.Balances) + cs.Balances = cs.Balances[:0] + clear(cs.Nonces) + cs.Nonces = cs.Nonces[:0] + clear(cs.Code) + cs.Code = cs.Code[:0] + clear(cs.Storage) + cs.Storage = cs.Storage[:0] +} diff --git a/giga/evmonly/state_db.go b/giga/evmonly/state_db.go index f3a929042a..e91fcd6ed1 100644 --- a/giga/evmonly/state_db.go +++ b/giga/evmonly/state_db.go @@ -114,6 +114,13 @@ func newNativeStateDB(source StateReader) *nativeStateDB { } func (s *nativeStateDB) ChangeSet() StateChangeSet { + var changes StateChangeSet + s.ChangeSetInto(&changes) + return changes +} + +func (s *nativeStateDB) ChangeSetInto(changes *StateChangeSet) { + changes.resetForReuse() addresses := make([]common.Address, 0, len(s.accounts)) for addr := range s.accounts { addresses = append(addresses, addr) @@ -122,7 +129,6 @@ func (s *nativeStateDB) ChangeSet() StateChangeSet { return bytes.Compare(addresses[i][:], addresses[j][:]) < 0 }) - var changes StateChangeSet for _, addr := range addresses { acct := s.accounts[addr] base := s.baseAccount(addr) @@ -161,7 +167,6 @@ func (s *nativeStateDB) ChangeSet() StateChangeSet { }) } } - return changes } func (s *nativeStateDB) CreateAccount(addr common.Address) { @@ -413,8 +418,8 @@ func (s *nativeStateDB) AddSlotToAccessList(addr common.Address, slot common.Has } func (s *nativeStateDB) Prepare(_ params.Rules, sender, coinbase common.Address, dest *common.Address, precompiles []common.Address, txAccesses ethtypes.AccessList) { - s.accessList = newAccessList() - s.transientStates = map[common.Address]map[common.Hash]common.Hash{} + s.accessList.reset() + clearNestedHashMaps(s.transientStates) s.AddAddressToAccessList(sender) s.AddAddressToAccessList(coinbase) if dest != nil { @@ -576,8 +581,16 @@ func (s *nativeStateDB) SetEVM(evm *vm.EVM) { } func (s *nativeStateDB) enableAccessTracking() { - s.readSet = map[stateAccessKey]struct{}{} - s.writeSet = map[stateAccessKey]struct{}{} + if s.readSet == nil { + s.readSet = map[stateAccessKey]struct{}{} + } else { + clear(s.readSet) + } + if s.writeSet == nil { + s.writeSet = map[stateAccessKey]struct{}{} + } else { + clear(s.writeSet) + } } func (s *nativeStateDB) accessSets() (map[stateAccessKey]struct{}, map[stateAccessKey]struct{}) { @@ -626,8 +639,41 @@ func (s *nativeStateDB) markForFinalise(addr common.Address) { } func (s *nativeStateDB) clearSnapshots() { + clear(s.journal) + s.journal = s.journal[:0] + clear(s.snapshots) + s.snapshots = s.snapshots[:0] +} + +func (s *nativeStateDB) reset(source StateReader) { + if source == nil { + source = NewMemoryState() + } + s.source = source + clear(s.accounts) + clear(s.base) + s.refund = 0 + clear(s.logs) + s.logs = s.logs[:0] + clearBytesMap(s.preimages) + s.accessList.reset() + clearNestedHashMaps(s.transientStates) + clear(s.finaliseAddrs) + clear(s.journal) s.journal = s.journal[:0] + clear(s.snapshots) s.snapshots = s.snapshots[:0] + if s.readSet != nil { + clear(s.readSet) + } + if s.writeSet != nil { + clear(s.writeSet) + } + s.txHash = common.Hash{} + s.txIndex = 0 + s.txIndexUint = 0 + s.err = nil + s.evm = nil } func (s *nativeStateDB) account(addr common.Address) *nativeAccount { @@ -705,6 +751,22 @@ func newAccessList() accessList { } } +func (al *accessList) reset() { + if al.addresses == nil { + al.addresses = map[common.Address]struct{}{} + } else { + clear(al.addresses) + } + if al.slots == nil { + al.slots = map[common.Address]map[common.Hash]struct{}{} + return + } + for _, slots := range al.slots { + clear(slots) + } + clear(al.slots) +} + func cloneAccessList(al accessList) accessList { cp := newAccessList() for addr := range al.addresses { @@ -765,6 +827,19 @@ func clonePreimages(preimages map[common.Hash][]byte) map[common.Hash][]byte { return cp } +func clearBytesMap(values map[common.Hash][]byte) { + for key := range values { + delete(values, key) + } +} + +func clearNestedHashMaps(values map[common.Address]map[common.Hash]common.Hash) { + for _, slots := range values { + clear(slots) + } + clear(values) +} + func cloneJournal(journal []nativeJournalEntry) []nativeJournalEntry { cp := make([]nativeJournalEntry, len(journal)) for i, entry := range journal { diff --git a/giga/evmonly/types.go b/giga/evmonly/types.go index 639d3f1694..6b5706e39a 100644 --- a/giga/evmonly/types.go +++ b/giga/evmonly/types.go @@ -28,6 +28,15 @@ type ResultSink interface { StoreReceipts(ctx context.Context, height uint64, receipts ethtypes.Receipts) error } +// BlockResultSink can retain a complete BlockResult without forcing the +// executor to copy changesets or receipts before handing them to an async sink. +// The sink must invoke release exactly once after it no longer references +// result. If StoreBlockResult returns an error, the executor releases that sink +// reference. +type BlockResultSink interface { + StoreBlockResult(ctx context.Context, height uint64, result *BlockResult, release func()) error +} + // BlockRequest contains all consensus/runtime inputs needed to execute a block. // Txs must be raw Ethereum transaction RLP bytes. type BlockRequest struct { @@ -68,6 +77,19 @@ type BlockResult struct { Txs []TxResult Receipts ethtypes.Receipts GasUsed uint64 + + lease *blockResultLease +} + +// Release returns a pooled BlockResult to its executor-owned pool. It is a +// no-op for results that were not allocated from a pool. +func (r *BlockResult) Release() { + if r == nil || r.lease == nil { + return + } + lease := r.lease + r.lease = nil + lease.release() } // StateChangeSet is the deterministic EVM-native state output for a block. From a38d56ec60ed02e452d930e22512c0027a6732d3 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Thu, 2 Jul 2026 21:15:34 +0800 Subject: [PATCH 19/27] preserve pooled result leases --- giga/evmonly/result_pool.go | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/giga/evmonly/result_pool.go b/giga/evmonly/result_pool.go index 31d2be5f83..f8617853f5 100644 --- a/giga/evmonly/result_pool.go +++ b/giga/evmonly/result_pool.go @@ -88,9 +88,18 @@ func (r *BlockResult) prepareForBlock(txCapacity int) { } func (r *BlockResult) prepareIndexedResults(txCount int) { - r.prepareForBlock(txCount) - r.Txs = r.Txs[:txCount] - r.Receipts = r.Receipts[:txCount] + if cap(r.Txs) < txCount { + r.Txs = make([]TxResult, txCount) + } else { + r.Txs = r.Txs[:txCount] + clear(r.Txs) + } + if cap(r.Receipts) < txCount { + r.Receipts = make(ethtypes.Receipts, txCount) + } else { + r.Receipts = r.Receipts[:txCount] + clear(r.Receipts) + } } func (r *BlockResult) resetForPool() { From 7ba41c4ff7f6e21cbeb6ddfd2822be58be0b04d1 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Thu, 2 Jul 2026 21:20:43 +0800 Subject: [PATCH 20/27] avoid evmonly statedb scratch reuse --- giga/evmonly/occ.go | 23 +++-------------------- giga/evmonly/occ_pool.go | 11 +++++------ 2 files changed, 8 insertions(+), 26 deletions(-) diff --git a/giga/evmonly/occ.go b/giga/evmonly/occ.go index c9977a0628..27c8032745 100644 --- a/giga/evmonly/occ.go +++ b/giga/evmonly/occ.go @@ -29,19 +29,6 @@ type occTxRange struct { end int } -type occWorkerScratch struct { - stateDB *nativeStateDB -} - -func (s *occWorkerScratch) resetStateDB(source StateReader) *nativeStateDB { - if s.stateDB == nil { - s.stateDB = newNativeStateDB(source) - } else { - s.stateDB.reset(source) - } - return s.stateDB -} - func (e *Executor) executeBlockOCC(ctx context.Context, req PreparedBlock) (*BlockResult, error) { chainConfig := e.chainConfig(req.Context) blockCtx := buildBlockContext(req.Context) @@ -63,9 +50,9 @@ func (e *Executor) executeBlockOCC(ctx context.Context, req PreparedBlock) (*Blo pool = newOCCWorkerPool(workers) defer pool.Close() } - if err := pool.Run(ctx, occRanges(txCount, chunkSize), func(workerCtx context.Context, txRange occTxRange, scratch *occWorkerScratch) error { + if err := pool.Run(ctx, occRanges(txCount, chunkSize), func(workerCtx context.Context, txRange occTxRange) error { for idx := txRange.start; idx < txRange.end; idx++ { - result, err := e.executeTxSpeculative(workerCtx, scratch, req, idx, chainConfig, blockCtx, baseFee, gasLimit) + result, err := e.executeTxSpeculative(workerCtx, req, idx, chainConfig, blockCtx, baseFee, gasLimit) if err != nil { return err } @@ -113,7 +100,6 @@ func occChunkSize(txCount int, workers int) int { func (e *Executor) executeTxSpeculative( ctx context.Context, - scratch *occWorkerScratch, req PreparedBlock, txIndex int, chainConfig *params.ChainConfig, @@ -121,11 +107,8 @@ func (e *Executor) executeTxSpeculative( baseFee *big.Int, gasLimit uint64, ) (occTxExecution, error) { - if scratch == nil { - scratch = &occWorkerScratch{} - } p := req.Txs[txIndex] - stateDB := scratch.resetStateDB(e.state) + stateDB := newNativeStateDB(e.state) stateDB.enableAccessTracking() evm := vm.NewEVM(blockCtx, stateDB, chainConfig, vm.Config{}, nil) stateDB.SetEVM(evm) diff --git a/giga/evmonly/occ_pool.go b/giga/evmonly/occ_pool.go index 900126bad1..0678968a96 100644 --- a/giga/evmonly/occ_pool.go +++ b/giga/evmonly/occ_pool.go @@ -16,7 +16,7 @@ type occWorkerPool struct { type occPoolJob struct { ctx context.Context txRange occTxRange - run func(context.Context, occTxRange, *occWorkerScratch) error + run func(context.Context, occTxRange) error done *sync.WaitGroup cancel context.CancelFunc @@ -46,23 +46,22 @@ func newOCCWorkerPool(workers int) *occWorkerPool { } func (p *occWorkerPool) runWorker() { - scratch := &occWorkerScratch{} for { select { case <-p.stop: return case job := <-p.jobs: - p.runJob(job, scratch) + p.runJob(job) } } } -func (p *occWorkerPool) runJob(job occPoolJob, scratch *occWorkerScratch) { +func (p *occWorkerPool) runJob(job occPoolJob) { defer job.done.Done() if err := job.ctx.Err(); err != nil { return } - if err := job.run(job.ctx, job.txRange, scratch); err != nil { + if err := job.run(job.ctx, job.txRange); err != nil { job.errOnce.Do(func() { *job.err = err job.cancel() @@ -70,7 +69,7 @@ func (p *occWorkerPool) runJob(job occPoolJob, scratch *occWorkerScratch) { } } -func (p *occWorkerPool) Run(ctx context.Context, ranges []occTxRange, run func(context.Context, occTxRange, *occWorkerScratch) error) error { +func (p *occWorkerPool) Run(ctx context.Context, ranges []occTxRange, run func(context.Context, occTxRange) error) error { jobCtx, cancel := context.WithCancel(ctx) defer cancel() From 9ae1cfb7369d52fa8117f4054ec4bc86c82fac5e Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Thu, 2 Jul 2026 21:42:26 +0800 Subject: [PATCH 21/27] add evmonly OCC fallback stats --- giga/evmonly/executor_test.go | 16 ++++ giga/evmonly/occ.go | 138 +++++++++++++++++++++++++++++++--- giga/evmonly/result_pool.go | 1 + giga/evmonly/types.go | 20 +++++ 4 files changed, 164 insertions(+), 11 deletions(-) diff --git a/giga/evmonly/executor_test.go b/giga/evmonly/executor_test.go index 911d184b41..4a4bf9d290 100644 --- a/giga/evmonly/executor_test.go +++ b/giga/evmonly/executor_test.go @@ -215,6 +215,9 @@ func TestExecutorOCCNonConflictingTransfersMatchSequential(t *testing.T) { require.Equal(t, seqResult.GasUsed, occResult.GasUsed) require.Len(t, occResult.Txs, txCount) require.Len(t, occResult.Receipts, txCount) + require.True(t, occResult.OCCStats.Attempted) + require.False(t, occResult.OCCStats.Fallback) + require.Zero(t, occResult.OCCStats.ConflictCount) for i := range txCount { require.Equal(t, seqResult.Txs[i].Hash, occResult.Txs[i].Hash) require.Equal(t, seqResult.Txs[i].Status, occResult.Txs[i].Status) @@ -256,6 +259,19 @@ func TestExecutorOCCConflictingTransfersMatchSequential(t *testing.T) { seqState.ApplyChangeSet(seqResult.ChangeSet) occState.ApplyChangeSet(occResult.ChangeSet) require.Equal(t, seqResult.GasUsed, occResult.GasUsed) + require.True(t, occResult.OCCStats.Attempted) + require.True(t, occResult.OCCStats.Fallback) + require.Equal(t, "conflict", occResult.OCCStats.FallbackReason) + require.Greater(t, occResult.OCCStats.ConflictCount, uint64(0)) + require.NotEmpty(t, occResult.OCCStats.ConflictSamples) + foundRecipientBalanceConflict := false + for _, conflict := range occResult.OCCStats.ConflictSamples { + if conflict.Kind == "balance" && conflict.Address == recipient { + foundRecipientBalanceConflict = true + require.Greater(t, conflict.Count, uint64(0)) + } + } + require.True(t, foundRecipientBalanceConflict) require.Equal(t, seqState.GetBalance(recipient), occState.GetBalance(recipient)) require.Equal(t, big.NewInt(int64(txCount*3)), occState.GetBalance(recipient)) } diff --git a/giga/evmonly/occ.go b/giga/evmonly/occ.go index 27c8032745..30654c46fd 100644 --- a/giga/evmonly/occ.go +++ b/giga/evmonly/occ.go @@ -62,10 +62,21 @@ func (e *Executor) executeBlockOCC(ctx context.Context, req PreparedBlock) (*Blo }); err != nil { return nil, err } - if !validateOCCResults(results, gasLimit) { - return e.executeBlockSequential(ctx, req) + validation := validateOCCResults(results, gasLimit) + if !validation.valid { + result, err := e.executeBlockSequential(ctx, req) + if err != nil { + return nil, err + } + result.OCCStats = validation.stats(true) + return result, nil } - return e.mergeOCCResults(ctx, results) + result, err := e.mergeOCCResults(ctx, results) + if err != nil { + return nil, err + } + result.OCCStats = validation.stats(false) + return result, nil } func occRanges(txCount int, chunkSize int) []occTxRange { @@ -139,23 +150,125 @@ func (e *Executor) executeTxSpeculative( }, nil } -func validateOCCResults(results []occTxExecution, gasLimit uint64) bool { +type occValidationResult struct { + valid bool + fallbackReason string + conflictCount uint64 + conflicts map[occConflictAggregationKey]uint64 +} + +type occConflictAggregationKey struct { + access string + kind stateAccessKind + address common.Address + slot common.Hash +} + +const ( + occFallbackReasonConflict = "conflict" + occFallbackReasonGasLimit = "gas_limit" + occFallbackReasonGasOverflow = "gas_overflow" +) + +func validateOCCResults(results []occTxExecution, gasLimit uint64) occValidationResult { writes := newStateAccessIndex() var totalGas uint64 + validation := occValidationResult{valid: true} for _, result := range results { if result.gasUsed > math.MaxUint64-totalGas { - return false + validation.valid = false + validation.fallbackReason = occFallbackReasonGasOverflow + return validation } totalGas += result.gasUsed if totalGas > gasLimit { - return false - } - if writes.conflictsWithAny(result.readSet) || writes.conflictsWithAny(result.writeSet) { - return false + validation.valid = false + validation.fallbackReason = occFallbackReasonGasLimit + return validation } + validation.addConflicts("read", writes, result.readSet) + validation.addConflicts("write", writes, result.writeSet) writes.addAll(result.writeSet) } - return true + if validation.conflictCount > 0 { + validation.valid = false + validation.fallbackReason = occFallbackReasonConflict + } + return validation +} + +func (r *occValidationResult) addConflicts(access string, writes *stateAccessIndex, set map[stateAccessKey]struct{}) { + for key := range set { + if !writes.conflictsWith(key) { + continue + } + if r.conflicts == nil { + r.conflicts = map[occConflictAggregationKey]uint64{} + } + r.conflictCount++ + r.conflicts[occConflictAggregationKey{ + access: access, + kind: key.kind, + address: key.address, + slot: key.slot, + }]++ + } +} + +func (r occValidationResult) stats(fallback bool) OCCStats { + stats := OCCStats{ + Attempted: true, + Fallback: fallback, + FallbackReason: r.fallbackReason, + ConflictCount: r.conflictCount, + } + if len(r.conflicts) == 0 { + return stats + } + keys := make([]occConflictAggregationKey, 0, len(r.conflicts)) + for key := range r.conflicts { + keys = append(keys, key) + } + sort.Slice(keys, func(i, j int) bool { + left, right := keys[i], keys[j] + if left.access != right.access { + return left.access < right.access + } + if left.kind != right.kind { + return left.kind < right.kind + } + if cmp := bytes.Compare(left.address[:], right.address[:]); cmp != 0 { + return cmp < 0 + } + return bytes.Compare(left.slot[:], right.slot[:]) < 0 + }) + for _, key := range keys { + stats.ConflictSamples = append(stats.ConflictSamples, OCCConflictCount{ + Access: key.access, + Kind: key.kind.String(), + Address: key.address, + Slot: key.slot, + Count: r.conflicts[key], + }) + } + return stats +} + +func (k stateAccessKind) String() string { + switch k { + case stateAccessAccount: + return "account" + case stateAccessBalance: + return "balance" + case stateAccessNonce: + return "nonce" + case stateAccessCode: + return "code" + case stateAccessStorage: + return "storage" + default: + return "unknown" + } } func (e *Executor) mergeOCCResults(ctx context.Context, results []occTxExecution) (*BlockResult, error) { @@ -220,7 +333,10 @@ func (i *stateAccessIndex) conflictsWith(key stateAccessKey) bool { func (i *stateAccessIndex) addAll(set map[stateAccessKey]struct{}) { for key := range set { i.exact[key] = struct{}{} - i.touched[key.address] = struct{}{} + // Exist/Empty account reads depend on account metadata, not storage slots. + if key.kind != stateAccessStorage { + i.touched[key.address] = struct{}{} + } if key.kind == stateAccessAccount { i.account[key.address] = struct{}{} } diff --git a/giga/evmonly/result_pool.go b/giga/evmonly/result_pool.go index f8617853f5..15e31adc4c 100644 --- a/giga/evmonly/result_pool.go +++ b/giga/evmonly/result_pool.go @@ -109,6 +109,7 @@ func (r *BlockResult) resetForPool() { clear(r.Receipts) r.Receipts = r.Receipts[:0] r.GasUsed = 0 + r.OCCStats = OCCStats{} r.lease = nil } diff --git a/giga/evmonly/types.go b/giga/evmonly/types.go index 6b5706e39a..730a64a02a 100644 --- a/giga/evmonly/types.go +++ b/giga/evmonly/types.go @@ -77,6 +77,7 @@ type BlockResult struct { Txs []TxResult Receipts ethtypes.Receipts GasUsed uint64 + OCCStats OCCStats lease *blockResultLease } @@ -92,6 +93,25 @@ func (r *BlockResult) Release() { lease.release() } +// OCCStats reports optimistic concurrency control behavior for a block. +type OCCStats struct { + Attempted bool + Fallback bool + FallbackReason string + ConflictCount uint64 + ConflictSamples []OCCConflictCount +} + +// OCCConflictCount aggregates conflicts by the access key that forced OCC to +// fall back to sequential execution. +type OCCConflictCount struct { + Access string + Kind string + Address common.Address + Slot common.Hash + Count uint64 +} + // StateChangeSet is the deterministic EVM-native state output for a block. // Values are post-block values, not deltas. type StateChangeSet struct { From f36a57cf1e59033b90540506fd585d6800e61a7d Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Mon, 22 Jun 2026 15:50:15 +0800 Subject: [PATCH 22/27] add evm-only staking precompile --- giga/evmonly/README.md | 18 +- giga/evmonly/executor.go | 32 +- giga/evmonly/executor_test.go | 417 +++++++++ giga/evmonly/precompile_adapter.go | 302 ++++++ giga/evmonly/precompiles/context.go | 51 +- giga/evmonly/precompiles/staking/abi.json | 1 + giga/evmonly/precompiles/staking/balances.go | 52 ++ giga/evmonly/precompiles/staking/endblock.go | 365 ++++++++ giga/evmonly/precompiles/staking/events.go | 16 + giga/evmonly/precompiles/staking/helpers.go | 63 ++ giga/evmonly/precompiles/staking/staking.go | 872 ++++++++++++++++++ .../precompiles/staking/staking_test.go | 295 ++++++ giga/evmonly/precompiles/staking/state.go | 667 ++++++++++++++ giga/evmonly/precompiles/staking/types.go | 122 +++ giga/evmonly/precompiles/util/events.go | 27 + giga/evmonly/precompiles/util/helpers.go | 69 ++ giga/evmonly/precompiles/util/json.go | 28 + giga/evmonly/result_pool.go | 2 + giga/evmonly/types.go | 15 +- 19 files changed, 3360 insertions(+), 54 deletions(-) create mode 100644 giga/evmonly/precompile_adapter.go create mode 100644 giga/evmonly/precompiles/staking/abi.json create mode 100644 giga/evmonly/precompiles/staking/balances.go create mode 100644 giga/evmonly/precompiles/staking/endblock.go create mode 100644 giga/evmonly/precompiles/staking/events.go create mode 100644 giga/evmonly/precompiles/staking/helpers.go create mode 100644 giga/evmonly/precompiles/staking/staking.go create mode 100644 giga/evmonly/precompiles/staking/staking_test.go create mode 100644 giga/evmonly/precompiles/staking/state.go create mode 100644 giga/evmonly/precompiles/staking/types.go create mode 100644 giga/evmonly/precompiles/util/events.go create mode 100644 giga/evmonly/precompiles/util/helpers.go create mode 100644 giga/evmonly/precompiles/util/json.go diff --git a/giga/evmonly/README.md b/giga/evmonly/README.md index b748f13247..cb1ea464ea 100644 --- a/giga/evmonly/README.md +++ b/giga/evmonly/README.md @@ -15,9 +15,10 @@ The target execution model is based on the `sei-v3` executor: 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. +receipts. The staking custom precompile is the first SDK-free implementation; +other 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 @@ -32,7 +33,7 @@ The `evmonly` package currently provides: - 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 +- fail-closed custom precompile placeholders plus an SDK-free staking precompile The executor accepts config for nonce checks, gas-price checks, minimum gas price, chain config, and the custom precompile registry. @@ -123,7 +124,7 @@ 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 +Most 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. @@ -133,9 +134,10 @@ 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`. +The staking precompile under `giga/evmonly/precompiles/staking` follows this +shape with a byte-key store backed by storage slots owned by the staking +precompile address. Registry entries without an implementation still fail +closed with `ErrCustomPrecompilesOpen`. ## Current limitations diff --git a/giga/evmonly/executor.go b/giga/evmonly/executor.go index 92bfd05561..5368ef7961 100644 --- a/giga/evmonly/executor.go +++ b/giga/evmonly/executor.go @@ -9,12 +9,10 @@ import ( "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. @@ -203,6 +201,11 @@ func (e *Executor) executeBlockSequential(ctx context.Context, req PreparedBlock result.GasUsed += txResult.GasUsed txIndexUint++ } + validatorUpdates, err := runCustomPrecompileEndBlock(e.cfg.CustomPrecompiles, evm) + if err != nil { + return nil, fmt.Errorf("run custom precompile end block: %w", err) + } + result.ValidatorUpdates = validatorUpdates stateDB.clearSnapshots() stateDB.Finalise(true) stateDB.ChangeSetInto(&result.ChangeSet) @@ -342,31 +345,6 @@ func buildBlockContext(ctx BlockContext) vm.BlockContext { } } -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 { diff --git a/giga/evmonly/executor_test.go b/giga/evmonly/executor_test.go index 4a4bf9d290..8812db412b 100644 --- a/giga/evmonly/executor_test.go +++ b/giga/evmonly/executor_test.go @@ -16,6 +16,8 @@ import ( "github.com/stretchr/testify/require" "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles" + stakingprecompile "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles/staking" + precompileutil "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles/util" ) const testGasPriceWei = 1_000_000_000 @@ -842,6 +844,243 @@ func TestExecutorCustomPrecompilePlaceholder(t *testing.T) { require.True(t, errors.Is(result.Txs[0].Err, precompiles.ErrCustomPrecompilesOpen)) } +func TestExecutorRegisteredCustomPrecompile(t *testing.T) { + chainID := big.NewInt(713715) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + customAddr := common.HexToAddress("0x0000000000000000000000000000000000001005") + + 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: contractPrecompileRegistry{ + customAddr: storeWritePrecompile{}, + }, + }, 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, ethtypes.ReceiptStatusSuccessful, result.Txs[0].Status) + require.NotEmpty(t, result.ChangeSet.Storage) + + state.ApplyChangeSet(result.ChangeSet) + require.Equal(t, encodedStoredLength(2), state.GetState(customAddr, storeBaseSlot([]byte("seen")))) +} + +func TestExecutorStakingPrecompileForwardsPayableValue(t *testing.T) { + chainID := big.NewInt(713715) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + stakingAddr := common.HexToAddress(stakingprecompile.StakingAddress) + + state := NewMemoryState() + initialBalance := big.NewInt(1_000_000_000_000_000_000) + state.SetBalance(sender, initialBalance) + + contract, err := stakingprecompile.NewPrecompile() + require.NoError(t, err) + registry, err := stakingprecompile.NewRegistry() + require.NoError(t, err) + input, err := contract.ABI().Pack( + stakingprecompile.CreateValidatorMethod, + "01020304", + "validator-one", + "0.100000000000000000", + "0.200000000000000000", + "0.010000000000000000", + big.NewInt(1), + ) + require.NoError(t, err) + value := new(big.Int).Mul(big.NewInt(5), big.NewInt(1_000_000_000_000)) + rawTx := signLegacyTx(t, key, chainID, 0, &stakingAddr, value, input) + executor := NewExecutor(Config{ + CustomPrecompiles: registry, + }, 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, ethtypes.ReceiptStatusSuccessful, result.Txs[0].Status) + require.Equal(t, []ValidatorUpdate{{PubKey: []byte{0x01, 0x02, 0x03, 0x04}, Power: 5}}, result.ValidatorUpdates) + + state.ApplyChangeSet(result.ChangeSet) + gasCost := new(big.Int).Mul(new(big.Int).SetUint64(result.Txs[0].GasUsed), result.Txs[0].EffectiveGasPrice) + require.Equal(t, new(big.Int).Sub(new(big.Int).Sub(initialBalance, value), gasCost), state.GetBalance(sender)) + require.Zero(t, state.GetBalance(stakingAddr).Sign()) + require.Equal(t, value, state.GetBalance(stakingprecompile.EscrowAddress())) +} + +func TestExecutorStakingDelegationLifecycleE2E(t *testing.T) { + chainID := big.NewInt(713715) + sourceKey, err := crypto.GenerateKey() + require.NoError(t, err) + dstKey, err := crypto.GenerateKey() + require.NoError(t, err) + delegatorKey, err := crypto.GenerateKey() + require.NoError(t, err) + + source := crypto.PubkeyToAddress(sourceKey.PublicKey) + destination := crypto.PubkeyToAddress(dstKey.PublicKey) + delegator := crypto.PubkeyToAddress(delegatorKey.PublicKey) + stakingAddr := common.HexToAddress(stakingprecompile.StakingAddress) + escrowAddr := stakingprecompile.EscrowAddress() + + state := NewMemoryState() + initialBalance := big.NewInt(1_000_000_000_000_000_000) + state.SetBalance(source, initialBalance) + state.SetBalance(destination, initialBalance) + state.SetBalance(delegator, initialBalance) + + registry, err := stakingprecompile.NewRegistry() + require.NoError(t, err) + contract, err := stakingprecompile.NewPrecompile() + require.NoError(t, err) + executor := NewExecutor(Config{CustomPrecompiles: registry}, WithState(state)) + + nonces := map[common.Address]uint64{} + signStakingTx := func(key *ecdsa.PrivateKey, value *big.Int, input []byte) []byte { + sender := crypto.PubkeyToAddress(key.PublicKey) + raw := signLegacyTx(t, key, chainID, nonces[sender], &stakingAddr, value, input) + nonces[sender]++ + return raw + } + expectedBalances := map[common.Address]*big.Int{ + source: new(big.Int).Set(initialBalance), + destination: new(big.Int).Set(initialBalance), + delegator: new(big.Int).Set(initialBalance), + stakingAddr: new(big.Int), + escrowAddr: new(big.Int), + } + + sourceSelfStake := usei(10) + destinationSelfStake := usei(5) + sourceSetupResult := executeBlockAndApply(t, executor, state, blockContextAt(chainID, 1, 100), [][]byte{ + signStakingTx(sourceKey, sourceSelfStake, mustPackStaking(t, contract, stakingprecompile.CreateValidatorMethod, + "01020304", + "source-validator", + "0.100000000000000000", + "0.200000000000000000", + "0.010000000000000000", + big.NewInt(1), + )), + }) + requireTxsSuccessful(t, sourceSetupResult, 1) + debitExpectedBalance(expectedBalances, source, sourceSelfStake, sourceSetupResult.Txs[0]) + addExpectedBalance(expectedBalances, escrowAddr, sourceSelfStake) + requireNativeBalances(t, state, expectedBalances) + require.Equal(t, []ValidatorUpdate{{PubKey: []byte{0x01, 0x02, 0x03, 0x04}, Power: 10}}, sourceSetupResult.ValidatorUpdates) + requireStakingPool(t, state, "10", "0") + requireStakingValidator(t, state, source, "10", "10", 3) + + destinationSetupResult := executeBlockAndApply(t, executor, state, blockContextAt(chainID, 2, 125), [][]byte{ + signStakingTx(dstKey, destinationSelfStake, mustPackStaking(t, contract, stakingprecompile.CreateValidatorMethod, + "05060708", + "destination-validator", + "0.100000000000000000", + "0.200000000000000000", + "0.010000000000000000", + big.NewInt(1), + )), + }) + requireTxsSuccessful(t, destinationSetupResult, 1) + debitExpectedBalance(expectedBalances, destination, destinationSelfStake, destinationSetupResult.Txs[0]) + addExpectedBalance(expectedBalances, escrowAddr, destinationSelfStake) + requireNativeBalances(t, state, expectedBalances) + require.Equal(t, []ValidatorUpdate{{PubKey: []byte{0x05, 0x06, 0x07, 0x08}, Power: 5}}, destinationSetupResult.ValidatorUpdates) + requireStakingPool(t, state, "15", "0") + requireStakingValidator(t, state, source, "10", "10", 3) + requireStakingValidator(t, state, destination, "5", "5", 3) + + delegationValue := usei(7) + delegateResult := executeBlockAndApply(t, executor, state, blockContextAt(chainID, 3, 150), [][]byte{ + signStakingTx(delegatorKey, delegationValue, mustPackStaking(t, contract, stakingprecompile.DelegateMethod, source.Hex())), + }) + requireTxsSuccessful(t, delegateResult, 1) + debitExpectedBalance(expectedBalances, delegator, delegationValue, delegateResult.Txs[0]) + addExpectedBalance(expectedBalances, escrowAddr, delegationValue) + requireNativeBalances(t, state, expectedBalances) + require.Equal(t, []ValidatorUpdate{{PubKey: []byte{0x01, 0x02, 0x03, 0x04}, Power: 17}}, delegateResult.ValidatorUpdates) + requireStakingPool(t, state, "22", "0") + requireStakingValidator(t, state, source, "17", "17", 3) + requireStakingValidator(t, state, destination, "5", "5", 3) + requireStakingDelegation(t, state, delegator, source, "7") + + redelegationAmount := big.NewInt(3) + redelegationTime := uint64(200) + redelegationCompletion := int64(redelegationTime + 1_814_400) + redelegateResult := executeBlockAndApply(t, executor, state, blockContextAt(chainID, 4, redelegationTime), [][]byte{ + signStakingTx(delegatorKey, nil, mustPackStaking(t, contract, stakingprecompile.RedelegateMethod, source.Hex(), destination.Hex(), redelegationAmount)), + }) + requireTxsSuccessful(t, redelegateResult, 1) + debitExpectedBalance(expectedBalances, delegator, nil, redelegateResult.Txs[0]) + requireNativeBalances(t, state, expectedBalances) + require.Equal(t, []ValidatorUpdate{ + {PubKey: []byte{0x01, 0x02, 0x03, 0x04}, Power: 14}, + {PubKey: []byte{0x05, 0x06, 0x07, 0x08}, Power: 8}, + }, redelegateResult.ValidatorUpdates) + requireStakingPool(t, state, "22", "0") + requireStakingValidator(t, state, source, "14", "14", 3) + requireStakingValidator(t, state, destination, "8", "8", 3) + requireStakingDelegation(t, state, delegator, source, "4") + requireStakingDelegation(t, state, delegator, destination, "3") + requireStakingRedelegation(t, state, delegator, source, destination, "3", redelegationCompletion) + + undelegationAmount := big.NewInt(2) + undelegationTime := uint64(300) + undelegationCompletion := int64(undelegationTime + 1_814_400) + undelegateResult := executeBlockAndApply(t, executor, state, blockContextAt(chainID, 5, undelegationTime), [][]byte{ + signStakingTx(delegatorKey, nil, mustPackStaking(t, contract, stakingprecompile.UndelegateMethod, destination.Hex(), undelegationAmount)), + }) + requireTxsSuccessful(t, undelegateResult, 1) + debitExpectedBalance(expectedBalances, delegator, nil, undelegateResult.Txs[0]) + requireNativeBalances(t, state, expectedBalances) + require.Equal(t, []ValidatorUpdate{{PubKey: []byte{0x05, 0x06, 0x07, 0x08}, Power: 6}}, undelegateResult.ValidatorUpdates) + requireStakingPool(t, state, "20", "2") + requireStakingValidator(t, state, source, "14", "14", 3) + requireStakingValidator(t, state, destination, "6", "6", 3) + requireStakingDelegation(t, state, delegator, source, "4") + requireStakingDelegation(t, state, delegator, destination, "1") + requireStakingRedelegation(t, state, delegator, source, destination, "3", redelegationCompletion) + requireStakingUnbonding(t, state, delegator, destination, "2", undelegationCompletion) + + redelegationMaturityResult := executeBlockAndApply(t, executor, state, blockContextAt(chainID, 6, uint64(redelegationCompletion)), nil) + require.Empty(t, redelegationMaturityResult.ValidatorUpdates) + requireNativeBalances(t, state, expectedBalances) + requireStakingPool(t, state, "20", "2") + requireStakingValidator(t, state, source, "14", "14", 3) + requireStakingValidator(t, state, destination, "6", "6", 3) + requireStakingDelegation(t, state, delegator, source, "4") + requireStakingDelegation(t, state, delegator, destination, "1") + requireNoStakingRedelegation(t, state, delegator, source, destination) + requireStakingUnbonding(t, state, delegator, destination, "2", undelegationCompletion) + + undelegationMaturityResult := executeBlockAndApply(t, executor, state, blockContextAt(chainID, 7, uint64(undelegationCompletion)), nil) + require.Empty(t, undelegationMaturityResult.ValidatorUpdates) + addExpectedBalance(expectedBalances, delegator, usei(2)) + addExpectedBalance(expectedBalances, escrowAddr, new(big.Int).Neg(usei(2))) + requireNativeBalances(t, state, expectedBalances) + requireStakingPool(t, state, "20", "0") + requireStakingValidator(t, state, source, "14", "14", 3) + requireStakingValidator(t, state, destination, "6", "6", 3) + requireStakingDelegation(t, state, delegator, source, "4") + requireStakingDelegation(t, state, delegator, destination, "1") + requireNoStakingRedelegation(t, state, delegator, source, destination) + requireNoStakingUnbonding(t, state, delegator, destination) +} + 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) @@ -940,6 +1179,158 @@ func blockContext(chainID *big.Int) BlockContext { } } +func blockContextAt(chainID *big.Int, number uint64, blockTime uint64) BlockContext { + ctx := blockContext(chainID) + ctx.Number = number + ctx.Time = blockTime + return ctx +} + +func executeBlockAndApply(t *testing.T, executor *Executor, state StateWriter, block BlockContext, txs [][]byte) *BlockResult { + t.Helper() + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: block, + Txs: txs, + }) + require.NoError(t, err) + state.ApplyChangeSet(result.ChangeSet) + return result +} + +func requireTxsSuccessful(t *testing.T, result *BlockResult, count int) { + t.Helper() + require.Len(t, result.Txs, count) + require.Len(t, result.Receipts, count) + for _, tx := range result.Txs { + require.Equal(t, ethtypes.ReceiptStatusSuccessful, tx.Status) + require.NoError(t, tx.Err) + } +} + +func mustPackStaking(t *testing.T, contract *stakingprecompile.Precompile, method string, args ...interface{}) []byte { + t.Helper() + input, err := contract.ABI().Pack(method, args...) + require.NoError(t, err) + return input +} + +func usei(amount int64) *big.Int { + return new(big.Int).Mul(big.NewInt(amount), big.NewInt(1_000_000_000_000)) +} + +func debitExpectedBalance(expected map[common.Address]*big.Int, sender common.Address, value *big.Int, tx TxResult) { + gasCost := new(big.Int).Mul(new(big.Int).SetUint64(tx.GasUsed), tx.EffectiveGasPrice) + total := new(big.Int).Add(cloneBig(value), gasCost) + addExpectedBalance(expected, sender, new(big.Int).Neg(total)) +} + +func addExpectedBalance(expected map[common.Address]*big.Int, addr common.Address, amount *big.Int) { + if amount == nil || amount.Sign() == 0 { + return + } + current := expected[addr] + if current == nil { + current = new(big.Int) + } + expected[addr] = new(big.Int).Add(current, amount) +} + +func requireNativeBalances(t *testing.T, state StateReader, expected map[common.Address]*big.Int) { + t.Helper() + for addr, balance := range expected { + require.Equal(t, balance, state.GetBalance(addr), "balance %s", addr.Hex()) + } +} + +type stakingDelegationRecordForTest struct { + DelegatorAddress string `json:"delegator_address"` + ValidatorAddress string `json:"validator_address"` + Amount string `json:"amount"` +} + +func requireStakingPool(t *testing.T, state StateReader, bonded string, notBonded string) { + t.Helper() + pool, ok := loadStakingJSON[stakingprecompile.Pool](t, state, []byte("pool")) + require.True(t, ok) + require.Equal(t, bonded, pool.BondedTokens) + require.Equal(t, notBonded, pool.NotBondedTokens) +} + +func requireStakingValidator(t *testing.T, state StateReader, validator common.Address, tokens string, shares string, status int32) { + t.Helper() + record, ok := loadStakingJSON[stakingprecompile.Validator](t, state, []byte("validator/"+validator.Hex())) + require.True(t, ok) + require.Equal(t, validator.Hex(), record.OperatorAddress) + require.Equal(t, tokens, record.Tokens) + require.Equal(t, shares, record.DelegatorShares) + require.Equal(t, status, record.Status) +} + +func requireStakingDelegation(t *testing.T, state StateReader, delegator common.Address, validator common.Address, amount string) { + t.Helper() + record, ok := loadStakingJSON[stakingDelegationRecordForTest](t, state, []byte("delegation/"+delegator.Hex()+"/"+validator.Hex())) + require.True(t, ok) + require.Equal(t, delegator.Hex(), record.DelegatorAddress) + require.Equal(t, validator.Hex(), record.ValidatorAddress) + require.Equal(t, amount, record.Amount) +} + +func requireStakingRedelegation(t *testing.T, state StateReader, delegator common.Address, src common.Address, dst common.Address, amount string, completionTime int64) { + t.Helper() + record, ok := loadStakingJSON[stakingprecompile.Redelegation](t, state, stakingRedelegationKey(delegator, src, dst)) + require.True(t, ok) + require.Equal(t, delegator.Hex(), record.DelegatorAddress) + require.Equal(t, src.Hex(), record.ValidatorSrcAddress) + require.Equal(t, dst.Hex(), record.ValidatorDstAddress) + require.Len(t, record.Entries, 1) + require.Equal(t, amount, record.Entries[0].InitialBalance) + require.Equal(t, amount, record.Entries[0].SharesDst) + require.Equal(t, completionTime, record.Entries[0].CompletionTime) +} + +func requireNoStakingRedelegation(t *testing.T, state StateReader, delegator common.Address, src common.Address, dst common.Address) { + t.Helper() + _, ok := loadStakingJSON[stakingprecompile.Redelegation](t, state, stakingRedelegationKey(delegator, src, dst)) + require.False(t, ok) +} + +func requireStakingUnbonding(t *testing.T, state StateReader, delegator common.Address, validator common.Address, amount string, completionTime int64) { + t.Helper() + record, ok := loadStakingJSON[stakingprecompile.UnbondingDelegation](t, state, stakingUnbondingKey(delegator, validator)) + require.True(t, ok) + require.Equal(t, delegator.Hex(), record.DelegatorAddress) + require.Equal(t, validator.Hex(), record.ValidatorAddress) + require.Len(t, record.Entries, 1) + require.Equal(t, amount, record.Entries[0].InitialBalance) + require.Equal(t, amount, record.Entries[0].Balance) + require.Equal(t, completionTime, record.Entries[0].CompletionTime) +} + +func requireNoStakingUnbonding(t *testing.T, state StateReader, delegator common.Address, validator common.Address) { + t.Helper() + _, ok := loadStakingJSON[stakingprecompile.UnbondingDelegation](t, state, stakingUnbondingKey(delegator, validator)) + require.False(t, ok) +} + +func loadStakingJSON[T any](t *testing.T, state StateReader, key []byte) (T, bool) { + t.Helper() + store := storageBackedStore{ + db: newNativeStateDB(state), + address: common.HexToAddress(stakingprecompile.StakingAddress), + } + value, ok, err := precompileutil.GetJSON[T](store, key) + require.NoError(t, err) + return value, ok +} + +func stakingRedelegationKey(delegator common.Address, src common.Address, dst common.Address) []byte { + return []byte("redelegation/" + delegator.Hex() + "\x00" + src.Hex() + "\x00" + dst.Hex()) +} + +func stakingUnbondingKey(delegator common.Address, validator common.Address) []byte { + return []byte("unbonding/" + delegator.Hex() + "/" + validator.Hex()) +} + func legacySelfDestructChainConfig(chainID *big.Int) *params.ChainConfig { return ¶ms.ChainConfig{ ChainID: chainID, @@ -1010,3 +1401,29 @@ func (r staticPrecompileRegistry) Get(addr common.Address) (precompiles.Contract func (r staticPrecompileRegistry) Addresses() []common.Address { return []common.Address{r.addr} } + +type contractPrecompileRegistry map[common.Address]precompiles.Contract + +func (r contractPrecompileRegistry) Get(addr common.Address) (precompiles.Contract, bool) { + contract, ok := r[addr] + return contract, ok +} + +func (r contractPrecompileRegistry) Addresses() []common.Address { + addresses := make([]common.Address, 0, len(r)) + for addr := range r { + addresses = append(addresses, addr) + } + return addresses +} + +type storeWritePrecompile struct{} + +func (storeWritePrecompile) RequiredGas([]byte) uint64 { + return 100 +} + +func (storeWritePrecompile) Run(ctx *precompiles.Context, _ []byte) ([]byte, error) { + ctx.Store.Set([]byte("seen"), []byte{0xaa, 0xbb}) + return []byte{0x01}, nil +} diff --git a/giga/evmonly/precompile_adapter.go b/giga/evmonly/precompile_adapter.go new file mode 100644 index 0000000000..d023f88399 --- /dev/null +++ b/giga/evmonly/precompile_adapter.go @@ -0,0 +1,302 @@ +package evmonly + +import ( + "encoding/binary" + "errors" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/tracing" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" + "github.com/holiman/uint256" + + "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles" +) + +var errInvalidPrecompileStateDB = errors.New("evm-only precompile requires native state db") + +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 +} + +type registeredCustomPrecompile struct { + address common.Address + contract precompiles.Contract +} + +func (p registeredCustomPrecompile) RequiredGas(input []byte) uint64 { + return p.contract.RequiredGas(input) +} + +func (p registeredCustomPrecompile) Run(evm *vm.EVM, caller common.Address, _ common.Address, input []byte, value *big.Int, readOnly bool, isFromDelegateCall bool, _ *tracing.Hooks) ([]byte, error) { + return p.run(evm, caller, input, value, readOnly, isFromDelegateCall, 0) +} + +func (p registeredCustomPrecompile) RunAndCalculateGas(evm *vm.EVM, caller common.Address, _ common.Address, input []byte, suppliedGas uint64, value *big.Int, hooks *tracing.Hooks, readOnly bool, isFromDelegateCall bool) ([]byte, uint64, error) { + gasCost := p.RequiredGas(input) + if suppliedGas < gasCost { + return nil, 0, vm.ErrOutOfGas + } + remainingGas := suppliedGas - gasCost + if hooks != nil && hooks.OnGasChange != nil { + hooks.OnGasChange(suppliedGas, remainingGas, tracing.GasChangeCallPrecompiledContract) + } + ret, err := p.run(evm, caller, input, value, readOnly, isFromDelegateCall, remainingGas) + return ret, remainingGas, err +} + +func (p registeredCustomPrecompile) run(evm *vm.EVM, caller common.Address, input []byte, value *big.Int, readOnly bool, isFromDelegateCall bool, remainingGas uint64) ([]byte, error) { + stateDB, ok := evm.StateDB.(*nativeStateDB) + if !ok { + return nil, errInvalidPrecompileStateDB + } + ctx := &precompiles.Context{ + Caller: caller, + Address: p.address, + ApparentValue: cloneBig(value), + ReadOnly: readOnly, + DelegateCall: isFromDelegateCall, + GasRemaining: remainingGas, + Block: evmPrecompileBlockContext(evm), + Store: storageBackedStore{db: stateDB, address: p.address}, + Balances: nativeBalanceTransfer{db: stateDB}, + Logs: stateDB, + } + return p.contract.Run(ctx, input) +} + +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 { + contract, ok := registry.Get(addr) + if !ok || contract == nil { + contracts[addr] = unresolvedCustomPrecompile{} + continue + } + contracts[addr] = registeredCustomPrecompile{ + address: addr, + contract: contract, + } + } + return contracts +} + +func runCustomPrecompileEndBlock(registry precompiles.Registry, evm *vm.EVM) ([]precompiles.ValidatorUpdate, error) { + if registry == nil { + return nil, nil + } + stateDB, ok := evm.StateDB.(*nativeStateDB) + if !ok { + return nil, errInvalidPrecompileStateDB + } + addresses := registry.Addresses() + updates := make([]precompiles.ValidatorUpdate, 0) + for _, addr := range addresses { + contract, ok := registry.Get(addr) + if !ok || contract == nil { + continue + } + endBlocker, ok := contract.(precompiles.EndBlocker) + if !ok { + continue + } + ctx := &precompiles.EndBlockContext{ + Address: addr, + Block: evmPrecompileBlockContext(evm), + Store: storageBackedStore{db: stateDB, address: addr}, + Balances: nativeBalanceTransfer{db: stateDB}, + Logs: stateDB, + } + contractUpdates, err := endBlocker.EndBlock(ctx) + if err != nil { + return nil, err + } + updates = append(updates, contractUpdates...) + } + return updates, nil +} + +func evmPrecompileBlockContext(evm *vm.EVM) precompiles.BlockContext { + var number uint64 + if evm.Context.BlockNumber != nil { + number = evm.Context.BlockNumber.Uint64() + } + var chainID *big.Int + if cfg := evm.ChainConfig(); cfg != nil && cfg.ChainID != nil { + chainID = new(big.Int).Set(cfg.ChainID) + } + var prevRandao common.Hash + if evm.Context.Random != nil { + prevRandao = *evm.Context.Random + } + return precompiles.BlockContext{ + Number: number, + Time: evm.Context.Time, + ChainID: chainID, + BaseFee: cloneBig(evm.Context.BaseFee), + BlobBaseFee: cloneBig(evm.Context.BlobBaseFee), + Coinbase: evm.Context.Coinbase, + PrevRandao: prevRandao, + } +} + +type nativeBalanceTransfer struct { + db *nativeStateDB +} + +func (t nativeBalanceTransfer) Transfer(from common.Address, to common.Address, amount *big.Int) error { + if amount == nil || amount.Sign() == 0 { + return nil + } + if t.db.err != nil { + return t.db.err + } + u, err := uint256FromBigChecked(amount) + if err != nil { + t.db.err = err + return err + } + if t.db.GetBalance(from).Cmp(u) < 0 { + t.db.err = errInsufficientBalance + return errInsufficientBalance + } + t.db.SubBalance(from, u, tracing.BalanceChangeTransfer) + if t.db.err != nil { + return t.db.err + } + t.db.AddBalance(to, u, tracing.BalanceChangeTransfer) + return t.db.err +} + +func uint256FromBigChecked(v *big.Int) (*uint256.Int, error) { + if v == nil { + return uint256.NewInt(0), nil + } + if v.Sign() < 0 { + return nil, errors.New("negative amount") + } + u, overflow := uint256.FromBig(v) + if overflow { + return nil, errors.New("amount exceeds uint256") + } + if u == nil { + return uint256.NewInt(0), nil + } + return u, nil +} + +const ( + storeLengthDomain = "sei/evmonly/precompile-store/length/v1" + storeChunkDomain = "sei/evmonly/precompile-store/chunk/v1" +) + +type storageBackedStore struct { + db *nativeStateDB + address common.Address +} + +func (s storageBackedStore) Get(key []byte) ([]byte, bool) { + baseSlot := storeBaseSlot(key) + length, ok := s.length(baseSlot) + if !ok { + return nil, false + } + if length > uint64(^uint(0)>>1) { + return nil, false + } + chunks := chunkCount(length) + out := make([]byte, 0, int(chunks*32)) //nolint:gosec // length was bounded by max int above. + for i := uint64(0); i < chunks; i++ { + chunk := s.db.GetState(s.address, storeChunkSlot(baseSlot, i)) + out = append(out, chunk.Bytes()...) + } + return out[:int(length)], true //nolint:gosec // length was bounded by max int above. +} + +func (s storageBackedStore) Set(key []byte, value []byte) { + baseSlot := storeBaseSlot(key) + oldLength, oldOK := s.length(baseSlot) + oldChunks := uint64(0) + if oldOK { + oldChunks = chunkCount(oldLength) + } + newLength := uint64(len(value)) //nolint:gosec // slices cannot exceed max int. + newChunks := chunkCount(newLength) + s.db.SetState(s.address, baseSlot, encodedStoredLength(newLength)) + for i := uint64(0); i < newChunks; i++ { + start := int(i * 32) //nolint:gosec // i is bounded by len(value) chunks. + end := start + 32 + if end > len(value) { + end = len(value) + } + var chunk common.Hash + copy(chunk[:], value[start:end]) + s.db.SetState(s.address, storeChunkSlot(baseSlot, i), chunk) + } + for i := newChunks; i < oldChunks; i++ { + s.db.SetState(s.address, storeChunkSlot(baseSlot, i), common.Hash{}) + } +} + +func (s storageBackedStore) Delete(key []byte) { + baseSlot := storeBaseSlot(key) + length, ok := s.length(baseSlot) + if !ok { + return + } + for i := uint64(0); i < chunkCount(length); i++ { + s.db.SetState(s.address, storeChunkSlot(baseSlot, i), common.Hash{}) + } + s.db.SetState(s.address, baseSlot, common.Hash{}) +} + +func (s storageBackedStore) length(baseSlot common.Hash) (uint64, bool) { + encoded := s.db.GetState(s.address, baseSlot) + if encoded == (common.Hash{}) { + return 0, false + } + n := encoded.Big() + if n.Sign() == 0 { + return 0, false + } + n.Sub(n, big.NewInt(1)) + if !n.IsUint64() { + return 0, false + } + return n.Uint64(), true +} + +func storeBaseSlot(key []byte) common.Hash { + return crypto.Keccak256Hash([]byte(storeLengthDomain), key) +} + +func storeChunkSlot(baseSlot common.Hash, index uint64) common.Hash { + var indexBz [8]byte + binary.BigEndian.PutUint64(indexBz[:], index) + return crypto.Keccak256Hash([]byte(storeChunkDomain), baseSlot.Bytes(), indexBz[:]) +} + +func encodedStoredLength(length uint64) common.Hash { + return common.BigToHash(new(big.Int).SetUint64(length + 1)) +} + +func chunkCount(length uint64) uint64 { + if length == 0 { + return 0 + } + return (length + 31) / 32 +} diff --git a/giga/evmonly/precompiles/context.go b/giga/evmonly/precompiles/context.go index 828937d6b1..390566ff5f 100644 --- a/giga/evmonly/precompiles/context.go +++ b/giga/evmonly/precompiles/context.go @@ -22,6 +22,12 @@ type Contract interface { Run(*Context, []byte) ([]byte, error) } +// EndBlocker is implemented by custom precompiles that need per-block work +// after all transactions have executed. +type EndBlocker interface { + EndBlock(*EndBlockContext) ([]ValidatorUpdate, 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 { @@ -32,10 +38,21 @@ type Context struct { DelegateCall bool GasRemaining uint64 Block BlockContext - State State + Store Store + Balances BalanceTransfer Logs LogSink } +// EndBlockContext is the SDK-free context custom precompiles receive after all +// transactions in a block have executed. +type EndBlockContext struct { + Address common.Address + Block BlockContext + Store Store + Balances BalanceTransfer + Logs LogSink +} + // BlockContext is the block data custom precompiles may read. type BlockContext struct { Number uint64 @@ -47,19 +64,25 @@ type BlockContext struct { 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) +// ValidatorUpdate is the EVM-only validator set update shape. +type ValidatorUpdate struct { + PubKey []byte + Power int64 +} + +// Store is the byte-keyed state boundary custom precompiles use for module-like +// data. Implementations should make Get/Set/Delete visible through the same +// read/write tracking as ordinary EVM storage. +type Store interface { + Get([]byte) ([]byte, bool) + Set([]byte, []byte) + Delete([]byte) +} + +// BalanceTransfer moves native EVM value for precompiles that need to forward +// payable call value or adjust native balances alongside module-like state. +type BalanceTransfer interface { + Transfer(from common.Address, to common.Address, amount *big.Int) error } // LogSink lets custom precompiles emit Ethereum logs without Cosmos events. diff --git a/giga/evmonly/precompiles/staking/abi.json b/giga/evmonly/precompiles/staking/abi.json new file mode 100644 index 0000000000..a6584f25c5 --- /dev/null +++ b/giga/evmonly/precompiles/staking/abi.json @@ -0,0 +1 @@ +[{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"delegator","type":"address"},{"indexed":false,"internalType":"string","name":"validator","type":"string"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Delegate","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"delegator","type":"address"},{"indexed":false,"internalType":"string","name":"validator","type":"string"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"DelegationRewardsWithdrawn","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"delegator","type":"address"},{"indexed":false,"internalType":"string","name":"srcValidator","type":"string"},{"indexed":false,"internalType":"string","name":"dstValidator","type":"string"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Redelegate","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"delegator","type":"address"},{"indexed":false,"internalType":"string","name":"validator","type":"string"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Undelegate","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"creator","type":"address"},{"indexed":false,"internalType":"string","name":"validatorAddress","type":"string"},{"indexed":false,"internalType":"string","name":"moniker","type":"string"}],"name":"ValidatorCreated","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"editor","type":"address"},{"indexed":false,"internalType":"string","name":"validatorAddress","type":"string"},{"indexed":false,"internalType":"string","name":"moniker","type":"string"}],"name":"ValidatorEdited","type":"event"},{"inputs":[{"internalType":"string","name":"pubKeyHex","type":"string"},{"internalType":"string","name":"moniker","type":"string"},{"internalType":"string","name":"commissionRate","type":"string"},{"internalType":"string","name":"commissionMaxRate","type":"string"},{"internalType":"string","name":"commissionMaxChangeRate","type":"string"},{"internalType":"uint256","name":"minSelfDelegation","type":"uint256"}],"name":"createValidator","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"string","name":"valAddress","type":"string"}],"name":"delegate","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"delegator","type":"address"},{"internalType":"string","name":"valAddress","type":"string"}],"name":"delegation","outputs":[{"components":[{"components":[{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"string","name":"denom","type":"string"}],"internalType":"struct IStaking.Balance","name":"balance","type":"tuple"},{"components":[{"internalType":"string","name":"delegator_address","type":"string"},{"internalType":"uint256","name":"shares","type":"uint256"},{"internalType":"uint256","name":"decimals","type":"uint256"},{"internalType":"string","name":"validator_address","type":"string"}],"internalType":"struct IStaking.DelegationDetails","name":"delegation","type":"tuple"}],"internalType":"struct IStaking.Delegation","name":"delegation","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"delegator","type":"address"},{"internalType":"bytes","name":"nextKey","type":"bytes"}],"name":"delegatorDelegations","outputs":[{"components":[{"components":[{"components":[{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"string","name":"denom","type":"string"}],"internalType":"struct IStaking.Balance","name":"balance","type":"tuple"},{"components":[{"internalType":"string","name":"delegator_address","type":"string"},{"internalType":"uint256","name":"shares","type":"uint256"},{"internalType":"uint256","name":"decimals","type":"uint256"},{"internalType":"string","name":"validator_address","type":"string"}],"internalType":"struct IStaking.DelegationDetails","name":"delegation","type":"tuple"}],"internalType":"struct IStaking.Delegation[]","name":"delegations","type":"tuple[]"},{"internalType":"bytes","name":"nextKey","type":"bytes"}],"internalType":"struct IStaking.DelegationsResponse","name":"response","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"delegator","type":"address"},{"internalType":"bytes","name":"nextKey","type":"bytes"}],"name":"delegatorUnbondingDelegations","outputs":[{"components":[{"components":[{"internalType":"string","name":"delegatorAddress","type":"string"},{"internalType":"string","name":"validatorAddress","type":"string"},{"components":[{"internalType":"int64","name":"creationHeight","type":"int64"},{"internalType":"int64","name":"completionTime","type":"int64"},{"internalType":"string","name":"initialBalance","type":"string"},{"internalType":"string","name":"balance","type":"string"}],"internalType":"struct IStaking.UnbondingDelegationEntry[]","name":"entries","type":"tuple[]"}],"internalType":"struct IStaking.UnbondingDelegation[]","name":"unbondingDelegations","type":"tuple[]"},{"internalType":"bytes","name":"nextKey","type":"bytes"}],"internalType":"struct IStaking.UnbondingDelegationsResponse","name":"response","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"delegator","type":"address"},{"internalType":"string","name":"validatorAddress","type":"string"}],"name":"delegatorValidator","outputs":[{"components":[{"internalType":"string","name":"operatorAddress","type":"string"},{"internalType":"bytes","name":"consensusPubkey","type":"bytes"},{"internalType":"bool","name":"jailed","type":"bool"},{"internalType":"int32","name":"status","type":"int32"},{"internalType":"string","name":"tokens","type":"string"},{"internalType":"string","name":"delegatorShares","type":"string"},{"internalType":"string","name":"description","type":"string"},{"internalType":"int64","name":"unbondingHeight","type":"int64"},{"internalType":"int64","name":"unbondingTime","type":"int64"},{"internalType":"string","name":"commissionRate","type":"string"},{"internalType":"string","name":"commissionMaxRate","type":"string"},{"internalType":"string","name":"commissionMaxChangeRate","type":"string"},{"internalType":"int64","name":"commissionUpdateTime","type":"int64"},{"internalType":"string","name":"minSelfDelegation","type":"string"}],"internalType":"struct IStaking.Validator","name":"validator","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"delegator","type":"address"},{"internalType":"bytes","name":"nextKey","type":"bytes"}],"name":"delegatorValidators","outputs":[{"components":[{"components":[{"internalType":"string","name":"operatorAddress","type":"string"},{"internalType":"bytes","name":"consensusPubkey","type":"bytes"},{"internalType":"bool","name":"jailed","type":"bool"},{"internalType":"int32","name":"status","type":"int32"},{"internalType":"string","name":"tokens","type":"string"},{"internalType":"string","name":"delegatorShares","type":"string"},{"internalType":"string","name":"description","type":"string"},{"internalType":"int64","name":"unbondingHeight","type":"int64"},{"internalType":"int64","name":"unbondingTime","type":"int64"},{"internalType":"string","name":"commissionRate","type":"string"},{"internalType":"string","name":"commissionMaxRate","type":"string"},{"internalType":"string","name":"commissionMaxChangeRate","type":"string"},{"internalType":"int64","name":"commissionUpdateTime","type":"int64"},{"internalType":"string","name":"minSelfDelegation","type":"string"}],"internalType":"struct IStaking.Validator[]","name":"validators","type":"tuple[]"},{"internalType":"bytes","name":"nextKey","type":"bytes"}],"internalType":"struct IStaking.ValidatorsResponse","name":"response","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"moniker","type":"string"},{"internalType":"string","name":"commissionRate","type":"string"},{"internalType":"uint256","name":"minSelfDelegation","type":"uint256"}],"name":"editValidator","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"int64","name":"height","type":"int64"}],"name":"historicalInfo","outputs":[{"components":[{"internalType":"int64","name":"height","type":"int64"},{"components":[{"internalType":"string","name":"operatorAddress","type":"string"},{"internalType":"bytes","name":"consensusPubkey","type":"bytes"},{"internalType":"bool","name":"jailed","type":"bool"},{"internalType":"int32","name":"status","type":"int32"},{"internalType":"string","name":"tokens","type":"string"},{"internalType":"string","name":"delegatorShares","type":"string"},{"internalType":"string","name":"description","type":"string"},{"internalType":"int64","name":"unbondingHeight","type":"int64"},{"internalType":"int64","name":"unbondingTime","type":"int64"},{"internalType":"string","name":"commissionRate","type":"string"},{"internalType":"string","name":"commissionMaxRate","type":"string"},{"internalType":"string","name":"commissionMaxChangeRate","type":"string"},{"internalType":"int64","name":"commissionUpdateTime","type":"int64"},{"internalType":"string","name":"minSelfDelegation","type":"string"}],"internalType":"struct IStaking.Validator[]","name":"validators","type":"tuple[]"}],"internalType":"struct IStaking.HistoricalInfo","name":"historicalInfo","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"params","outputs":[{"components":[{"internalType":"uint64","name":"unbondingTime","type":"uint64"},{"internalType":"uint32","name":"maxValidators","type":"uint32"},{"internalType":"uint32","name":"maxEntries","type":"uint32"},{"internalType":"uint32","name":"historicalEntries","type":"uint32"},{"internalType":"string","name":"bondDenom","type":"string"},{"internalType":"string","name":"minCommissionRate","type":"string"},{"internalType":"string","name":"maxVotingPowerRatio","type":"string"},{"internalType":"string","name":"maxVotingPowerEnforcementThreshold","type":"string"}],"internalType":"struct IStaking.Params","name":"params","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"pool","outputs":[{"components":[{"internalType":"string","name":"notBondedTokens","type":"string"},{"internalType":"string","name":"bondedTokens","type":"string"}],"internalType":"struct IStaking.Pool","name":"pool","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"srcAddress","type":"string"},{"internalType":"string","name":"dstAddress","type":"string"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"redelegate","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"delegator","type":"string"},{"internalType":"string","name":"srcValidator","type":"string"},{"internalType":"string","name":"dstValidator","type":"string"},{"internalType":"bytes","name":"nextKey","type":"bytes"}],"name":"redelegations","outputs":[{"components":[{"components":[{"internalType":"string","name":"delegatorAddress","type":"string"},{"internalType":"string","name":"validatorSrcAddress","type":"string"},{"internalType":"string","name":"validatorDstAddress","type":"string"},{"components":[{"internalType":"int64","name":"creationHeight","type":"int64"},{"internalType":"int64","name":"completionTime","type":"int64"},{"internalType":"string","name":"initialBalance","type":"string"},{"internalType":"string","name":"sharesDst","type":"string"}],"internalType":"struct IStaking.RedelegationEntry[]","name":"entries","type":"tuple[]"}],"internalType":"struct IStaking.Redelegation[]","name":"redelegations","type":"tuple[]"},{"internalType":"bytes","name":"nextKey","type":"bytes"}],"internalType":"struct IStaking.RedelegationsResponse","name":"response","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"delegator","type":"address"},{"internalType":"string","name":"validatorAddress","type":"string"}],"name":"unbondingDelegation","outputs":[{"components":[{"internalType":"string","name":"delegatorAddress","type":"string"},{"internalType":"string","name":"validatorAddress","type":"string"},{"components":[{"internalType":"int64","name":"creationHeight","type":"int64"},{"internalType":"int64","name":"completionTime","type":"int64"},{"internalType":"string","name":"initialBalance","type":"string"},{"internalType":"string","name":"balance","type":"string"}],"internalType":"struct IStaking.UnbondingDelegationEntry[]","name":"entries","type":"tuple[]"}],"internalType":"struct IStaking.UnbondingDelegation","name":"unbondingDelegation","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"valAddress","type":"string"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"undelegate","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"validatorAddress","type":"string"}],"name":"validator","outputs":[{"components":[{"internalType":"string","name":"operatorAddress","type":"string"},{"internalType":"bytes","name":"consensusPubkey","type":"bytes"},{"internalType":"bool","name":"jailed","type":"bool"},{"internalType":"int32","name":"status","type":"int32"},{"internalType":"string","name":"tokens","type":"string"},{"internalType":"string","name":"delegatorShares","type":"string"},{"internalType":"string","name":"description","type":"string"},{"internalType":"int64","name":"unbondingHeight","type":"int64"},{"internalType":"int64","name":"unbondingTime","type":"int64"},{"internalType":"string","name":"commissionRate","type":"string"},{"internalType":"string","name":"commissionMaxRate","type":"string"},{"internalType":"string","name":"commissionMaxChangeRate","type":"string"},{"internalType":"int64","name":"commissionUpdateTime","type":"int64"},{"internalType":"string","name":"minSelfDelegation","type":"string"}],"internalType":"struct IStaking.Validator","name":"validator","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"validatorAddress","type":"string"},{"internalType":"bytes","name":"nextKey","type":"bytes"}],"name":"validatorDelegations","outputs":[{"components":[{"components":[{"components":[{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"string","name":"denom","type":"string"}],"internalType":"struct IStaking.Balance","name":"balance","type":"tuple"},{"components":[{"internalType":"string","name":"delegator_address","type":"string"},{"internalType":"uint256","name":"shares","type":"uint256"},{"internalType":"uint256","name":"decimals","type":"uint256"},{"internalType":"string","name":"validator_address","type":"string"}],"internalType":"struct IStaking.DelegationDetails","name":"delegation","type":"tuple"}],"internalType":"struct IStaking.Delegation[]","name":"delegations","type":"tuple[]"},{"internalType":"bytes","name":"nextKey","type":"bytes"}],"internalType":"struct IStaking.DelegationsResponse","name":"response","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"validatorAddress","type":"string"},{"internalType":"bytes","name":"nextKey","type":"bytes"}],"name":"validatorUnbondingDelegations","outputs":[{"components":[{"components":[{"internalType":"string","name":"delegatorAddress","type":"string"},{"internalType":"string","name":"validatorAddress","type":"string"},{"components":[{"internalType":"int64","name":"creationHeight","type":"int64"},{"internalType":"int64","name":"completionTime","type":"int64"},{"internalType":"string","name":"initialBalance","type":"string"},{"internalType":"string","name":"balance","type":"string"}],"internalType":"struct IStaking.UnbondingDelegationEntry[]","name":"entries","type":"tuple[]"}],"internalType":"struct IStaking.UnbondingDelegation[]","name":"unbondingDelegations","type":"tuple[]"},{"internalType":"bytes","name":"nextKey","type":"bytes"}],"internalType":"struct IStaking.UnbondingDelegationsResponse","name":"response","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"status","type":"string"},{"internalType":"bytes","name":"nextKey","type":"bytes"}],"name":"validators","outputs":[{"components":[{"components":[{"internalType":"string","name":"operatorAddress","type":"string"},{"internalType":"bytes","name":"consensusPubkey","type":"bytes"},{"internalType":"bool","name":"jailed","type":"bool"},{"internalType":"int32","name":"status","type":"int32"},{"internalType":"string","name":"tokens","type":"string"},{"internalType":"string","name":"delegatorShares","type":"string"},{"internalType":"string","name":"description","type":"string"},{"internalType":"int64","name":"unbondingHeight","type":"int64"},{"internalType":"int64","name":"unbondingTime","type":"int64"},{"internalType":"string","name":"commissionRate","type":"string"},{"internalType":"string","name":"commissionMaxRate","type":"string"},{"internalType":"string","name":"commissionMaxChangeRate","type":"string"},{"internalType":"int64","name":"commissionUpdateTime","type":"int64"},{"internalType":"string","name":"minSelfDelegation","type":"string"}],"internalType":"struct IStaking.Validator[]","name":"validators","type":"tuple[]"},{"internalType":"bytes","name":"nextKey","type":"bytes"}],"internalType":"struct IStaking.ValidatorsResponse","name":"response","type":"tuple"}],"stateMutability":"view","type":"function"}] \ No newline at end of file diff --git a/giga/evmonly/precompiles/staking/balances.go b/giga/evmonly/precompiles/staking/balances.go new file mode 100644 index 0000000000..40a71c204a --- /dev/null +++ b/giga/evmonly/precompiles/staking/balances.go @@ -0,0 +1,52 @@ +package staking + +import ( + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + + "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles" +) + +const escrowAddressSeed = "sei/evmonly/staking/escrow/v1" + +var escrowAddress = common.BytesToAddress(crypto.Keccak256([]byte(escrowAddressSeed))[12:]) + +// EscrowAddress is the module-account-like address that holds bonded stake. +func EscrowAddress() common.Address { + return escrowAddress +} + +func transferPrecompileValueToEscrow(ctx *precompiles.Context) error { + return transferNativeValue(ctx, ctx.Address, escrowAddress, ctx.ApparentValue) +} + +func transferStakeFromEscrowToAddress(balances precompiles.BalanceTransfer, delegator string, amount *big.Int) error { + if !common.IsHexAddress(delegator) { + return fmt.Errorf("delegator address %q is not an EVM address", delegator) + } + return transferNativeValueWithBalances(balances, escrowAddress, common.HexToAddress(delegator), sweiFromUsei(amount)) +} + +func transferNativeValue(ctx *precompiles.Context, from common.Address, to common.Address, amount *big.Int) error { + return transferNativeValueWithBalances(ctx.Balances, from, to, amount) +} + +func transferNativeValueWithBalances(balances precompiles.BalanceTransfer, from common.Address, to common.Address, amount *big.Int) error { + if amount == nil || amount.Sign() == 0 { + return nil + } + if balances == nil { + return errMissingBalanceTransfer + } + return balances.Transfer(from, to, amount) +} + +func sweiFromUsei(amount *big.Int) *big.Int { + if amount == nil { + return new(big.Int) + } + return new(big.Int).Mul(amount, useiToSwei) +} diff --git a/giga/evmonly/precompiles/staking/endblock.go b/giga/evmonly/precompiles/staking/endblock.go new file mode 100644 index 0000000000..a55a1d0573 --- /dev/null +++ b/giga/evmonly/precompiles/staking/endblock.go @@ -0,0 +1,365 @@ +package staking + +import ( + "errors" + "math" + "math/big" + "sort" + + "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles" + "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles/util" +) + +// EndBlock runs the SDK-free staking end-block transition. +func (p *Precompile) EndBlock(ctx *precompiles.EndBlockContext) ([]precompiles.ValidatorUpdate, error) { + if ctx.Store == nil { + return nil, errMissingStore + } + updates, err := applyAndReturnValidatorSetUpdates(ctx.Store, ctx.Block) + if err != nil { + return nil, err + } + if err := unbondAllMatureValidators(ctx.Store, ctx.Block); err != nil { + return nil, err + } + if err := completeMatureUnbondings(ctx); err != nil { + return nil, err + } + if err := completeMatureRedelegations(ctx.Store, ctx.Block); err != nil { + return nil, err + } + return updates, nil +} + +func applyAndReturnValidatorSetUpdates(store precompiles.Store, block precompiles.BlockContext) ([]precompiles.ValidatorUpdate, error) { + params, err := loadParams(store) + if err != nil { + return nil, err + } + last, err := getLastValidatorPowers(store) + if err != nil { + return nil, err + } + candidates, err := validatorsByPower(store) + if err != nil { + return nil, err + } + maxValidators := int(params.MaxValidators) + if maxValidators < 0 { + maxValidators = 0 + } + if maxValidators > len(candidates) { + maxValidators = len(candidates) + } + + updates := make([]precompiles.ValidatorUpdate, 0) + totalPower := int64(0) + amtFromBondedToNotBonded := new(big.Int) + amtFromNotBondedToBonded := new(big.Int) + + for i := 0; i < maxValidators; i++ { + validator := candidates[i] + newPower, err := validatorPower(validator) + if err != nil { + return nil, err + } + if newPower == 0 { + break + } + tokens, err := util.ParseAmount(validator.Tokens) + if err != nil { + return nil, err + } + switch validator.Status { + case bondStatusUnbonded: + validator.Status = bondStatusBonded + amtFromNotBondedToBonded.Add(amtFromNotBondedToBonded, tokens) + case bondStatusUnbonding: + if err := deleteValidatorQueue(store, validator.UnbondingTime, validator.UnbondingHeight, validator.OperatorAddress); err != nil { + return nil, err + } + validator.Status = bondStatusBonded + validator.UnbondingHeight = 0 + validator.UnbondingTime = 0 + amtFromNotBondedToBonded.Add(amtFromNotBondedToBonded, tokens) + case bondStatusBonded: + default: + return nil, errors.New("unexpected validator status") + } + if err := setValidator(store, validator); err != nil { + return nil, err + } + + oldPower, found := last[validator.OperatorAddress] + if !found || oldPower != newPower { + updates = append(updates, validatorUpdate(validator, newPower)) + if err := setLastValidatorPower(store, validator.OperatorAddress, newPower); err != nil { + return nil, err + } + } + delete(last, validator.OperatorAddress) + if totalPower > math.MaxInt64-newPower { + return nil, errors.New("validator power overflow") + } + totalPower += newPower + } + + noLongerBonded := make([]string, 0, len(last)) + for validator := range last { + noLongerBonded = append(noLongerBonded, validator) + } + sort.Strings(noLongerBonded) + for _, validatorAddress := range noLongerBonded { + validator, ok, err := getValidator(store, validatorAddress) + if err != nil { + return nil, err + } + if !ok { + return nil, errValidatorMissing + } + tokens, err := util.ParseAmount(validator.Tokens) + if err != nil { + return nil, err + } + if validator.Status == bondStatusBonded { + validator.Status = bondStatusUnbonding + validator.UnbondingTime = util.SaturatingCompletionTime(block.Time, params.UnbondingTime) + validator.UnbondingHeight = int64(block.Number) //nolint:gosec // block heights fit signed ABI output in normal operation. + if err := setValidator(store, validator); err != nil { + return nil, err + } + if err := insertValidatorQueue(store, validator.UnbondingTime, validator.UnbondingHeight, validator.OperatorAddress); err != nil { + return nil, err + } + amtFromBondedToNotBonded.Add(amtFromBondedToNotBonded, tokens) + } + if err := deleteLastValidatorPower(store, validator.OperatorAddress); err != nil { + return nil, err + } + updates = append(updates, validatorUpdate(validator, 0)) + } + + if amtFromNotBondedToBonded.Cmp(amtFromBondedToNotBonded) > 0 { + delta := new(big.Int).Sub(amtFromNotBondedToBonded, amtFromBondedToNotBonded) + if err := addPoolNotBonded(store, new(big.Int).Neg(delta)); err != nil { + return nil, err + } + if err := addPoolBonded(store, delta); err != nil { + return nil, err + } + } else if amtFromBondedToNotBonded.Cmp(amtFromNotBondedToBonded) > 0 { + delta := new(big.Int).Sub(amtFromBondedToNotBonded, amtFromNotBondedToBonded) + if err := addPoolBonded(store, new(big.Int).Neg(delta)); err != nil { + return nil, err + } + if err := addPoolNotBonded(store, delta); err != nil { + return nil, err + } + } + + if len(updates) > 0 { + if err := setLastTotalPower(store, totalPower); err != nil { + return nil, err + } + } + return updates, nil +} + +func validatorsByPower(store precompiles.Store) ([]Validator, error) { + validatorAddresses, err := getStringList(store, validatorsIndexKey()) + if err != nil { + return nil, err + } + validators := make([]Validator, 0, len(validatorAddresses)) + for _, validatorAddress := range validatorAddresses { + validator, ok, err := getValidator(store, validatorAddress) + if err != nil { + return nil, err + } + if !ok || validator.Jailed { + continue + } + power, err := validatorPower(validator) + if err != nil { + return nil, err + } + if power == 0 { + continue + } + validators = append(validators, validator) + } + sort.SliceStable(validators, func(i, j int) bool { + left, _ := validatorPower(validators[i]) + right, _ := validatorPower(validators[j]) + if left != right { + return left > right + } + return validators[i].OperatorAddress < validators[j].OperatorAddress + }) + return validators, nil +} + +func validatorPower(validator Validator) (int64, error) { + tokens, err := util.ParseAmount(validator.Tokens) + if err != nil { + return 0, err + } + if powerReduction <= 0 { + return 0, errors.New("invalid power reduction") + } + power := new(big.Int).Quo(tokens, big.NewInt(powerReduction)) + if !power.IsInt64() { + return 0, errors.New("validator power exceeds int64") + } + return power.Int64(), nil +} + +func validatorUpdate(validator Validator, power int64) precompiles.ValidatorUpdate { + return precompiles.ValidatorUpdate{ + PubKey: append([]byte(nil), validator.ConsensusPubkey...), + Power: power, + } +} + +func unbondAllMatureValidators(store precompiles.Store, block precompiles.BlockContext) error { + ids, err := matureValidatorQueueIDs(store, block.Time, block.Number) + if err != nil { + return err + } + for _, id := range ids { + validators, err := getStringList(store, validatorQueueKey(id)) + if err != nil { + return err + } + for _, validatorAddress := range validators { + validator, ok, err := getValidator(store, validatorAddress) + if err != nil { + return err + } + if !ok { + return errValidatorMissing + } + if validator.Status != bondStatusUnbonding { + return errors.New("validator queue contains a validator that is not unbonding") + } + validator.Status = bondStatusUnbonded + if err := setValidator(store, validator); err != nil { + return err + } + shares, err := util.ParseAmount(validator.DelegatorShares) + if err != nil { + return err + } + if shares.Sign() == 0 { + if err := removeValidator(store, validator.OperatorAddress); err != nil { + return err + } + } + } + store.Delete(validatorQueueKey(id)) + if err := removeStringListItem(store, validatorQueueIndexKey(), id); err != nil { + return err + } + } + return nil +} + +func completeMatureUnbondings(ctx *precompiles.EndBlockContext) error { + ids, err := matureTimeQueueIDs(ctx.Store, unbondingQueueIndexKey(), ctx.Block.Time) + if err != nil { + return err + } + for _, id := range ids { + pairs, err := getDelegationPairList(ctx.Store, unbondingQueueKey(id)) + if err != nil { + return err + } + for _, pair := range pairs { + if err := completeUnbonding(ctx, pair); err != nil { + return err + } + } + ctx.Store.Delete(unbondingQueueKey(id)) + if err := removeStringListItem(ctx.Store, unbondingQueueIndexKey(), id); err != nil { + return err + } + } + return nil +} + +func completeUnbonding(ctx *precompiles.EndBlockContext, pair delegationPair) error { + record, ok, err := getUnbondingDelegation(ctx.Store, pair.DelegatorAddress, pair.ValidatorAddress) + if err != nil || !ok { + return err + } + remaining := record.Entries[:0] + for _, entry := range record.Entries { + if entry.CompletionTime > int64(ctx.Block.Time) { + remaining = append(remaining, entry) + continue + } + amount, err := util.ParseAmount(entry.Balance) + if err != nil { + return err + } + if amount.Sign() != 0 { + if err := transferStakeFromEscrowToAddress(ctx.Balances, record.DelegatorAddress, amount); err != nil { + return err + } + if err := addPoolNotBonded(ctx.Store, new(big.Int).Neg(amount)); err != nil { + return err + } + } + } + if len(remaining) == 0 { + ctx.Store.Delete(unbondingDelegationKey(pair.DelegatorAddress, pair.ValidatorAddress)) + if err := removeStringListItem(ctx.Store, delegatorUnbondingsIndexKey(pair.DelegatorAddress), pair.ValidatorAddress); err != nil { + return err + } + return removeStringListItem(ctx.Store, validatorUnbondingsIndexKey(pair.ValidatorAddress), pair.DelegatorAddress) + } + record.Entries = remaining + return util.SetJSON(ctx.Store, unbondingDelegationKey(pair.DelegatorAddress, pair.ValidatorAddress), record) +} + +func completeMatureRedelegations(store precompiles.Store, block precompiles.BlockContext) error { + ids, err := matureTimeQueueIDs(store, redelegationQueueIndexKey(), block.Time) + if err != nil { + return err + } + for _, id := range ids { + triplets, err := getRedelegationTripletList(store, redelegationQueueKey(id)) + if err != nil { + return err + } + for _, triplet := range triplets { + if err := completeRedelegation(store, triplet, block.Time); err != nil { + return err + } + } + store.Delete(redelegationQueueKey(id)) + if err := removeStringListItem(store, redelegationQueueIndexKey(), id); err != nil { + return err + } + } + return nil +} + +func completeRedelegation(store precompiles.Store, triplet redelegationTriplet, blockTime uint64) error { + record, ok, err := getRedelegation(store, triplet.DelegatorAddress, triplet.ValidatorSrcAddress, triplet.ValidatorDstAddress) + if err != nil || !ok { + return err + } + remaining := record.Entries[:0] + for _, entry := range record.Entries { + if entry.CompletionTime > int64(blockTime) { + remaining = append(remaining, entry) + } + } + if len(remaining) == 0 { + store.Delete(redelegationKey(triplet.DelegatorAddress, triplet.ValidatorSrcAddress, triplet.ValidatorDstAddress)) + return removeStringListItem(store, redelegationsIndexKey(), redelegationID(triplet.DelegatorAddress, triplet.ValidatorSrcAddress, triplet.ValidatorDstAddress)) + } + record.Entries = remaining + return util.SetJSON(store, redelegationKey(triplet.DelegatorAddress, triplet.ValidatorSrcAddress, triplet.ValidatorDstAddress), record) +} diff --git a/giga/evmonly/precompiles/staking/events.go b/giga/evmonly/precompiles/staking/events.go new file mode 100644 index 0000000000..0d98fa0cdf --- /dev/null +++ b/giga/evmonly/precompiles/staking/events.go @@ -0,0 +1,16 @@ +package staking + +import ( + "github.com/ethereum/go-ethereum/common" + + "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles" + "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles/util" +) + +func (p *Precompile) emit(ctx *precompiles.Context, name string, indexed common.Address, args ...interface{}) { + event, ok := p.abi.Events[name] + if !ok { + return + } + util.EmitEvent(ctx.Logs, p.address, event, indexed, args...) +} diff --git a/giga/evmonly/precompiles/staking/helpers.go b/giga/evmonly/precompiles/staking/helpers.go new file mode 100644 index 0000000000..3ca29e2260 --- /dev/null +++ b/giga/evmonly/precompiles/staking/helpers.go @@ -0,0 +1,63 @@ +package staking + +import ( + "errors" + "fmt" + "math/big" + "strconv" + "strings" + + "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles" +) + +func stakingValue(value *big.Int) (*big.Int, error) { + if value == nil || value.Sign() == 0 { + return nil, errors.New("set `value` field to non-zero to send delegate fund") + } + if value.Sign() < 0 { + return nil, errors.New("staking value cannot be negative") + } + usei, remainder := new(big.Int).QuoRem(value, useiToSwei, new(big.Int)) + if remainder.Sign() != 0 { + return nil, fmt.Errorf("selected precompile function does not allow payment with non-zero wei remainder: received %s", value) + } + if usei.Sign() == 0 { + return nil, errors.New("staking value is below one usei") + } + return usei, nil +} + +func validateWritable(ctx *precompiles.Context) error { + if ctx.ReadOnly { + return errReadOnly + } + return nil +} + +func statusMatches(filter string, status int32) bool { + if filter == "" { + return true + } + switch strings.ToUpper(filter) { + case "BOND_STATUS_UNSPECIFIED": + return status == 0 + case "BOND_STATUS_UNBONDED": + return status == 1 + case "BOND_STATUS_UNBONDING": + return status == 2 + case "BOND_STATUS_BONDED": + return status == 3 + default: + parsed, err := strconv.ParseInt(filter, 10, 32) + return err == nil && int32(parsed) == status //nolint:gosec // parsed is limited to 32 bits. + } +} + +func isTransaction(method string) bool { + switch method { + case DelegateMethod, RedelegateMethod, UndelegateMethod, CreateValidatorMethod, EditValidatorMethod: + return true + default: + return false + } +} diff --git a/giga/evmonly/precompiles/staking/staking.go b/giga/evmonly/precompiles/staking/staking.go new file mode 100644 index 0000000000..c3e42b5bdd --- /dev/null +++ b/giga/evmonly/precompiles/staking/staking.go @@ -0,0 +1,872 @@ +package staking + +import ( + "bytes" + "embed" + "encoding/hex" + "errors" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + + "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles" + "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles/util" +) + +const ( + DelegateMethod = "delegate" + RedelegateMethod = "redelegate" + UndelegateMethod = "undelegate" + DelegationMethod = "delegation" + CreateValidatorMethod = "createValidator" + EditValidatorMethod = "editValidator" + ValidatorsMethod = "validators" + ValidatorMethod = "validator" + ValidatorDelegationsMethod = "validatorDelegations" + ValidatorUnbondingDelegationsMethod = "validatorUnbondingDelegations" + UnbondingDelegationMethod = "unbondingDelegation" + DelegatorDelegationsMethod = "delegatorDelegations" + DelegatorValidatorMethod = "delegatorValidator" + DelegatorUnbondingDelegationsMethod = "delegatorUnbondingDelegations" + RedelegationsMethod = "redelegations" + DelegatorValidatorsMethod = "delegatorValidators" + HistoricalInfoMethod = "historicalInfo" + PoolMethod = "pool" + ParamsMethod = "params" +) + +const ( + StakingAddress = "0x0000000000000000000000000000000000001005" + + unknownMethodGas uint64 = 3000 + readGas uint64 = 3000 + writeGas uint64 = 20000 + inputByteGas uint64 = 16 + + bondDenom = "usei" + precision int64 = 18 + pageLimit = 100 +) + +const ( + bondStatusUnspecified int32 = 0 + bondStatusUnbonded int32 = 1 + bondStatusUnbonding int32 = 2 + bondStatusBonded int32 = 3 + + powerReduction int64 = 1 +) + +var ( + address = common.HexToAddress(StakingAddress) + useiToSwei = big.NewInt(1_000_000_000_000) + errReadOnly = errors.New("cannot call staking precompile from staticcall") + errDelegateCall = errors.New("cannot delegatecall staking") + errMissingStore = errors.New("staking precompile requires a store") + errMissingBalanceTransfer = errors.New("staking precompile requires balance transfer") + errValidatorMissing = errors.New("validator not found") +) + +//go:embed abi.json +var abiFS embed.FS + +// Precompile is the SDK-free staking custom precompile for the evm-only path. +type Precompile struct { + abi abi.ABI + address common.Address +} + +// Registry exposes only the staking precompile to the evm-only executor. +type Registry struct { + contract *Precompile +} + +// NewPrecompile constructs the staking precompile without Cosmos keepers or +// sdk.Context dependencies. +func NewPrecompile() (*Precompile, error) { + abiBz, err := abiFS.ReadFile("abi.json") + if err != nil { + return nil, err + } + parsedABI, err := abi.JSON(bytes.NewReader(abiBz)) + if err != nil { + return nil, err + } + return &Precompile{abi: parsedABI, address: address}, nil +} + +// NewRegistry returns a registry containing the staking precompile. +func NewRegistry() (Registry, error) { + contract, err := NewPrecompile() + if err != nil { + return Registry{}, err + } + return Registry{contract: contract}, nil +} + +func (r Registry) Get(addr common.Address) (precompiles.Contract, bool) { + if addr != address || r.contract == nil { + return nil, false + } + return r.contract, true +} + +func (r Registry) Addresses() []common.Address { + return []common.Address{address} +} + +func (p *Precompile) Address() common.Address { + return p.address +} + +func (p *Precompile) ABI() abi.ABI { + return p.abi +} + +func (p *Precompile) RequiredGas(input []byte) uint64 { + method, _, err := p.prepare(input) + if err != nil { + return unknownMethodGas + } + gas := readGas + if isTransaction(method.Name) { + gas = writeGas + } + return gas + inputByteGas*uint64(len(input)) //nolint:gosec // input length is bounded by memory. +} + +func (p *Precompile) Run(ctx *precompiles.Context, input []byte) ([]byte, error) { + if ctx.DelegateCall { + return nil, errDelegateCall + } + if ctx.Store == nil { + return nil, errMissingStore + } + method, args, err := p.prepare(input) + if err != nil { + return nil, err + } + switch method.Name { + case DelegateMethod: + return p.delegate(ctx, method, args) + case RedelegateMethod: + return p.redelegate(ctx, method, args) + case UndelegateMethod: + return p.undelegate(ctx, method, args) + case CreateValidatorMethod: + return p.createValidator(ctx, method, args) + case EditValidatorMethod: + return p.editValidator(ctx, method, args) + case DelegationMethod: + return p.delegation(ctx, method, args) + case ValidatorsMethod: + return p.validators(ctx, method, args) + case ValidatorMethod: + return p.validator(ctx, method, args) + case ValidatorDelegationsMethod: + return p.validatorDelegations(ctx, method, args) + case ValidatorUnbondingDelegationsMethod: + return p.validatorUnbondingDelegations(ctx, method, args) + case UnbondingDelegationMethod: + return p.unbondingDelegation(ctx, method, args) + case DelegatorDelegationsMethod: + return p.delegatorDelegations(ctx, method, args) + case DelegatorValidatorMethod: + return p.delegatorValidator(ctx, method, args) + case DelegatorUnbondingDelegationsMethod: + return p.delegatorUnbondingDelegations(ctx, method, args) + case RedelegationsMethod: + return p.redelegations(ctx, method, args) + case DelegatorValidatorsMethod: + return p.delegatorValidators(ctx, method, args) + case HistoricalInfoMethod: + return p.historicalInfo(ctx, method, args) + case PoolMethod: + return p.pool(ctx, method) + case ParamsMethod: + return p.params(ctx, method) + default: + return nil, fmt.Errorf("unsupported staking method %s", method.Name) + } +} + +func (p *Precompile) prepare(input []byte) (*abi.Method, []interface{}, error) { + if len(input) < 4 { + return nil, nil, errors.New("input too short to extract method ID") + } + method, err := p.abi.MethodById(input[:4]) + if err != nil { + return nil, nil, err + } + args, err := method.Inputs.Unpack(input[4:]) + if err != nil { + return nil, nil, err + } + return method, args, nil +} + +func (p *Precompile) delegate(ctx *precompiles.Context, method *abi.Method, args []interface{}) ([]byte, error) { + if err := validateWritable(ctx); err != nil { + return nil, err + } + if err := util.ValidateArgsLength(args, 1); err != nil { + return nil, err + } + validatorAddress := args[0].(string) + validator, ok, err := getValidator(ctx.Store, validatorAddress) + if err != nil { + return nil, err + } + if !ok { + return nil, errValidatorMissing + } + useiAmount, err := stakingValue(ctx.ApparentValue) + if err != nil { + return nil, err + } + if err := transferPrecompileValueToEscrow(ctx); err != nil { + return nil, err + } + delegator := util.AddressString(ctx.Caller) + if err := addDelegation(ctx.Store, delegator, validatorAddress, useiAmount); err != nil { + return nil, err + } + if err := addValidatorTokens(ctx.Store, validatorAddress, useiAmount); err != nil { + return nil, err + } + if validator.Status == bondStatusBonded { + if err := addPoolBonded(ctx.Store, useiAmount); err != nil { + return nil, err + } + } else if err := addPoolNotBonded(ctx.Store, useiAmount); err != nil { + return nil, err + } + p.emit(ctx, "Delegate", ctx.Caller, validatorAddress, util.CloneBig(ctx.ApparentValue)) + p.emit(ctx, "DelegationRewardsWithdrawn", ctx.Caller, validatorAddress, new(big.Int)) + return method.Outputs.Pack(true) +} + +func (p *Precompile) redelegate(ctx *precompiles.Context, method *abi.Method, args []interface{}) ([]byte, error) { + if err := validateWritable(ctx); err != nil { + return nil, err + } + if err := util.ValidateNonPayable(ctx.ApparentValue); err != nil { + return nil, err + } + if err := util.ValidateArgsLength(args, 3); err != nil { + return nil, err + } + delegator := util.AddressString(ctx.Caller) + srcValidator := args[0].(string) + dstValidator := args[1].(string) + amount := args[2].(*big.Int) + if err := util.ValidatePositiveAmount(amount, "redelegation amount"); err != nil { + return nil, err + } + src, ok, err := getValidator(ctx.Store, srcValidator) + if err != nil { + return nil, err + } + if !ok { + return nil, fmt.Errorf("source %w", errValidatorMissing) + } + dst, ok, err := getValidator(ctx.Store, dstValidator) + if err != nil { + return nil, err + } + if !ok { + return nil, fmt.Errorf("destination %w", errValidatorMissing) + } + if err := validateDelegationAmount(ctx.Store, delegator, srcValidator, amount); err != nil { + return nil, err + } + if err := addDelegation(ctx.Store, delegator, srcValidator, new(big.Int).Neg(amount)); err != nil { + return nil, err + } + if err := addDelegation(ctx.Store, delegator, dstValidator, amount); err != nil { + return nil, err + } + if err := addValidatorTokens(ctx.Store, srcValidator, new(big.Int).Neg(amount)); err != nil { + return nil, err + } + if err := addValidatorTokens(ctx.Store, dstValidator, amount); err != nil { + return nil, err + } + if err := movePoolsForRedelegation(ctx.Store, src.Status, dst.Status, amount); err != nil { + return nil, err + } + params, err := loadParams(ctx.Store) + if err != nil { + return nil, err + } + if err := addRedelegation(ctx.Store, delegator, srcValidator, dstValidator, amount, util.SaturatingCompletionTime(ctx.Block.Time, params.UnbondingTime)); err != nil { + return nil, err + } + p.emit(ctx, "Redelegate", ctx.Caller, srcValidator, dstValidator, amount) + p.emit(ctx, "DelegationRewardsWithdrawn", ctx.Caller, srcValidator, new(big.Int)) + p.emit(ctx, "DelegationRewardsWithdrawn", ctx.Caller, dstValidator, new(big.Int)) + return method.Outputs.Pack(true) +} + +func (p *Precompile) undelegate(ctx *precompiles.Context, method *abi.Method, args []interface{}) ([]byte, error) { + if err := validateWritable(ctx); err != nil { + return nil, err + } + if err := util.ValidateNonPayable(ctx.ApparentValue); err != nil { + return nil, err + } + if err := util.ValidateArgsLength(args, 2); err != nil { + return nil, err + } + delegator := util.AddressString(ctx.Caller) + validatorAddress := args[0].(string) + amount := args[1].(*big.Int) + if err := util.ValidatePositiveAmount(amount, "undelegation amount"); err != nil { + return nil, err + } + validator, ok, err := getValidator(ctx.Store, validatorAddress) + if err != nil { + return nil, err + } + if !ok { + return nil, errValidatorMissing + } + if err := validateDelegationAmount(ctx.Store, delegator, validatorAddress, amount); err != nil { + return nil, err + } + if err := addDelegation(ctx.Store, delegator, validatorAddress, new(big.Int).Neg(amount)); err != nil { + return nil, err + } + if err := addValidatorTokens(ctx.Store, validatorAddress, new(big.Int).Neg(amount)); err != nil { + return nil, err + } + params, err := loadParams(ctx.Store) + if err != nil { + return nil, err + } + if err := addUnbondingDelegation(ctx.Store, delegator, validatorAddress, amount, ctx.Block.Number, util.SaturatingCompletionTime(ctx.Block.Time, params.UnbondingTime)); err != nil { + return nil, err + } + if validator.Status == bondStatusBonded { + if err := addPoolBonded(ctx.Store, new(big.Int).Neg(amount)); err != nil { + return nil, err + } + if err := addPoolNotBonded(ctx.Store, amount); err != nil { + return nil, err + } + } + p.emit(ctx, "Undelegate", ctx.Caller, validatorAddress, amount) + p.emit(ctx, "DelegationRewardsWithdrawn", ctx.Caller, validatorAddress, new(big.Int)) + return method.Outputs.Pack(true) +} + +func (p *Precompile) createValidator(ctx *precompiles.Context, method *abi.Method, args []interface{}) ([]byte, error) { + if err := validateWritable(ctx); err != nil { + return nil, err + } + if err := util.ValidateArgsLength(args, 6); err != nil { + return nil, err + } + pubKeyHex := args[0].(string) + moniker := args[1].(string) + commissionRate := args[2].(string) + commissionMaxRate := args[3].(string) + commissionMaxChangeRate := args[4].(string) + minSelfDelegation := args[5].(*big.Int) + pubKey, err := hex.DecodeString(pubKeyHex) + if err != nil { + return nil, errors.New("invalid public key hex format") + } + if err := util.ValidateDecimal(commissionRate, "commission rate"); err != nil { + return nil, err + } + if err := util.ValidateDecimal(commissionMaxRate, "commission max rate"); err != nil { + return nil, err + } + if err := util.ValidateDecimal(commissionMaxChangeRate, "commission max change rate"); err != nil { + return nil, err + } + if err := util.ValidatePositiveAmount(minSelfDelegation, "minimum self delegation"); err != nil { + return nil, errors.New("minimum self delegation must be a positive integer: invalid request") + } + selfDelegation, err := stakingValue(ctx.ApparentValue) + if err != nil { + return nil, err + } + if selfDelegation.Cmp(minSelfDelegation) < 0 { + return nil, errors.New("self delegation is below minimum self delegation") + } + validatorAddress := util.AddressString(ctx.Caller) + if _, exists, err := getValidator(ctx.Store, validatorAddress); err != nil { + return nil, err + } else if exists { + return nil, errors.New("validator already exists") + } + if err := transferPrecompileValueToEscrow(ctx); err != nil { + return nil, err + } + validator := Validator{ + OperatorAddress: validatorAddress, + ConsensusPubkey: pubKey, + Jailed: false, + Status: bondStatusUnbonded, + Tokens: selfDelegation.String(), + DelegatorShares: selfDelegation.String(), + Description: moniker, + UnbondingHeight: 0, + UnbondingTime: 0, + CommissionRate: commissionRate, + CommissionMaxRate: commissionMaxRate, + CommissionMaxChangeRate: commissionMaxChangeRate, + CommissionUpdateTime: int64(ctx.Block.Time), //nolint:gosec // block times fit signed ABI output in normal operation. + MinSelfDelegation: minSelfDelegation.String(), + } + if err := setValidator(ctx.Store, validator); err != nil { + return nil, err + } + if err := addDelegation(ctx.Store, validatorAddress, validatorAddress, selfDelegation); err != nil { + return nil, err + } + if err := addPoolNotBonded(ctx.Store, selfDelegation); err != nil { + return nil, err + } + if err := setHistoricalInfo(ctx.Store, ctx.Block.Number); err != nil { + return nil, err + } + p.emit(ctx, "ValidatorCreated", ctx.Caller, validatorAddress, moniker) + return method.Outputs.Pack(true) +} + +func (p *Precompile) editValidator(ctx *precompiles.Context, method *abi.Method, args []interface{}) ([]byte, error) { + if err := validateWritable(ctx); err != nil { + return nil, err + } + if err := util.ValidateNonPayable(ctx.ApparentValue); err != nil { + return nil, err + } + if err := util.ValidateArgsLength(args, 3); err != nil { + return nil, err + } + validatorAddress := util.AddressString(ctx.Caller) + validator, ok, err := getValidator(ctx.Store, validatorAddress) + if err != nil { + return nil, err + } + if !ok { + return nil, errValidatorMissing + } + moniker := args[0].(string) + commissionRate := args[1].(string) + minSelfDelegation := args[2].(*big.Int) + if moniker != "" { + validator.Description = moniker + } + if commissionRate != "" { + if err := util.ValidateDecimal(commissionRate, "commission rate"); err != nil { + return nil, err + } + validator.CommissionRate = commissionRate + validator.CommissionUpdateTime = int64(ctx.Block.Time) //nolint:gosec // block times fit signed ABI output in normal operation. + } + if minSelfDelegation != nil && minSelfDelegation.Sign() > 0 { + validator.MinSelfDelegation = minSelfDelegation.String() + } + if err := setValidator(ctx.Store, validator); err != nil { + return nil, err + } + if err := setHistoricalInfo(ctx.Store, ctx.Block.Number); err != nil { + return nil, err + } + p.emit(ctx, "ValidatorEdited", ctx.Caller, validatorAddress, moniker) + return method.Outputs.Pack(true) +} + +func (p *Precompile) delegation(ctx *precompiles.Context, method *abi.Method, args []interface{}) ([]byte, error) { + if err := util.ValidateNonPayable(ctx.ApparentValue); err != nil { + return nil, err + } + if err := util.ValidateArgsLength(args, 2); err != nil { + return nil, err + } + delegator := util.AddressString(args[0].(common.Address)) + validatorAddress := args[1].(string) + record, ok, err := getDelegation(ctx.Store, delegator, validatorAddress) + if err != nil { + return nil, err + } + if !ok { + return nil, errors.New("delegation not found") + } + delegation, err := delegationFromRecord(record) + if err != nil { + return nil, err + } + return method.Outputs.Pack(delegation) +} + +func (p *Precompile) validators(ctx *precompiles.Context, method *abi.Method, args []interface{}) ([]byte, error) { + if err := util.ValidateNonPayable(ctx.ApparentValue); err != nil { + return nil, err + } + if err := util.ValidateArgsLength(args, 2); err != nil { + return nil, err + } + status := args[0].(string) + nextKey := args[1].([]byte) + validatorAddresses, err := getStringList(ctx.Store, validatorsIndexKey()) + if err != nil { + return nil, err + } + filtered := make([]string, 0, len(validatorAddresses)) + for _, validatorAddress := range validatorAddresses { + validator, ok, err := getValidator(ctx.Store, validatorAddress) + if err != nil { + return nil, err + } + if ok && statusMatches(status, validator.Status) { + filtered = append(filtered, validatorAddress) + } + } + page, outNextKey, err := pageStrings(filtered, nextKey) + if err != nil { + return nil, err + } + result := ValidatorsResponse{Validators: make([]Validator, 0, len(page)), NextKey: outNextKey} + for _, validatorAddress := range page { + validator, ok, err := getValidator(ctx.Store, validatorAddress) + if err != nil { + return nil, err + } + if ok { + result.Validators = append(result.Validators, validator) + } + } + return method.Outputs.Pack(result) +} + +func (p *Precompile) validator(ctx *precompiles.Context, method *abi.Method, args []interface{}) ([]byte, error) { + if err := util.ValidateNonPayable(ctx.ApparentValue); err != nil { + return nil, err + } + if err := util.ValidateArgsLength(args, 1); err != nil { + return nil, err + } + validator, ok, err := getValidator(ctx.Store, args[0].(string)) + if err != nil { + return nil, err + } + if !ok { + return nil, errValidatorMissing + } + return method.Outputs.Pack(validator) +} + +func (p *Precompile) validatorDelegations(ctx *precompiles.Context, method *abi.Method, args []interface{}) ([]byte, error) { + if err := util.ValidateNonPayable(ctx.ApparentValue); err != nil { + return nil, err + } + if err := util.ValidateArgsLength(args, 2); err != nil { + return nil, err + } + validatorAddress := args[0].(string) + nextKey := args[1].([]byte) + delegators, err := getStringList(ctx.Store, validatorDelegationsIndexKey(validatorAddress)) + if err != nil { + return nil, err + } + page, outNextKey, err := pageStrings(delegators, nextKey) + if err != nil { + return nil, err + } + result := DelegationsResponse{Delegations: make([]Delegation, 0, len(page)), NextKey: outNextKey} + for _, delegator := range page { + record, ok, err := getDelegation(ctx.Store, delegator, validatorAddress) + if err != nil { + return nil, err + } + if !ok { + continue + } + delegation, err := delegationFromRecord(record) + if err != nil { + return nil, err + } + result.Delegations = append(result.Delegations, delegation) + } + return method.Outputs.Pack(result) +} + +func (p *Precompile) validatorUnbondingDelegations(ctx *precompiles.Context, method *abi.Method, args []interface{}) ([]byte, error) { + if err := util.ValidateNonPayable(ctx.ApparentValue); err != nil { + return nil, err + } + if err := util.ValidateArgsLength(args, 2); err != nil { + return nil, err + } + validatorAddress := args[0].(string) + nextKey := args[1].([]byte) + delegators, err := getStringList(ctx.Store, validatorUnbondingsIndexKey(validatorAddress)) + if err != nil { + return nil, err + } + page, outNextKey, err := pageStrings(delegators, nextKey) + if err != nil { + return nil, err + } + result := UnbondingDelegationsResponse{UnbondingDelegations: make([]UnbondingDelegation, 0, len(page)), NextKey: outNextKey} + for _, delegator := range page { + record, ok, err := getUnbondingDelegation(ctx.Store, delegator, validatorAddress) + if err != nil { + return nil, err + } + if ok { + result.UnbondingDelegations = append(result.UnbondingDelegations, record) + } + } + return method.Outputs.Pack(result) +} + +func (p *Precompile) unbondingDelegation(ctx *precompiles.Context, method *abi.Method, args []interface{}) ([]byte, error) { + if err := util.ValidateNonPayable(ctx.ApparentValue); err != nil { + return nil, err + } + if err := util.ValidateArgsLength(args, 2); err != nil { + return nil, err + } + delegator := util.AddressString(args[0].(common.Address)) + validatorAddress := args[1].(string) + record, ok, err := getUnbondingDelegation(ctx.Store, delegator, validatorAddress) + if err != nil { + return nil, err + } + if !ok { + return nil, errors.New("unbonding delegation not found") + } + return method.Outputs.Pack(record) +} + +func (p *Precompile) delegatorDelegations(ctx *precompiles.Context, method *abi.Method, args []interface{}) ([]byte, error) { + if err := util.ValidateNonPayable(ctx.ApparentValue); err != nil { + return nil, err + } + if err := util.ValidateArgsLength(args, 2); err != nil { + return nil, err + } + delegator := util.AddressString(args[0].(common.Address)) + nextKey := args[1].([]byte) + validators, err := getStringList(ctx.Store, delegatorDelegationsIndexKey(delegator)) + if err != nil { + return nil, err + } + page, outNextKey, err := pageStrings(validators, nextKey) + if err != nil { + return nil, err + } + result := DelegationsResponse{Delegations: make([]Delegation, 0, len(page)), NextKey: outNextKey} + for _, validatorAddress := range page { + record, ok, err := getDelegation(ctx.Store, delegator, validatorAddress) + if err != nil { + return nil, err + } + if !ok { + continue + } + delegation, err := delegationFromRecord(record) + if err != nil { + return nil, err + } + result.Delegations = append(result.Delegations, delegation) + } + return method.Outputs.Pack(result) +} + +func (p *Precompile) delegatorValidator(ctx *precompiles.Context, method *abi.Method, args []interface{}) ([]byte, error) { + if err := util.ValidateNonPayable(ctx.ApparentValue); err != nil { + return nil, err + } + if err := util.ValidateArgsLength(args, 2); err != nil { + return nil, err + } + delegator := util.AddressString(args[0].(common.Address)) + validatorAddress := args[1].(string) + if _, ok, err := getDelegation(ctx.Store, delegator, validatorAddress); err != nil { + return nil, err + } else if !ok { + return nil, errors.New("delegation not found") + } + validator, ok, err := getValidator(ctx.Store, validatorAddress) + if err != nil { + return nil, err + } + if !ok { + return nil, errValidatorMissing + } + return method.Outputs.Pack(validator) +} + +func (p *Precompile) delegatorUnbondingDelegations(ctx *precompiles.Context, method *abi.Method, args []interface{}) ([]byte, error) { + if err := util.ValidateNonPayable(ctx.ApparentValue); err != nil { + return nil, err + } + if err := util.ValidateArgsLength(args, 2); err != nil { + return nil, err + } + delegator := util.AddressString(args[0].(common.Address)) + nextKey := args[1].([]byte) + validators, err := getStringList(ctx.Store, delegatorUnbondingsIndexKey(delegator)) + if err != nil { + return nil, err + } + page, outNextKey, err := pageStrings(validators, nextKey) + if err != nil { + return nil, err + } + result := UnbondingDelegationsResponse{UnbondingDelegations: make([]UnbondingDelegation, 0, len(page)), NextKey: outNextKey} + for _, validatorAddress := range page { + record, ok, err := getUnbondingDelegation(ctx.Store, delegator, validatorAddress) + if err != nil { + return nil, err + } + if ok { + result.UnbondingDelegations = append(result.UnbondingDelegations, record) + } + } + return method.Outputs.Pack(result) +} + +func (p *Precompile) redelegations(ctx *precompiles.Context, method *abi.Method, args []interface{}) ([]byte, error) { + if err := util.ValidateNonPayable(ctx.ApparentValue); err != nil { + return nil, err + } + if err := util.ValidateArgsLength(args, 4); err != nil { + return nil, err + } + delegatorFilter := args[0].(string) + srcFilter := args[1].(string) + dstFilter := args[2].(string) + nextKey := args[3].([]byte) + ids, err := getStringList(ctx.Store, redelegationsIndexKey()) + if err != nil { + return nil, err + } + filtered := make([]string, 0, len(ids)) + for _, id := range ids { + delegator, src, dst, ok := splitRedelegationID(id) + if !ok { + continue + } + if delegatorFilter != "" && delegatorFilter != delegator { + continue + } + if srcFilter != "" && srcFilter != src { + continue + } + if dstFilter != "" && dstFilter != dst { + continue + } + filtered = append(filtered, id) + } + page, outNextKey, err := pageStrings(filtered, nextKey) + if err != nil { + return nil, err + } + result := RedelegationsResponse{Redelegations: make([]Redelegation, 0, len(page)), NextKey: outNextKey} + for _, id := range page { + delegator, src, dst, ok := splitRedelegationID(id) + if !ok { + continue + } + record, ok, err := getRedelegation(ctx.Store, delegator, src, dst) + if err != nil { + return nil, err + } + if ok { + result.Redelegations = append(result.Redelegations, record) + } + } + return method.Outputs.Pack(result) +} + +func (p *Precompile) delegatorValidators(ctx *precompiles.Context, method *abi.Method, args []interface{}) ([]byte, error) { + if err := util.ValidateNonPayable(ctx.ApparentValue); err != nil { + return nil, err + } + if err := util.ValidateArgsLength(args, 2); err != nil { + return nil, err + } + delegator := util.AddressString(args[0].(common.Address)) + nextKey := args[1].([]byte) + validators, err := getStringList(ctx.Store, delegatorDelegationsIndexKey(delegator)) + if err != nil { + return nil, err + } + page, outNextKey, err := pageStrings(validators, nextKey) + if err != nil { + return nil, err + } + result := ValidatorsResponse{Validators: make([]Validator, 0, len(page)), NextKey: outNextKey} + for _, validatorAddress := range page { + validator, ok, err := getValidator(ctx.Store, validatorAddress) + if err != nil { + return nil, err + } + if ok { + result.Validators = append(result.Validators, validator) + } + } + return method.Outputs.Pack(result) +} + +func (p *Precompile) historicalInfo(ctx *precompiles.Context, method *abi.Method, args []interface{}) ([]byte, error) { + if err := util.ValidateNonPayable(ctx.ApparentValue); err != nil { + return nil, err + } + if err := util.ValidateArgsLength(args, 1); err != nil { + return nil, err + } + height := args[0].(int64) + info, ok, err := getHistoricalInfo(ctx.Store, height) + if err != nil { + return nil, err + } + if !ok { + if height < 0 || uint64(height) != ctx.Block.Number { + return nil, errors.New("historical info not found") + } + if err := setHistoricalInfo(ctx.Store, ctx.Block.Number); err != nil { + return nil, err + } + info, ok, err = getHistoricalInfo(ctx.Store, height) + if err != nil { + return nil, err + } + if !ok { + return nil, errors.New("historical info not found") + } + } + return method.Outputs.Pack(info) +} + +func (p *Precompile) pool(ctx *precompiles.Context, method *abi.Method) ([]byte, error) { + if err := util.ValidateNonPayable(ctx.ApparentValue); err != nil { + return nil, err + } + pool, err := loadPool(ctx.Store) + if err != nil { + return nil, err + } + return method.Outputs.Pack(pool) +} + +func (p *Precompile) params(ctx *precompiles.Context, method *abi.Method) ([]byte, error) { + if err := util.ValidateNonPayable(ctx.ApparentValue); err != nil { + return nil, err + } + params, err := loadParams(ctx.Store) + if err != nil { + return nil, err + } + return method.Outputs.Pack(params) +} diff --git a/giga/evmonly/precompiles/staking/staking_test.go b/giga/evmonly/precompiles/staking/staking_test.go new file mode 100644 index 0000000000..321a5dbbb9 --- /dev/null +++ b/giga/evmonly/precompiles/staking/staking_test.go @@ -0,0 +1,295 @@ +package staking + +import ( + "errors" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/stretchr/testify/require" + + "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles" +) + +func TestPrecompileCreateDelegateAndQuery(t *testing.T) { + p, err := NewPrecompile() + require.NoError(t, err) + + caller := common.HexToAddress("0x0000000000000000000000000000000000000abc") + store := newMemoryStore() + logs := &memoryLogs{} + balances := newMemoryBalances() + ctx := &precompiles.Context{ + Caller: caller, + Address: address, + ApparentValue: new(big.Int).Mul(big.NewInt(5), useiToSwei), + Block: precompiles.BlockContext{Number: 7, Time: 100}, + Store: store, + Balances: balances, + Logs: logs, + } + + input, err := p.abi.Pack( + CreateValidatorMethod, + "01020304", + "validator-one", + "0.100000000000000000", + "0.200000000000000000", + "0.010000000000000000", + big.NewInt(1), + ) + require.NoError(t, err) + balances.add(address, ctx.ApparentValue) + ret, err := p.Run(ctx, input) + require.NoError(t, err) + requireBoolReturn(t, p, CreateValidatorMethod, ret, true) + require.Len(t, logs.logs, 1) + require.Equal(t, new(big.Int).Mul(big.NewInt(5), useiToSwei), balances.balance(EscrowAddress())) + require.Zero(t, balances.balance(caller).Sign()) + require.Zero(t, balances.balance(address).Sign()) + + ctx.ApparentValue = new(big.Int).Mul(big.NewInt(2), useiToSwei) + input, err = p.abi.Pack(DelegateMethod, caller.Hex()) + require.NoError(t, err) + balances.add(address, ctx.ApparentValue) + ret, err = p.Run(ctx, input) + require.NoError(t, err) + requireBoolReturn(t, p, DelegateMethod, ret, true) + require.Len(t, logs.logs, 3) + require.Equal(t, new(big.Int).Mul(big.NewInt(7), useiToSwei), balances.balance(EscrowAddress())) + require.Zero(t, balances.balance(caller).Sign()) + require.Zero(t, balances.balance(address).Sign()) + + updates, err := p.EndBlock(&precompiles.EndBlockContext{ + Address: address, + Block: ctx.Block, + Store: store, + Balances: balances, + Logs: logs, + }) + require.NoError(t, err) + require.Len(t, updates, 1) + + ctx.ApparentValue = nil + input, err = p.abi.Pack(DelegationMethod, caller, caller.Hex()) + require.NoError(t, err) + ret, err = p.Run(ctx, input) + require.NoError(t, err) + var delegationOut struct { + Delegation Delegation + } + require.NoError(t, p.abi.UnpackIntoInterface(&delegationOut, DelegationMethod, ret)) + delegation := delegationOut.Delegation + require.Equal(t, big.NewInt(7), delegation.Balance.Amount) + require.Equal(t, "usei", delegation.Balance.Denom) + require.Equal(t, caller.Hex(), delegation.Delegation.DelegatorAddress) + require.Equal(t, caller.Hex(), delegation.Delegation.ValidatorAddress) + + input, err = p.abi.Pack(PoolMethod) + require.NoError(t, err) + ret, err = p.Run(ctx, input) + require.NoError(t, err) + var poolOut struct { + Pool Pool + } + require.NoError(t, p.abi.UnpackIntoInterface(&poolOut, PoolMethod, ret)) + pool := poolOut.Pool + require.Equal(t, "7", pool.BondedTokens) + require.Equal(t, "0", pool.NotBondedTokens) + + input, err = p.abi.Pack(ValidatorsMethod, "BOND_STATUS_BONDED", []byte{}) + require.NoError(t, err) + ret, err = p.Run(ctx, input) + require.NoError(t, err) + var validatorsOut struct { + Response ValidatorsResponse + } + require.NoError(t, p.abi.UnpackIntoInterface(&validatorsOut, ValidatorsMethod, ret)) + validators := validatorsOut.Response + require.Len(t, validators.Validators, 1) + require.Equal(t, caller.Hex(), validators.Validators[0].OperatorAddress) + require.Empty(t, validators.NextKey) +} + +func TestPrecompileMovesNativeBalancesForStakingTransitions(t *testing.T) { + p, err := NewPrecompile() + require.NoError(t, err) + + delegator := common.HexToAddress("0x0000000000000000000000000000000000000abc") + dstValidator := common.HexToAddress("0x0000000000000000000000000000000000000def") + store := newMemoryStore() + balances := newMemoryBalances() + ctx := &precompiles.Context{ + Caller: delegator, + Address: address, + ApparentValue: new(big.Int).Mul(big.NewInt(5), useiToSwei), + Block: precompiles.BlockContext{Number: 7, Time: 100}, + Store: store, + Balances: balances, + Logs: &memoryLogs{}, + } + + balances.add(address, ctx.ApparentValue) + input, err := p.abi.Pack( + CreateValidatorMethod, + "01020304", + "validator-one", + "0.100000000000000000", + "0.200000000000000000", + "0.010000000000000000", + big.NewInt(1), + ) + require.NoError(t, err) + _, err = p.Run(ctx, input) + require.NoError(t, err) + + ctx.Caller = dstValidator + ctx.ApparentValue = new(big.Int).Mul(big.NewInt(1), useiToSwei) + balances.add(address, ctx.ApparentValue) + input, err = p.abi.Pack( + CreateValidatorMethod, + "05060708", + "validator-two", + "0.100000000000000000", + "0.200000000000000000", + "0.010000000000000000", + big.NewInt(1), + ) + require.NoError(t, err) + _, err = p.Run(ctx, input) + require.NoError(t, err) + + ctx.Caller = delegator + ctx.ApparentValue = nil + input, err = p.abi.Pack(RedelegateMethod, delegator.Hex(), dstValidator.Hex(), big.NewInt(2)) + require.NoError(t, err) + _, err = p.Run(ctx, input) + require.NoError(t, err) + src, ok, err := getValidator(store, delegator.Hex()) + require.NoError(t, err) + require.True(t, ok) + require.Equal(t, "3", src.Tokens) + dst, ok, err := getValidator(store, dstValidator.Hex()) + require.NoError(t, err) + require.True(t, ok) + require.Equal(t, "3", dst.Tokens) + require.Equal(t, new(big.Int).Mul(big.NewInt(6), useiToSwei), balances.balance(EscrowAddress())) + require.Zero(t, balances.balance(delegator).Sign()) + require.Zero(t, balances.balance(dstValidator).Sign()) + + input, err = p.abi.Pack(UndelegateMethod, dstValidator.Hex(), big.NewInt(1)) + require.NoError(t, err) + _, err = p.Run(ctx, input) + require.NoError(t, err) + require.Equal(t, new(big.Int).Mul(big.NewInt(6), useiToSwei), balances.balance(EscrowAddress())) + require.Zero(t, balances.balance(delegator).Sign()) + require.Zero(t, balances.balance(dstValidator).Sign()) + require.Zero(t, balances.balance(address).Sign()) + + _, err = p.EndBlock(&precompiles.EndBlockContext{ + Address: address, + Block: precompiles.BlockContext{ + Number: 8, + Time: 100 + 1_814_400, + }, + Store: store, + Balances: balances, + Logs: &memoryLogs{}, + }) + require.NoError(t, err) + require.Equal(t, new(big.Int).Mul(big.NewInt(5), useiToSwei), balances.balance(EscrowAddress())) + require.Equal(t, new(big.Int).Mul(big.NewInt(1), useiToSwei), balances.balance(delegator)) +} + +func TestPrecompileRejectsDelegateCall(t *testing.T) { + p, err := NewPrecompile() + require.NoError(t, err) + + input, err := p.abi.Pack(PoolMethod) + require.NoError(t, err) + _, err = p.Run(&precompiles.Context{ + DelegateCall: true, + Store: newMemoryStore(), + }, input) + require.ErrorIs(t, err, errDelegateCall) +} + +func requireBoolReturn(t *testing.T, p *Precompile, method string, ret []byte, expected bool) { + t.Helper() + values, err := p.abi.Unpack(method, ret) + require.NoError(t, err) + require.Len(t, values, 1) + require.Equal(t, expected, values[0]) +} + +type memoryStore struct { + values map[string][]byte +} + +func newMemoryStore() *memoryStore { + return &memoryStore{values: map[string][]byte{}} +} + +func (s *memoryStore) Get(key []byte) ([]byte, bool) { + value, ok := s.values[string(key)] + if !ok { + return nil, false + } + return append([]byte(nil), value...), true +} + +func (s *memoryStore) Set(key []byte, value []byte) { + s.values[string(key)] = append([]byte(nil), value...) +} + +func (s *memoryStore) Delete(key []byte) { + delete(s.values, string(key)) +} + +type memoryLogs struct { + logs []*ethtypes.Log +} + +func (l *memoryLogs) AddLog(log *ethtypes.Log) { + l.logs = append(l.logs, log) +} + +type memoryBalances struct { + balances map[common.Address]*big.Int +} + +func newMemoryBalances() *memoryBalances { + return &memoryBalances{balances: map[common.Address]*big.Int{}} +} + +func (b *memoryBalances) Transfer(from common.Address, to common.Address, amount *big.Int) error { + if amount == nil || amount.Sign() == 0 { + return nil + } + if amount.Sign() < 0 { + return errors.New("negative amount") + } + fromBalance := b.balance(from) + if fromBalance.Cmp(amount) < 0 { + return errors.New("insufficient balance") + } + b.balances[from] = new(big.Int).Sub(fromBalance, amount) + b.balances[to] = new(big.Int).Add(b.balance(to), amount) + return nil +} + +func (b *memoryBalances) add(addr common.Address, amount *big.Int) { + if amount == nil || amount.Sign() == 0 { + return + } + b.balances[addr] = new(big.Int).Add(b.balance(addr), amount) +} + +func (b *memoryBalances) balance(addr common.Address) *big.Int { + balance, ok := b.balances[addr] + if !ok { + return new(big.Int) + } + return new(big.Int).Set(balance) +} diff --git a/giga/evmonly/precompiles/staking/state.go b/giga/evmonly/precompiles/staking/state.go new file mode 100644 index 0000000000..626539e7e9 --- /dev/null +++ b/giga/evmonly/precompiles/staking/state.go @@ -0,0 +1,667 @@ +package staking + +import ( + "errors" + "math/big" + "sort" + "strconv" + "strings" + + "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles" + "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles/util" +) + +func addDelegation(store precompiles.Store, delegator string, validator string, delta *big.Int) error { + record, ok, err := getDelegation(store, delegator, validator) + if err != nil { + return err + } + current := new(big.Int) + if ok { + current, err = util.ParseAmount(record.Amount) + if err != nil { + return err + } + } + next := new(big.Int).Add(current, delta) + if next.Sign() < 0 { + return errors.New("delegation amount is insufficient") + } + if next.Sign() == 0 { + store.Delete(delegationKey(delegator, validator)) + if err := removeStringListItem(store, delegatorDelegationsIndexKey(delegator), validator); err != nil { + return err + } + return removeStringListItem(store, validatorDelegationsIndexKey(validator), delegator) + } + record = delegationRecord{ + DelegatorAddress: delegator, + ValidatorAddress: validator, + Amount: next.String(), + } + if err := util.SetJSON(store, delegationKey(delegator, validator), record); err != nil { + return err + } + if err := addStringListItem(store, delegatorDelegationsIndexKey(delegator), validator); err != nil { + return err + } + return addStringListItem(store, validatorDelegationsIndexKey(validator), delegator) +} + +func getDelegation(store precompiles.Store, delegator string, validator string) (delegationRecord, bool, error) { + return util.GetJSON[delegationRecord](store, delegationKey(delegator, validator)) +} + +func validateDelegationAmount(store precompiles.Store, delegator string, validator string, amount *big.Int) error { + record, ok, err := getDelegation(store, delegator, validator) + if err != nil { + return err + } + if !ok { + return errors.New("delegation amount is insufficient") + } + current, err := util.ParseAmount(record.Amount) + if err != nil { + return err + } + if current.Cmp(amount) < 0 { + return errors.New("delegation amount is insufficient") + } + return nil +} + +func delegationFromRecord(record delegationRecord) (Delegation, error) { + amount, err := util.ParseAmount(record.Amount) + if err != nil { + return Delegation{}, err + } + return Delegation{ + Balance: Balance{ + Amount: amount, + Denom: bondDenom, + }, + Delegation: DelegationDetails{ + DelegatorAddress: record.DelegatorAddress, + Shares: new(big.Int).Set(amount), + Decimals: big.NewInt(precision), + ValidatorAddress: record.ValidatorAddress, + }, + }, nil +} + +func setValidator(store precompiles.Store, validator Validator) error { + if err := util.SetJSON(store, validatorKey(validator.OperatorAddress), validator); err != nil { + return err + } + return addStringListItem(store, validatorsIndexKey(), validator.OperatorAddress) +} + +func getValidator(store precompiles.Store, validatorAddress string) (Validator, bool, error) { + return util.GetJSON[Validator](store, validatorKey(validatorAddress)) +} + +func removeValidator(store precompiles.Store, validatorAddress string) error { + store.Delete(validatorKey(validatorAddress)) + return removeStringListItem(store, validatorsIndexKey(), validatorAddress) +} + +func addValidatorTokens(store precompiles.Store, validatorAddress string, delta *big.Int) error { + validator, ok, err := getValidator(store, validatorAddress) + if err != nil { + return err + } + if !ok { + return errValidatorMissing + } + tokens, err := util.ParseAmount(validator.Tokens) + if err != nil { + return err + } + shares, err := util.ParseAmount(validator.DelegatorShares) + if err != nil { + return err + } + tokens.Add(tokens, delta) + shares.Add(shares, delta) + if tokens.Sign() < 0 || shares.Sign() < 0 { + return errors.New("validator tokens are insufficient") + } + validator.Tokens = tokens.String() + validator.DelegatorShares = shares.String() + return setValidator(store, validator) +} + +func addUnbondingDelegation(store precompiles.Store, delegator string, validator string, amount *big.Int, creationHeight uint64, completionTime int64) error { + record, _, err := getUnbondingDelegation(store, delegator, validator) + if err != nil { + return err + } + record.DelegatorAddress = delegator + record.ValidatorAddress = validator + record.Entries = append(record.Entries, UnbondingDelegationEntry{ + CreationHeight: int64(creationHeight), //nolint:gosec // block heights fit signed ABI output in normal operation. + CompletionTime: completionTime, + InitialBalance: amount.String(), + Balance: amount.String(), + }) + if err := util.SetJSON(store, unbondingDelegationKey(delegator, validator), record); err != nil { + return err + } + if err := addStringListItem(store, delegatorUnbondingsIndexKey(delegator), validator); err != nil { + return err + } + if err := addStringListItem(store, validatorUnbondingsIndexKey(validator), delegator); err != nil { + return err + } + return insertUnbondingQueue(store, completionTime, delegator, validator) +} + +func getUnbondingDelegation(store precompiles.Store, delegator string, validator string) (UnbondingDelegation, bool, error) { + return util.GetJSON[UnbondingDelegation](store, unbondingDelegationKey(delegator, validator)) +} + +func addRedelegation(store precompiles.Store, delegator string, srcValidator string, dstValidator string, amount *big.Int, completionTime int64) error { + record, _, err := getRedelegation(store, delegator, srcValidator, dstValidator) + if err != nil { + return err + } + record.DelegatorAddress = delegator + record.ValidatorSrcAddress = srcValidator + record.ValidatorDstAddress = dstValidator + record.Entries = append(record.Entries, RedelegationEntry{ + CreationHeight: 0, + CompletionTime: completionTime, + InitialBalance: amount.String(), + SharesDst: amount.String(), + }) + if err := util.SetJSON(store, redelegationKey(delegator, srcValidator, dstValidator), record); err != nil { + return err + } + if err := addStringListItem(store, redelegationsIndexKey(), redelegationID(delegator, srcValidator, dstValidator)); err != nil { + return err + } + return insertRedelegationQueue(store, completionTime, delegator, srcValidator, dstValidator) +} + +func getRedelegation(store precompiles.Store, delegator string, srcValidator string, dstValidator string) (Redelegation, bool, error) { + return util.GetJSON[Redelegation](store, redelegationKey(delegator, srcValidator, dstValidator)) +} + +func loadParams(store precompiles.Store) (Params, error) { + params, ok, err := util.GetJSON[Params](store, paramsKey()) + if err != nil { + return Params{}, err + } + if ok { + params.BondDenom = bondDenom + return params, nil + } + return Params{ + UnbondingTime: 1_814_400, + MaxValidators: 100, + MaxEntries: 7, + HistoricalEntries: 10_000, + BondDenom: bondDenom, + MinCommissionRate: "0.000000000000000000", + MaxVotingPowerRatio: "0.000000000000000000", + MaxVotingPowerEnforcementThreshold: "0.000000000000000000", + }, nil +} + +func loadPool(store precompiles.Store) (Pool, error) { + pool, ok, err := util.GetJSON[Pool](store, poolKey()) + if err != nil { + return Pool{}, err + } + if ok { + return pool, nil + } + return Pool{NotBondedTokens: "0", BondedTokens: "0"}, nil +} + +func addPoolBonded(store precompiles.Store, delta *big.Int) error { + pool, err := loadPool(store) + if err != nil { + return err + } + bonded, err := util.ParseAmount(pool.BondedTokens) + if err != nil { + return err + } + bonded.Add(bonded, delta) + if bonded.Sign() < 0 { + return errors.New("bonded pool tokens are insufficient") + } + pool.BondedTokens = bonded.String() + return util.SetJSON(store, poolKey(), pool) +} + +func addPoolNotBonded(store precompiles.Store, delta *big.Int) error { + pool, err := loadPool(store) + if err != nil { + return err + } + notBonded, err := util.ParseAmount(pool.NotBondedTokens) + if err != nil { + return err + } + notBonded.Add(notBonded, delta) + if notBonded.Sign() < 0 { + return errors.New("not bonded pool tokens are insufficient") + } + pool.NotBondedTokens = notBonded.String() + return util.SetJSON(store, poolKey(), pool) +} + +func movePoolsForRedelegation(store precompiles.Store, srcStatus int32, dstStatus int32, amount *big.Int) error { + srcBonded := srcStatus == bondStatusBonded + dstBonded := dstStatus == bondStatusBonded + switch { + case srcBonded == dstBonded: + return nil + case srcBonded: + if err := addPoolBonded(store, new(big.Int).Neg(amount)); err != nil { + return err + } + return addPoolNotBonded(store, amount) + default: + if err := addPoolNotBonded(store, new(big.Int).Neg(amount)); err != nil { + return err + } + return addPoolBonded(store, amount) + } +} + +func setHistoricalInfo(store precompiles.Store, height uint64) error { + if height > uint64(1<<63-1) { + return nil + } + validatorAddresses, err := getStringList(store, validatorsIndexKey()) + if err != nil { + return err + } + info := HistoricalInfo{ + Height: int64(height), //nolint:gosec // bounded above. + Validators: make([]Validator, 0, len(validatorAddresses)), + } + for _, validatorAddress := range validatorAddresses { + validator, ok, err := getValidator(store, validatorAddress) + if err != nil { + return err + } + if ok { + info.Validators = append(info.Validators, validator) + } + } + return util.SetJSON(store, historicalInfoKey(info.Height), info) +} + +func getHistoricalInfo(store precompiles.Store, height int64) (HistoricalInfo, bool, error) { + return util.GetJSON[HistoricalInfo](store, historicalInfoKey(height)) +} + +func getStringList(store precompiles.Store, key []byte) ([]string, error) { + items, ok, err := util.GetJSON[[]string](store, key) + if err != nil || !ok { + return nil, err + } + sort.Strings(items) + return items, nil +} + +func setStringList(store precompiles.Store, key []byte, items []string) error { + if len(items) == 0 { + store.Delete(key) + return nil + } + sort.Strings(items) + return util.SetJSON(store, key, items) +} + +func addStringListItem(store precompiles.Store, key []byte, item string) error { + items, err := getStringList(store, key) + if err != nil { + return err + } + for _, existing := range items { + if existing == item { + return nil + } + } + items = append(items, item) + sort.Strings(items) + return util.SetJSON(store, key, items) +} + +func removeStringListItem(store precompiles.Store, key []byte, item string) error { + items, err := getStringList(store, key) + if err != nil { + return err + } + out := items[:0] + for _, existing := range items { + if existing != item { + out = append(out, existing) + } + } + if len(out) == 0 { + store.Delete(key) + return nil + } + return setStringList(store, key, out) +} + +func insertUnbondingQueue(store precompiles.Store, completionTime int64, delegator string, validator string) error { + id := timeQueueID(completionTime) + items, err := getDelegationPairList(store, unbondingQueueKey(id)) + if err != nil { + return err + } + item := delegationPair{DelegatorAddress: delegator, ValidatorAddress: validator} + for _, existing := range items { + if existing == item { + return nil + } + } + items = append(items, item) + if err := util.SetJSON(store, unbondingQueueKey(id), items); err != nil { + return err + } + return addStringListItem(store, unbondingQueueIndexKey(), id) +} + +func insertRedelegationQueue(store precompiles.Store, completionTime int64, delegator string, srcValidator string, dstValidator string) error { + id := timeQueueID(completionTime) + items, err := getRedelegationTripletList(store, redelegationQueueKey(id)) + if err != nil { + return err + } + item := redelegationTriplet{DelegatorAddress: delegator, ValidatorSrcAddress: srcValidator, ValidatorDstAddress: dstValidator} + for _, existing := range items { + if existing == item { + return nil + } + } + items = append(items, item) + if err := util.SetJSON(store, redelegationQueueKey(id), items); err != nil { + return err + } + return addStringListItem(store, redelegationQueueIndexKey(), id) +} + +func insertValidatorQueue(store precompiles.Store, completionTime int64, completionHeight int64, validator string) error { + id := validatorQueueID(completionTime, completionHeight) + if err := addStringListItem(store, validatorQueueKey(id), validator); err != nil { + return err + } + return addStringListItem(store, validatorQueueIndexKey(), id) +} + +func deleteValidatorQueue(store precompiles.Store, completionTime int64, completionHeight int64, validator string) error { + id := validatorQueueID(completionTime, completionHeight) + if err := removeStringListItem(store, validatorQueueKey(id), validator); err != nil { + return err + } + if items, err := getStringList(store, validatorQueueKey(id)); err != nil { + return err + } else if len(items) == 0 { + return removeStringListItem(store, validatorQueueIndexKey(), id) + } + return nil +} + +func getDelegationPairList(store precompiles.Store, key []byte) ([]delegationPair, error) { + items, ok, err := util.GetJSON[[]delegationPair](store, key) + if err != nil || !ok { + return nil, err + } + return items, nil +} + +func getRedelegationTripletList(store precompiles.Store, key []byte) ([]redelegationTriplet, error) { + items, ok, err := util.GetJSON[[]redelegationTriplet](store, key) + if err != nil || !ok { + return nil, err + } + return items, nil +} + +func matureTimeQueueIDs(store precompiles.Store, indexKey []byte, blockTime uint64) ([]string, error) { + ids, err := getStringList(store, indexKey) + if err != nil { + return nil, err + } + mature := ids[:0] + for _, id := range ids { + time, err := parseTimeQueueID(id) + if err != nil { + return nil, err + } + if time <= int64(blockTime) { + mature = append(mature, id) + } + } + sort.SliceStable(mature, func(i, j int) bool { + left, _ := parseTimeQueueID(mature[i]) + right, _ := parseTimeQueueID(mature[j]) + if left == right { + return mature[i] < mature[j] + } + return left < right + }) + return mature, nil +} + +func matureValidatorQueueIDs(store precompiles.Store, blockTime uint64, blockHeight uint64) ([]string, error) { + ids, err := getStringList(store, validatorQueueIndexKey()) + if err != nil { + return nil, err + } + mature := ids[:0] + for _, id := range ids { + time, height, err := parseValidatorQueueID(id) + if err != nil { + return nil, err + } + if time <= int64(blockTime) && height <= int64(blockHeight) { + mature = append(mature, id) + } + } + sort.SliceStable(mature, func(i, j int) bool { + leftTime, leftHeight, _ := parseValidatorQueueID(mature[i]) + rightTime, rightHeight, _ := parseValidatorQueueID(mature[j]) + if leftTime != rightTime { + return leftTime < rightTime + } + if leftHeight != rightHeight { + return leftHeight < rightHeight + } + return mature[i] < mature[j] + }) + return mature, nil +} + +func setLastValidatorPower(store precompiles.Store, validator string, power int64) error { + if err := util.SetJSON(store, lastValidatorPowerKey(validator), power); err != nil { + return err + } + return addStringListItem(store, lastValidatorsIndexKey(), validator) +} + +func deleteLastValidatorPower(store precompiles.Store, validator string) error { + store.Delete(lastValidatorPowerKey(validator)) + return removeStringListItem(store, lastValidatorsIndexKey(), validator) +} + +func getLastValidatorPowers(store precompiles.Store) (map[string]int64, error) { + validators, err := getStringList(store, lastValidatorsIndexKey()) + if err != nil { + return nil, err + } + out := make(map[string]int64, len(validators)) + for _, validator := range validators { + power, ok, err := util.GetJSON[int64](store, lastValidatorPowerKey(validator)) + if err != nil { + return nil, err + } + if ok { + out[validator] = power + } + } + return out, nil +} + +func setLastTotalPower(store precompiles.Store, power int64) error { + return util.SetJSON(store, lastTotalPowerKey(), power) +} + +func pageStrings(items []string, nextKey []byte) ([]string, []byte, error) { + start := 0 + if len(nextKey) != 0 { + parsed, err := strconv.Atoi(string(nextKey)) + if err != nil || parsed < 0 { + return nil, nil, errors.New("invalid pagination key") + } + start = parsed + } + if start >= len(items) { + return nil, nil, nil + } + end := start + pageLimit + if end > len(items) { + end = len(items) + } + var outNextKey []byte + if end < len(items) { + outNextKey = []byte(strconv.Itoa(end)) + } + return items[start:end], outNextKey, nil +} + +func paramsKey() []byte { + return []byte("params") +} + +func poolKey() []byte { + return []byte("pool") +} + +func validatorsIndexKey() []byte { + return []byte("validators/index") +} + +func validatorKey(validator string) []byte { + return []byte("validator/" + validator) +} + +func delegationKey(delegator string, validator string) []byte { + return []byte("delegation/" + delegator + "/" + validator) +} + +func delegatorDelegationsIndexKey(delegator string) []byte { + return []byte("delegator-delegations/" + delegator) +} + +func validatorDelegationsIndexKey(validator string) []byte { + return []byte("validator-delegations/" + validator) +} + +func unbondingDelegationKey(delegator string, validator string) []byte { + return []byte("unbonding/" + delegator + "/" + validator) +} + +func delegatorUnbondingsIndexKey(delegator string) []byte { + return []byte("delegator-unbondings/" + delegator) +} + +func validatorUnbondingsIndexKey(validator string) []byte { + return []byte("validator-unbondings/" + validator) +} + +func redelegationsIndexKey() []byte { + return []byte("redelegations/index") +} + +func redelegationKey(delegator string, srcValidator string, dstValidator string) []byte { + return []byte("redelegation/" + redelegationID(delegator, srcValidator, dstValidator)) +} + +func redelegationID(delegator string, srcValidator string, dstValidator string) string { + return delegator + "\x00" + srcValidator + "\x00" + dstValidator +} + +func splitRedelegationID(id string) (string, string, string, bool) { + parts := strings.Split(id, "\x00") + if len(parts) != 3 { + return "", "", "", false + } + return parts[0], parts[1], parts[2], true +} + +func historicalInfoKey(height int64) []byte { + return []byte("historical/" + strconv.FormatInt(height, 10)) +} + +func lastValidatorsIndexKey() []byte { + return []byte("last-validators/index") +} + +func lastValidatorPowerKey(validator string) []byte { + return []byte("last-validators/power/" + validator) +} + +func lastTotalPowerKey() []byte { + return []byte("last-validators/total-power") +} + +func unbondingQueueIndexKey() []byte { + return []byte("unbonding-queue/index") +} + +func unbondingQueueKey(id string) []byte { + return []byte("unbonding-queue/" + id) +} + +func redelegationQueueIndexKey() []byte { + return []byte("redelegation-queue/index") +} + +func redelegationQueueKey(id string) []byte { + return []byte("redelegation-queue/" + id) +} + +func validatorQueueIndexKey() []byte { + return []byte("validator-queue/index") +} + +func validatorQueueKey(id string) []byte { + return []byte("validator-queue/" + id) +} + +func timeQueueID(completionTime int64) string { + return strconv.FormatInt(completionTime, 10) +} + +func parseTimeQueueID(id string) (int64, error) { + return strconv.ParseInt(id, 10, 64) +} + +func validatorQueueID(completionTime int64, completionHeight int64) string { + return strconv.FormatInt(completionTime, 10) + "/" + strconv.FormatInt(completionHeight, 10) +} + +func parseValidatorQueueID(id string) (int64, int64, error) { + timePart, heightPart, ok := strings.Cut(id, "/") + if !ok { + return 0, 0, errors.New("invalid validator queue id") + } + completionTime, err := strconv.ParseInt(timePart, 10, 64) + if err != nil { + return 0, 0, err + } + completionHeight, err := strconv.ParseInt(heightPart, 10, 64) + if err != nil { + return 0, 0, err + } + return completionTime, completionHeight, nil +} diff --git a/giga/evmonly/precompiles/staking/types.go b/giga/evmonly/precompiles/staking/types.go new file mode 100644 index 0000000000..c1328a91cc --- /dev/null +++ b/giga/evmonly/precompiles/staking/types.go @@ -0,0 +1,122 @@ +package staking + +import "math/big" + +type Delegation struct { + Balance Balance + Delegation DelegationDetails +} + +type Balance struct { + Amount *big.Int + Denom string +} + +type DelegationDetails struct { + DelegatorAddress string + Shares *big.Int + Decimals *big.Int + ValidatorAddress string +} + +type ValidatorsResponse struct { + Validators []Validator + NextKey []byte +} + +type DelegationsResponse struct { + Delegations []Delegation + NextKey []byte +} + +type UnbondingDelegationsResponse struct { + UnbondingDelegations []UnbondingDelegation + NextKey []byte +} + +type RedelegationsResponse struct { + Redelegations []Redelegation + NextKey []byte +} + +type Validator struct { + OperatorAddress string + ConsensusPubkey []byte + Jailed bool + Status int32 + Tokens string + DelegatorShares string + Description string + UnbondingHeight int64 + UnbondingTime int64 + CommissionRate string + CommissionMaxRate string + CommissionMaxChangeRate string + CommissionUpdateTime int64 + MinSelfDelegation string +} + +type delegationRecord struct { + DelegatorAddress string `json:"delegator_address"` + ValidatorAddress string `json:"validator_address"` + Amount string `json:"amount"` +} + +type UnbondingDelegationEntry struct { + CreationHeight int64 + CompletionTime int64 + InitialBalance string + Balance string +} + +type UnbondingDelegation struct { + DelegatorAddress string + ValidatorAddress string + Entries []UnbondingDelegationEntry +} + +type RedelegationEntry struct { + CreationHeight int64 + CompletionTime int64 + InitialBalance string + SharesDst string +} + +type Redelegation struct { + DelegatorAddress string + ValidatorSrcAddress string + ValidatorDstAddress string + Entries []RedelegationEntry +} + +type HistoricalInfo struct { + Height int64 + Validators []Validator +} + +type Pool struct { + NotBondedTokens string + BondedTokens string +} + +type Params struct { + UnbondingTime uint64 + MaxValidators uint32 + MaxEntries uint32 + HistoricalEntries uint32 + BondDenom string + MinCommissionRate string + MaxVotingPowerRatio string + MaxVotingPowerEnforcementThreshold string +} + +type delegationPair struct { + DelegatorAddress string + ValidatorAddress string +} + +type redelegationTriplet struct { + DelegatorAddress string + ValidatorSrcAddress string + ValidatorDstAddress string +} diff --git a/giga/evmonly/precompiles/util/events.go b/giga/evmonly/precompiles/util/events.go new file mode 100644 index 0000000000..0d8e3b1590 --- /dev/null +++ b/giga/evmonly/precompiles/util/events.go @@ -0,0 +1,27 @@ +package util + +import ( + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + + "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles" +) + +func EmitEvent(logs precompiles.LogSink, address common.Address, event abi.Event, indexed common.Address, args ...interface{}) { + if logs == nil { + return + } + data, err := event.Inputs.NonIndexed().Pack(args...) + if err != nil { + return + } + logs.AddLog(ðtypes.Log{ + Address: address, + Topics: []common.Hash{ + event.ID, + common.BytesToHash(indexed.Bytes()), + }, + Data: data, + }) +} diff --git a/giga/evmonly/precompiles/util/helpers.go b/giga/evmonly/precompiles/util/helpers.go new file mode 100644 index 0000000000..ae2b2704a2 --- /dev/null +++ b/giga/evmonly/precompiles/util/helpers.go @@ -0,0 +1,69 @@ +package util + +import ( + "errors" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" +) + +func ParseAmount(s string) (*big.Int, error) { + if s == "" { + return new(big.Int), nil + } + amount, ok := new(big.Int).SetString(s, 10) + if !ok { + return nil, fmt.Errorf("invalid integer amount %q", s) + } + return amount, nil +} + +func CloneBig(v *big.Int) *big.Int { + if v == nil { + return new(big.Int) + } + return new(big.Int).Set(v) +} + +func ValidateNonPayable(value *big.Int) error { + if value != nil && value.Sign() != 0 { + return errors.New("sending funds to a non-payable function") + } + return nil +} + +func ValidateArgsLength(args []interface{}, length int) error { + if len(args) != length { + return fmt.Errorf("expected %d arguments but got %d", length, len(args)) + } + return nil +} + +func ValidatePositiveAmount(amount *big.Int, name string) error { + if amount == nil || amount.Sign() <= 0 { + return fmt.Errorf("%s must be a positive integer", name) + } + return nil +} + +func ValidateDecimal(value string, name string) error { + if value == "" { + return fmt.Errorf("invalid %s", name) + } + if _, ok := new(big.Rat).SetString(value); !ok { + return fmt.Errorf("invalid %s", name) + } + return nil +} + +func SaturatingCompletionTime(blockTime uint64, offset uint64) int64 { + if blockTime > uint64(1<<63-1) || offset > uint64(1<<63-1)-blockTime { + return int64(1<<63 - 1) + } + return int64(blockTime + offset) //nolint:gosec // bounded above. +} + +func AddressString(addr common.Address) string { + return addr.Hex() +} diff --git a/giga/evmonly/precompiles/util/json.go b/giga/evmonly/precompiles/util/json.go new file mode 100644 index 0000000000..0fba6de350 --- /dev/null +++ b/giga/evmonly/precompiles/util/json.go @@ -0,0 +1,28 @@ +package util + +import ( + "encoding/json" + + "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles" +) + +func GetJSON[T any](store precompiles.Store, key []byte) (T, bool, error) { + var out T + bz, ok := store.Get(key) + if !ok { + return out, false, nil + } + if err := json.Unmarshal(bz, &out); err != nil { + return out, false, err + } + return out, true, nil +} + +func SetJSON[T any](store precompiles.Store, key []byte, value T) error { + bz, err := json.Marshal(value) + if err != nil { + return err + } + store.Set(key, bz) + return nil +} diff --git a/giga/evmonly/result_pool.go b/giga/evmonly/result_pool.go index 15e31adc4c..d8fcc17c76 100644 --- a/giga/evmonly/result_pool.go +++ b/giga/evmonly/result_pool.go @@ -104,6 +104,8 @@ func (r *BlockResult) prepareIndexedResults(txCount int) { func (r *BlockResult) resetForPool() { r.ChangeSet.resetForReuse() + clear(r.ValidatorUpdates) + r.ValidatorUpdates = r.ValidatorUpdates[:0] clear(r.Txs) r.Txs = r.Txs[:0] clear(r.Receipts) diff --git a/giga/evmonly/types.go b/giga/evmonly/types.go index 730a64a02a..6b017f87a9 100644 --- a/giga/evmonly/types.go +++ b/giga/evmonly/types.go @@ -6,6 +6,8 @@ import ( "github.com/ethereum/go-ethereum/common" ethtypes "github.com/ethereum/go-ethereum/core/types" + + "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles" ) // BlockExecutor is the Cosmos-free block execution boundary for the EVM-only path. @@ -73,11 +75,12 @@ type BlockContext struct { // 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 + ChangeSet StateChangeSet + ValidatorUpdates []ValidatorUpdate + Txs []TxResult + Receipts ethtypes.Receipts + GasUsed uint64 + OCCStats OCCStats lease *blockResultLease } @@ -93,6 +96,8 @@ func (r *BlockResult) Release() { lease.release() } +type ValidatorUpdate = precompiles.ValidatorUpdate + // OCCStats reports optimistic concurrency control behavior for a block. type OCCStats struct { Attempted bool From d1c4d72b090a78a81bb4b755f272630b2d4091ef Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Mon, 22 Jun 2026 16:49:43 +0800 Subject: [PATCH 23/27] fix staking completion time conversions --- giga/evmonly/precompiles/staking/endblock.go | 8 +++++--- giga/evmonly/precompiles/staking/staking.go | 4 ++-- giga/evmonly/precompiles/staking/state.go | 16 +++++++++++++--- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/giga/evmonly/precompiles/staking/endblock.go b/giga/evmonly/precompiles/staking/endblock.go index a55a1d0573..125bdc2d43 100644 --- a/giga/evmonly/precompiles/staking/endblock.go +++ b/giga/evmonly/precompiles/staking/endblock.go @@ -124,7 +124,7 @@ func applyAndReturnValidatorSetUpdates(store precompiles.Store, block precompile if validator.Status == bondStatusBonded { validator.Status = bondStatusUnbonding validator.UnbondingTime = util.SaturatingCompletionTime(block.Time, params.UnbondingTime) - validator.UnbondingHeight = int64(block.Number) //nolint:gosec // block heights fit signed ABI output in normal operation. + validator.UnbondingHeight = saturatingInt64FromUint64(block.Number) if err := setValidator(store, validator); err != nil { return nil, err } @@ -293,8 +293,9 @@ func completeUnbonding(ctx *precompiles.EndBlockContext, pair delegationPair) er return err } remaining := record.Entries[:0] + blockTime := saturatingInt64FromUint64(ctx.Block.Time) for _, entry := range record.Entries { - if entry.CompletionTime > int64(ctx.Block.Time) { + if entry.CompletionTime > blockTime { remaining = append(remaining, entry) continue } @@ -351,8 +352,9 @@ func completeRedelegation(store precompiles.Store, triplet redelegationTriplet, return err } remaining := record.Entries[:0] + completionTime := saturatingInt64FromUint64(blockTime) for _, entry := range record.Entries { - if entry.CompletionTime > int64(blockTime) { + if entry.CompletionTime > completionTime { remaining = append(remaining, entry) } } diff --git a/giga/evmonly/precompiles/staking/staking.go b/giga/evmonly/precompiles/staking/staking.go index c3e42b5bdd..ff792641af 100644 --- a/giga/evmonly/precompiles/staking/staking.go +++ b/giga/evmonly/precompiles/staking/staking.go @@ -420,7 +420,7 @@ func (p *Precompile) createValidator(ctx *precompiles.Context, method *abi.Metho CommissionRate: commissionRate, CommissionMaxRate: commissionMaxRate, CommissionMaxChangeRate: commissionMaxChangeRate, - CommissionUpdateTime: int64(ctx.Block.Time), //nolint:gosec // block times fit signed ABI output in normal operation. + CommissionUpdateTime: saturatingInt64FromUint64(ctx.Block.Time), MinSelfDelegation: minSelfDelegation.String(), } if err := setValidator(ctx.Store, validator); err != nil { @@ -468,7 +468,7 @@ func (p *Precompile) editValidator(ctx *precompiles.Context, method *abi.Method, return nil, err } validator.CommissionRate = commissionRate - validator.CommissionUpdateTime = int64(ctx.Block.Time) //nolint:gosec // block times fit signed ABI output in normal operation. + validator.CommissionUpdateTime = saturatingInt64FromUint64(ctx.Block.Time) } if minSelfDelegation != nil && minSelfDelegation.Sign() > 0 { validator.MinSelfDelegation = minSelfDelegation.String() diff --git a/giga/evmonly/precompiles/staking/state.go b/giga/evmonly/precompiles/staking/state.go index 626539e7e9..b551ab14dd 100644 --- a/giga/evmonly/precompiles/staking/state.go +++ b/giga/evmonly/precompiles/staking/state.go @@ -139,7 +139,7 @@ func addUnbondingDelegation(store precompiles.Store, delegator string, validator record.DelegatorAddress = delegator record.ValidatorAddress = validator record.Entries = append(record.Entries, UnbondingDelegationEntry{ - CreationHeight: int64(creationHeight), //nolint:gosec // block heights fit signed ABI output in normal operation. + CreationHeight: saturatingInt64FromUint64(creationHeight), CompletionTime: completionTime, InitialBalance: amount.String(), Balance: amount.String(), @@ -432,12 +432,13 @@ func matureTimeQueueIDs(store precompiles.Store, indexKey []byte, blockTime uint return nil, err } mature := ids[:0] + completionTime := saturatingInt64FromUint64(blockTime) for _, id := range ids { time, err := parseTimeQueueID(id) if err != nil { return nil, err } - if time <= int64(blockTime) { + if time <= completionTime { mature = append(mature, id) } } @@ -452,18 +453,27 @@ func matureTimeQueueIDs(store precompiles.Store, indexKey []byte, blockTime uint return mature, nil } +func saturatingInt64FromUint64(value uint64) int64 { + if value > uint64(1<<63-1) { + return int64(1<<63 - 1) + } + return int64(value) //nolint:gosec // bounded above. +} + func matureValidatorQueueIDs(store precompiles.Store, blockTime uint64, blockHeight uint64) ([]string, error) { ids, err := getStringList(store, validatorQueueIndexKey()) if err != nil { return nil, err } mature := ids[:0] + completionTime := saturatingInt64FromUint64(blockTime) + completionHeight := saturatingInt64FromUint64(blockHeight) for _, id := range ids { time, height, err := parseValidatorQueueID(id) if err != nil { return nil, err } - if time <= int64(blockTime) && height <= int64(blockHeight) { + if time <= completionTime && height <= completionHeight { mature = append(mature, id) } } From f1e05e97d7ce4e6185d6a7c9f4301f480c806772 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Wed, 24 Jun 2026 11:45:20 +0800 Subject: [PATCH 24/27] staking: align evm-only precompile with Cosmos semantics Address review findings on the SDK-free staking precompile: - redelegate/undelegate now enforce Cosmos guardrails: reject self-redelegation, transitive redelegation, and over-MaxEntries unbonding/redelegation entries. - delegation shares are reported as an sdk.Dec (scaled by 10^precision) to match the keeper-backed precompile. - create/editValidator validate commission bounds, the 24h rate-change rule, and min-self-delegation increase/cap; stricter decimal parsing rejects fraction/scientific forms. - historicalInfo query is read-only again; historical info is tracked and pruned in the end-block hook. - powerReduction set to 1_000_000 (Sei DefaultPowerReduction); tests use SEI-scale stakes. - validators query no longer re-reads validators; README documents the staking parity limitations. Co-Authored-By: Claude Opus 4.8 --- giga/evmonly/README.md | 7 + giga/evmonly/executor_test.go | 94 +++++----- .../evmonly/precompiles/staking/commission.go | 116 +++++++++++++ giga/evmonly/precompiles/staking/endblock.go | 3 + .../precompiles/staking/guardrails_test.go | 160 ++++++++++++++++++ giga/evmonly/precompiles/staking/staking.go | 101 +++++++---- .../precompiles/staking/staking_test.go | 21 ++- giga/evmonly/precompiles/staking/state.go | 80 ++++++++- giga/evmonly/precompiles/util/helpers.go | 10 -- 9 files changed, 493 insertions(+), 99 deletions(-) create mode 100644 giga/evmonly/precompiles/staking/commission.go create mode 100644 giga/evmonly/precompiles/staking/guardrails_test.go diff --git a/giga/evmonly/README.md b/giga/evmonly/README.md index cb1ea464ea..4945662901 100644 --- a/giga/evmonly/README.md +++ b/giga/evmonly/README.md @@ -148,3 +148,10 @@ closed with `ErrCustomPrecompilesOpen`. - 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. +- The staking precompile models bonding, delegation, redelegation, unbonding, + and validator-set updates in usei, but does not model staking rewards, + slashing, or jailing. Delegation shares track tokens 1:1 (no slash-driven + share/token divergence), and reward-withdrawal events are emitted with a zero + amount. Validator historical info is recorded in the end-block hook (Cosmos + tracks it in begin-block), so info for the current height is only queryable + after that block's end-block runs. diff --git a/giga/evmonly/executor_test.go b/giga/evmonly/executor_test.go index 8812db412b..9b70bd7f45 100644 --- a/giga/evmonly/executor_test.go +++ b/giga/evmonly/executor_test.go @@ -883,7 +883,7 @@ func TestExecutorStakingPrecompileForwardsPayableValue(t *testing.T) { stakingAddr := common.HexToAddress(stakingprecompile.StakingAddress) state := NewMemoryState() - initialBalance := big.NewInt(1_000_000_000_000_000_000) + initialBalance := sei(100) state.SetBalance(sender, initialBalance) contract, err := stakingprecompile.NewPrecompile() @@ -900,7 +900,7 @@ func TestExecutorStakingPrecompileForwardsPayableValue(t *testing.T) { big.NewInt(1), ) require.NoError(t, err) - value := new(big.Int).Mul(big.NewInt(5), big.NewInt(1_000_000_000_000)) + value := sei(5) rawTx := signLegacyTx(t, key, chainID, 0, &stakingAddr, value, input) executor := NewExecutor(Config{ CustomPrecompiles: registry, @@ -939,7 +939,7 @@ func TestExecutorStakingDelegationLifecycleE2E(t *testing.T) { escrowAddr := stakingprecompile.EscrowAddress() state := NewMemoryState() - initialBalance := big.NewInt(1_000_000_000_000_000_000) + initialBalance := sei(1000) state.SetBalance(source, initialBalance) state.SetBalance(destination, initialBalance) state.SetBalance(delegator, initialBalance) @@ -965,8 +965,8 @@ func TestExecutorStakingDelegationLifecycleE2E(t *testing.T) { escrowAddr: new(big.Int), } - sourceSelfStake := usei(10) - destinationSelfStake := usei(5) + sourceSelfStake := sei(10) + destinationSelfStake := sei(5) sourceSetupResult := executeBlockAndApply(t, executor, state, blockContextAt(chainID, 1, 100), [][]byte{ signStakingTx(sourceKey, sourceSelfStake, mustPackStaking(t, contract, stakingprecompile.CreateValidatorMethod, "01020304", @@ -982,8 +982,8 @@ func TestExecutorStakingDelegationLifecycleE2E(t *testing.T) { addExpectedBalance(expectedBalances, escrowAddr, sourceSelfStake) requireNativeBalances(t, state, expectedBalances) require.Equal(t, []ValidatorUpdate{{PubKey: []byte{0x01, 0x02, 0x03, 0x04}, Power: 10}}, sourceSetupResult.ValidatorUpdates) - requireStakingPool(t, state, "10", "0") - requireStakingValidator(t, state, source, "10", "10", 3) + requireStakingPool(t, state, "10000000", "0") + requireStakingValidator(t, state, source, "10000000", "10000000", 3) destinationSetupResult := executeBlockAndApply(t, executor, state, blockContextAt(chainID, 2, 125), [][]byte{ signStakingTx(dstKey, destinationSelfStake, mustPackStaking(t, contract, stakingprecompile.CreateValidatorMethod, @@ -1000,11 +1000,11 @@ func TestExecutorStakingDelegationLifecycleE2E(t *testing.T) { addExpectedBalance(expectedBalances, escrowAddr, destinationSelfStake) requireNativeBalances(t, state, expectedBalances) require.Equal(t, []ValidatorUpdate{{PubKey: []byte{0x05, 0x06, 0x07, 0x08}, Power: 5}}, destinationSetupResult.ValidatorUpdates) - requireStakingPool(t, state, "15", "0") - requireStakingValidator(t, state, source, "10", "10", 3) - requireStakingValidator(t, state, destination, "5", "5", 3) + requireStakingPool(t, state, "15000000", "0") + requireStakingValidator(t, state, source, "10000000", "10000000", 3) + requireStakingValidator(t, state, destination, "5000000", "5000000", 3) - delegationValue := usei(7) + delegationValue := sei(7) delegateResult := executeBlockAndApply(t, executor, state, blockContextAt(chainID, 3, 150), [][]byte{ signStakingTx(delegatorKey, delegationValue, mustPackStaking(t, contract, stakingprecompile.DelegateMethod, source.Hex())), }) @@ -1013,12 +1013,12 @@ func TestExecutorStakingDelegationLifecycleE2E(t *testing.T) { addExpectedBalance(expectedBalances, escrowAddr, delegationValue) requireNativeBalances(t, state, expectedBalances) require.Equal(t, []ValidatorUpdate{{PubKey: []byte{0x01, 0x02, 0x03, 0x04}, Power: 17}}, delegateResult.ValidatorUpdates) - requireStakingPool(t, state, "22", "0") - requireStakingValidator(t, state, source, "17", "17", 3) - requireStakingValidator(t, state, destination, "5", "5", 3) - requireStakingDelegation(t, state, delegator, source, "7") + requireStakingPool(t, state, "22000000", "0") + requireStakingValidator(t, state, source, "17000000", "17000000", 3) + requireStakingValidator(t, state, destination, "5000000", "5000000", 3) + requireStakingDelegation(t, state, delegator, source, "7000000") - redelegationAmount := big.NewInt(3) + redelegationAmount := big.NewInt(3_000_000) redelegationTime := uint64(200) redelegationCompletion := int64(redelegationTime + 1_814_400) redelegateResult := executeBlockAndApply(t, executor, state, blockContextAt(chainID, 4, redelegationTime), [][]byte{ @@ -1031,14 +1031,14 @@ func TestExecutorStakingDelegationLifecycleE2E(t *testing.T) { {PubKey: []byte{0x01, 0x02, 0x03, 0x04}, Power: 14}, {PubKey: []byte{0x05, 0x06, 0x07, 0x08}, Power: 8}, }, redelegateResult.ValidatorUpdates) - requireStakingPool(t, state, "22", "0") - requireStakingValidator(t, state, source, "14", "14", 3) - requireStakingValidator(t, state, destination, "8", "8", 3) - requireStakingDelegation(t, state, delegator, source, "4") - requireStakingDelegation(t, state, delegator, destination, "3") - requireStakingRedelegation(t, state, delegator, source, destination, "3", redelegationCompletion) - - undelegationAmount := big.NewInt(2) + requireStakingPool(t, state, "22000000", "0") + requireStakingValidator(t, state, source, "14000000", "14000000", 3) + requireStakingValidator(t, state, destination, "8000000", "8000000", 3) + requireStakingDelegation(t, state, delegator, source, "4000000") + requireStakingDelegation(t, state, delegator, destination, "3000000") + requireStakingRedelegation(t, state, delegator, source, destination, "3000000", redelegationCompletion) + + undelegationAmount := big.NewInt(2_000_000) undelegationTime := uint64(300) undelegationCompletion := int64(undelegationTime + 1_814_400) undelegateResult := executeBlockAndApply(t, executor, state, blockContextAt(chainID, 5, undelegationTime), [][]byte{ @@ -1048,35 +1048,35 @@ func TestExecutorStakingDelegationLifecycleE2E(t *testing.T) { debitExpectedBalance(expectedBalances, delegator, nil, undelegateResult.Txs[0]) requireNativeBalances(t, state, expectedBalances) require.Equal(t, []ValidatorUpdate{{PubKey: []byte{0x05, 0x06, 0x07, 0x08}, Power: 6}}, undelegateResult.ValidatorUpdates) - requireStakingPool(t, state, "20", "2") - requireStakingValidator(t, state, source, "14", "14", 3) - requireStakingValidator(t, state, destination, "6", "6", 3) - requireStakingDelegation(t, state, delegator, source, "4") - requireStakingDelegation(t, state, delegator, destination, "1") - requireStakingRedelegation(t, state, delegator, source, destination, "3", redelegationCompletion) - requireStakingUnbonding(t, state, delegator, destination, "2", undelegationCompletion) + requireStakingPool(t, state, "20000000", "2000000") + requireStakingValidator(t, state, source, "14000000", "14000000", 3) + requireStakingValidator(t, state, destination, "6000000", "6000000", 3) + requireStakingDelegation(t, state, delegator, source, "4000000") + requireStakingDelegation(t, state, delegator, destination, "1000000") + requireStakingRedelegation(t, state, delegator, source, destination, "3000000", redelegationCompletion) + requireStakingUnbonding(t, state, delegator, destination, "2000000", undelegationCompletion) redelegationMaturityResult := executeBlockAndApply(t, executor, state, blockContextAt(chainID, 6, uint64(redelegationCompletion)), nil) require.Empty(t, redelegationMaturityResult.ValidatorUpdates) requireNativeBalances(t, state, expectedBalances) - requireStakingPool(t, state, "20", "2") - requireStakingValidator(t, state, source, "14", "14", 3) - requireStakingValidator(t, state, destination, "6", "6", 3) - requireStakingDelegation(t, state, delegator, source, "4") - requireStakingDelegation(t, state, delegator, destination, "1") + requireStakingPool(t, state, "20000000", "2000000") + requireStakingValidator(t, state, source, "14000000", "14000000", 3) + requireStakingValidator(t, state, destination, "6000000", "6000000", 3) + requireStakingDelegation(t, state, delegator, source, "4000000") + requireStakingDelegation(t, state, delegator, destination, "1000000") requireNoStakingRedelegation(t, state, delegator, source, destination) - requireStakingUnbonding(t, state, delegator, destination, "2", undelegationCompletion) + requireStakingUnbonding(t, state, delegator, destination, "2000000", undelegationCompletion) undelegationMaturityResult := executeBlockAndApply(t, executor, state, blockContextAt(chainID, 7, uint64(undelegationCompletion)), nil) require.Empty(t, undelegationMaturityResult.ValidatorUpdates) - addExpectedBalance(expectedBalances, delegator, usei(2)) - addExpectedBalance(expectedBalances, escrowAddr, new(big.Int).Neg(usei(2))) + addExpectedBalance(expectedBalances, delegator, sei(2)) + addExpectedBalance(expectedBalances, escrowAddr, new(big.Int).Neg(sei(2))) requireNativeBalances(t, state, expectedBalances) - requireStakingPool(t, state, "20", "0") - requireStakingValidator(t, state, source, "14", "14", 3) - requireStakingValidator(t, state, destination, "6", "6", 3) - requireStakingDelegation(t, state, delegator, source, "4") - requireStakingDelegation(t, state, delegator, destination, "1") + requireStakingPool(t, state, "20000000", "0") + requireStakingValidator(t, state, source, "14000000", "14000000", 3) + requireStakingValidator(t, state, destination, "6000000", "6000000", 3) + requireStakingDelegation(t, state, delegator, source, "4000000") + requireStakingDelegation(t, state, delegator, destination, "1000000") requireNoStakingRedelegation(t, state, delegator, source, destination) requireNoStakingUnbonding(t, state, delegator, destination) } @@ -1214,8 +1214,10 @@ func mustPackStaking(t *testing.T, contract *stakingprecompile.Precompile, metho return input } -func usei(amount int64) *big.Int { - return new(big.Int).Mul(big.NewInt(amount), big.NewInt(1_000_000_000_000)) +// sei returns amount whole SEI in wei. One SEI is 1e6 usei, so a sei(n) stake +// yields n consensus power under the 1e6 powerReduction. +func sei(amount int64) *big.Int { + return new(big.Int).Mul(big.NewInt(amount), big.NewInt(1_000_000_000_000_000_000)) } func debitExpectedBalance(expected map[common.Address]*big.Int, sender common.Address, value *big.Int, tx TxResult) { diff --git a/giga/evmonly/precompiles/staking/commission.go b/giga/evmonly/precompiles/staking/commission.go new file mode 100644 index 0000000000..de5f92b77c --- /dev/null +++ b/giga/evmonly/precompiles/staking/commission.go @@ -0,0 +1,116 @@ +package staking + +import ( + "errors" + "fmt" + "math/big" + "strings" +) + +// commissionUpdateMinInterval is the minimum number of seconds between two +// commission rate changes, matching Cosmos Commission.ValidateNewRate (24h). +const commissionUpdateMinInterval = int64(24 * 60 * 60) + +var ( + oneRat = big.NewRat(1, 1) + + errCommissionNegative = errors.New("commission rate cannot be negative") + errCommissionHuge = errors.New("commission max rate cannot be greater than 100%") + errCommissionGTMaxRate = errors.New("commission rate cannot be greater than the max rate") + errCommissionChangeNegative = errors.New("commission max change rate cannot be negative") + errCommissionChangeGTMaxRate = errors.New("commission max change rate cannot be greater than the max rate") + errCommissionLTMinRate = errors.New("commission rate cannot be less than the min rate") + errCommissionGTMaxChange = errors.New("commission rate change cannot be greater than the max change rate") + errCommissionUpdateTime = errors.New("commission cannot be changed more than once in 24h") +) + +// parseRate parses a decimal commission rate string. Unlike a bare big.Rat +// parse it rejects fraction ("1/3") and scientific ("1e2") forms so the input +// space matches Cosmos sdk.Dec strings. +func parseRate(value string, name string) (*big.Rat, error) { + if value == "" || strings.ContainsAny(value, "/eE") { + return nil, fmt.Errorf("invalid %s", name) + } + rate, ok := new(big.Rat).SetString(value) + if !ok { + return nil, fmt.Errorf("invalid %s", name) + } + return rate, nil +} + +// validateInitialCommission mirrors CommissionRates.Validate plus the +// MinCommissionRate floor the staking msg server enforces on create. +func validateInitialCommission(rateStr, maxRateStr, maxChangeStr, minRateStr string) error { + rate, err := parseRate(rateStr, "commission rate") + if err != nil { + return err + } + maxRate, err := parseRate(maxRateStr, "commission max rate") + if err != nil { + return err + } + maxChange, err := parseRate(maxChangeStr, "commission max change rate") + if err != nil { + return err + } + minRate, err := parseRate(minRateStr, "min commission rate") + if err != nil { + return err + } + switch { + case maxRate.Sign() < 0: + return errCommissionNegative + case maxRate.Cmp(oneRat) > 0: + return errCommissionHuge + case rate.Sign() < 0: + return errCommissionNegative + case rate.Cmp(maxRate) > 0: + return errCommissionGTMaxRate + case maxChange.Sign() < 0: + return errCommissionChangeNegative + case maxChange.Cmp(maxRate) > 0: + return errCommissionChangeGTMaxRate + case rate.Cmp(minRate) < 0: + return errCommissionLTMinRate + } + return nil +} + +// validateCommissionUpdate mirrors Commission.ValidateNewRate plus the +// MinCommissionRate floor UpdateValidatorCommission enforces on edit. +func validateCommissionUpdate(validator Validator, newRateStr, minRateStr string, blockTime uint64) error { + newRate, err := parseRate(newRateStr, "commission rate") + if err != nil { + return err + } + oldRate, err := parseRate(validator.CommissionRate, "commission rate") + if err != nil { + return err + } + maxRate, err := parseRate(validator.CommissionMaxRate, "commission max rate") + if err != nil { + return err + } + maxChange, err := parseRate(validator.CommissionMaxChangeRate, "commission max change rate") + if err != nil { + return err + } + minRate, err := parseRate(minRateStr, "min commission rate") + if err != nil { + return err + } + if saturatingInt64FromUint64(blockTime)-validator.CommissionUpdateTime < commissionUpdateMinInterval { + return errCommissionUpdateTime + } + switch { + case newRate.Sign() < 0: + return errCommissionNegative + case newRate.Cmp(maxRate) > 0: + return errCommissionGTMaxRate + case new(big.Rat).Sub(newRate, oldRate).Cmp(maxChange) > 0: + return errCommissionGTMaxChange + case newRate.Cmp(minRate) < 0: + return errCommissionLTMinRate + } + return nil +} diff --git a/giga/evmonly/precompiles/staking/endblock.go b/giga/evmonly/precompiles/staking/endblock.go index 125bdc2d43..2a916a1ea4 100644 --- a/giga/evmonly/precompiles/staking/endblock.go +++ b/giga/evmonly/precompiles/staking/endblock.go @@ -28,6 +28,9 @@ func (p *Precompile) EndBlock(ctx *precompiles.EndBlockContext) ([]precompiles.V if err := completeMatureRedelegations(ctx.Store, ctx.Block); err != nil { return nil, err } + if err := trackHistoricalInfo(ctx.Store, ctx.Block); err != nil { + return nil, err + } return updates, nil } diff --git a/giga/evmonly/precompiles/staking/guardrails_test.go b/giga/evmonly/precompiles/staking/guardrails_test.go new file mode 100644 index 0000000000..90b672475e --- /dev/null +++ b/giga/evmonly/precompiles/staking/guardrails_test.go @@ -0,0 +1,160 @@ +package staking + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + + "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles" + "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles/util" +) + +func runStaking(t *testing.T, p *Precompile, store *memoryStore, balances *memoryBalances, caller common.Address, value *big.Int, method string, args ...interface{}) error { + t.Helper() + ctx := &precompiles.Context{ + Caller: caller, + Address: address, + ApparentValue: value, + Block: precompiles.BlockContext{Number: 1, Time: 100}, + Store: store, + Balances: balances, + Logs: &memoryLogs{}, + } + if value != nil { + balances.add(address, value) + } + input, err := p.abi.Pack(method, args...) + require.NoError(t, err) + _, err = p.Run(ctx, input) + return err +} + +func newValidator(t *testing.T, p *Precompile, store *memoryStore, balances *memoryBalances, operator common.Address, selfStakeUsei int64) { + t.Helper() + err := runStaking(t, p, store, balances, operator, new(big.Int).Mul(big.NewInt(selfStakeUsei), useiToSwei), + CreateValidatorMethod, + "01020304", + "moniker", + "0.100000000000000000", + "0.200000000000000000", + "0.010000000000000000", + big.NewInt(1), + ) + require.NoError(t, err) +} + +func TestRedelegateRejectsSelfRedelegation(t *testing.T) { + p, err := NewPrecompile() + require.NoError(t, err) + store := newMemoryStore() + balances := newMemoryBalances() + valA := common.HexToAddress("0x00000000000000000000000000000000000000a1") + newValidator(t, p, store, balances, valA, 10) + + err = runStaking(t, p, store, balances, valA, nil, RedelegateMethod, valA.Hex(), valA.Hex(), big.NewInt(1)) + require.ErrorIs(t, err, errSelfRedelegation) +} + +func TestRedelegateRejectsTransitiveRedelegation(t *testing.T) { + p, err := NewPrecompile() + require.NoError(t, err) + store := newMemoryStore() + balances := newMemoryBalances() + valA := common.HexToAddress("0x00000000000000000000000000000000000000a1") + valB := common.HexToAddress("0x00000000000000000000000000000000000000b2") + valC := common.HexToAddress("0x00000000000000000000000000000000000000c3") + newValidator(t, p, store, balances, valA, 10) + newValidator(t, p, store, balances, valB, 10) + newValidator(t, p, store, balances, valC, 10) + + // valA redelegates its self-delegation A -> B, so A now has a delegation to B + // that arrived via an in-progress redelegation. + require.NoError(t, runStaking(t, p, store, balances, valA, nil, RedelegateMethod, valA.Hex(), valB.Hex(), big.NewInt(2))) + + // Redelegating those tokens onward B -> C must be rejected as transitive. + err = runStaking(t, p, store, balances, valA, nil, RedelegateMethod, valB.Hex(), valC.Hex(), big.NewInt(1)) + require.ErrorIs(t, err, errTransitiveRedelegation) +} + +func TestRedelegateRejectsTooManyEntries(t *testing.T) { + p, err := NewPrecompile() + require.NoError(t, err) + store := newMemoryStore() + balances := newMemoryBalances() + require.NoError(t, util.SetJSON(store, paramsKey(), Params{UnbondingTime: 1, MaxValidators: 100, MaxEntries: 1, MinCommissionRate: "0.000000000000000000"})) + valA := common.HexToAddress("0x00000000000000000000000000000000000000a1") + valB := common.HexToAddress("0x00000000000000000000000000000000000000b2") + newValidator(t, p, store, balances, valA, 10) + newValidator(t, p, store, balances, valB, 10) + + require.NoError(t, runStaking(t, p, store, balances, valA, nil, RedelegateMethod, valA.Hex(), valB.Hex(), big.NewInt(2))) + err = runStaking(t, p, store, balances, valA, nil, RedelegateMethod, valA.Hex(), valB.Hex(), big.NewInt(2)) + require.ErrorIs(t, err, errMaxRedelegationEntries) +} + +func TestUndelegateRejectsTooManyEntries(t *testing.T) { + p, err := NewPrecompile() + require.NoError(t, err) + store := newMemoryStore() + balances := newMemoryBalances() + require.NoError(t, util.SetJSON(store, paramsKey(), Params{UnbondingTime: 1, MaxValidators: 100, MaxEntries: 1, MinCommissionRate: "0.000000000000000000"})) + valA := common.HexToAddress("0x00000000000000000000000000000000000000a1") + newValidator(t, p, store, balances, valA, 10) + + require.NoError(t, runStaking(t, p, store, balances, valA, nil, UndelegateMethod, valA.Hex(), big.NewInt(2))) + err = runStaking(t, p, store, balances, valA, nil, UndelegateMethod, valA.Hex(), big.NewInt(2)) + require.ErrorIs(t, err, errMaxUnbondingEntries) +} + +func TestCreateValidatorRejectsBadCommission(t *testing.T) { + p, err := NewPrecompile() + require.NoError(t, err) + store := newMemoryStore() + balances := newMemoryBalances() + valA := common.HexToAddress("0x00000000000000000000000000000000000000a1") + + // rate greater than the max rate + err = runStaking(t, p, store, balances, valA, new(big.Int).Mul(big.NewInt(10), useiToSwei), + CreateValidatorMethod, "01020304", "moniker", + "0.300000000000000000", "0.200000000000000000", "0.010000000000000000", big.NewInt(1)) + require.ErrorIs(t, err, errCommissionGTMaxRate) + + // max rate greater than 100% + err = runStaking(t, p, store, balances, valA, new(big.Int).Mul(big.NewInt(10), useiToSwei), + CreateValidatorMethod, "01020304", "moniker", + "0.100000000000000000", "1.500000000000000000", "0.010000000000000000", big.NewInt(1)) + require.ErrorIs(t, err, errCommissionHuge) +} + +func TestValidateInitialCommission(t *testing.T) { + require.NoError(t, validateInitialCommission("0.1", "0.2", "0.01", "0")) + require.NoError(t, validateInitialCommission("0.05", "0.05", "0.05", "0.05")) + require.ErrorIs(t, validateInitialCommission("-0.1", "0.2", "0.01", "0"), errCommissionNegative) + require.ErrorIs(t, validateInitialCommission("0.3", "0.2", "0.01", "0"), errCommissionGTMaxRate) + require.ErrorIs(t, validateInitialCommission("0.1", "1.1", "0.01", "0"), errCommissionHuge) + require.ErrorIs(t, validateInitialCommission("0.1", "0.2", "0.3", "0"), errCommissionChangeGTMaxRate) + require.ErrorIs(t, validateInitialCommission("0.01", "0.2", "0.01", "0.05"), errCommissionLTMinRate) + // fraction and scientific notation forms are rejected. + require.Error(t, validateInitialCommission("1/3", "0.2", "0.01", "0")) + require.Error(t, validateInitialCommission("1e-1", "0.2", "0.01", "0")) +} + +func TestValidateCommissionUpdate(t *testing.T) { + validator := Validator{ + CommissionRate: "0.100000000000000000", + CommissionMaxRate: "0.200000000000000000", + CommissionMaxChangeRate: "0.010000000000000000", + CommissionUpdateTime: 0, + } + dayLater := uint64(commissionUpdateMinInterval) + + require.NoError(t, validateCommissionUpdate(validator, "0.105", "0", dayLater)) + // too soon after the last change + require.ErrorIs(t, validateCommissionUpdate(validator, "0.105", "0", dayLater-1), errCommissionUpdateTime) + // change larger than the max change rate + require.ErrorIs(t, validateCommissionUpdate(validator, "0.2", "0", dayLater), errCommissionGTMaxChange) + // below the min commission rate + require.ErrorIs(t, validateCommissionUpdate(validator, "0.05", "0.08", dayLater), errCommissionLTMinRate) +} diff --git a/giga/evmonly/precompiles/staking/staking.go b/giga/evmonly/precompiles/staking/staking.go index ff792641af..5240a8c4db 100644 --- a/giga/evmonly/precompiles/staking/staking.go +++ b/giga/evmonly/precompiles/staking/staking.go @@ -56,7 +56,9 @@ const ( bondStatusUnbonding int32 = 2 bondStatusBonded int32 = 3 - powerReduction int64 = 1 + // powerReduction matches Cosmos sdk.DefaultPowerReduction so consensus + // power is denominated in whole SEI (1e6 usei == 1 power). + powerReduction int64 = 1_000_000 ) var ( @@ -67,6 +69,12 @@ var ( errMissingStore = errors.New("staking precompile requires a store") errMissingBalanceTransfer = errors.New("staking precompile requires balance transfer") errValidatorMissing = errors.New("validator not found") + errSelfRedelegation = errors.New("cannot redelegate to the same validator") + errTransitiveRedelegation = errors.New("redelegation to this validator already in progress; first redelegation not complete") + errMaxRedelegationEntries = errors.New("too many redelegation entries for (delegator, src-validator, dst-validator) tuple") + errMaxUnbondingEntries = errors.New("too many unbonding delegation entries for (delegator, validator) tuple") + errMinSelfDelegation = errors.New("minimum self delegation must be greater than the current value") + errSelfDelegationTooLow = errors.New("minimum self delegation cannot be greater than the validator's self delegation") ) //go:embed abi.json @@ -265,6 +273,9 @@ func (p *Precompile) redelegate(ctx *precompiles.Context, method *abi.Method, ar if err := util.ValidatePositiveAmount(amount, "redelegation amount"); err != nil { return nil, err } + if srcValidator == dstValidator { + return nil, errSelfRedelegation + } src, ok, err := getValidator(ctx.Store, srcValidator) if err != nil { return nil, err @@ -279,6 +290,25 @@ func (p *Precompile) redelegate(ctx *precompiles.Context, method *abi.Method, ar if !ok { return nil, fmt.Errorf("destination %w", errValidatorMissing) } + params, err := loadParams(ctx.Store) + if err != nil { + return nil, err + } + // Disallow transitive redelegations: tokens already received via an + // in-progress redelegation into srcValidator cannot be redelegated again + // until that redelegation completes (matches Cosmos HasReceivingRedelegation). + receiving, err := hasReceivingRedelegation(ctx.Store, delegator, srcValidator) + if err != nil { + return nil, err + } + if receiving { + return nil, errTransitiveRedelegation + } + if existing, ok, err := getRedelegation(ctx.Store, delegator, srcValidator, dstValidator); err != nil { + return nil, err + } else if ok && len(existing.Entries) >= int(params.MaxEntries) { + return nil, errMaxRedelegationEntries + } if err := validateDelegationAmount(ctx.Store, delegator, srcValidator, amount); err != nil { return nil, err } @@ -297,10 +327,6 @@ func (p *Precompile) redelegate(ctx *precompiles.Context, method *abi.Method, ar if err := movePoolsForRedelegation(ctx.Store, src.Status, dst.Status, amount); err != nil { return nil, err } - params, err := loadParams(ctx.Store) - if err != nil { - return nil, err - } if err := addRedelegation(ctx.Store, delegator, srcValidator, dstValidator, amount, util.SaturatingCompletionTime(ctx.Block.Time, params.UnbondingTime)); err != nil { return nil, err } @@ -333,6 +359,15 @@ func (p *Precompile) undelegate(ctx *precompiles.Context, method *abi.Method, ar if !ok { return nil, errValidatorMissing } + params, err := loadParams(ctx.Store) + if err != nil { + return nil, err + } + if existing, ok, err := getUnbondingDelegation(ctx.Store, delegator, validatorAddress); err != nil { + return nil, err + } else if ok && len(existing.Entries) >= int(params.MaxEntries) { + return nil, errMaxUnbondingEntries + } if err := validateDelegationAmount(ctx.Store, delegator, validatorAddress, amount); err != nil { return nil, err } @@ -342,10 +377,6 @@ func (p *Precompile) undelegate(ctx *precompiles.Context, method *abi.Method, ar if err := addValidatorTokens(ctx.Store, validatorAddress, new(big.Int).Neg(amount)); err != nil { return nil, err } - params, err := loadParams(ctx.Store) - if err != nil { - return nil, err - } if err := addUnbondingDelegation(ctx.Store, delegator, validatorAddress, amount, ctx.Block.Number, util.SaturatingCompletionTime(ctx.Block.Time, params.UnbondingTime)); err != nil { return nil, err } @@ -379,13 +410,11 @@ func (p *Precompile) createValidator(ctx *precompiles.Context, method *abi.Metho if err != nil { return nil, errors.New("invalid public key hex format") } - if err := util.ValidateDecimal(commissionRate, "commission rate"); err != nil { - return nil, err - } - if err := util.ValidateDecimal(commissionMaxRate, "commission max rate"); err != nil { + params, err := loadParams(ctx.Store) + if err != nil { return nil, err } - if err := util.ValidateDecimal(commissionMaxChangeRate, "commission max change rate"); err != nil { + if err := validateInitialCommission(commissionRate, commissionMaxRate, commissionMaxChangeRate, params.MinCommissionRate); err != nil { return nil, err } if err := util.ValidatePositiveAmount(minSelfDelegation, "minimum self delegation"); err != nil { @@ -464,13 +493,31 @@ func (p *Precompile) editValidator(ctx *precompiles.Context, method *abi.Method, validator.Description = moniker } if commissionRate != "" { - if err := util.ValidateDecimal(commissionRate, "commission rate"); err != nil { + params, err := loadParams(ctx.Store) + if err != nil { + return nil, err + } + if err := validateCommissionUpdate(validator, commissionRate, params.MinCommissionRate, ctx.Block.Time); err != nil { return nil, err } validator.CommissionRate = commissionRate validator.CommissionUpdateTime = saturatingInt64FromUint64(ctx.Block.Time) } if minSelfDelegation != nil && minSelfDelegation.Sign() > 0 { + current, err := util.ParseAmount(validator.MinSelfDelegation) + if err != nil { + return nil, err + } + if minSelfDelegation.Cmp(current) <= 0 { + return nil, errMinSelfDelegation + } + tokens, err := util.ParseAmount(validator.Tokens) + if err != nil { + return nil, err + } + if minSelfDelegation.Cmp(tokens) > 0 { + return nil, errSelfDelegationTooLow + } validator.MinSelfDelegation = minSelfDelegation.String() } if err := setValidator(ctx.Store, validator); err != nil { @@ -520,6 +567,7 @@ func (p *Precompile) validators(ctx *precompiles.Context, method *abi.Method, ar return nil, err } filtered := make([]string, 0, len(validatorAddresses)) + matched := make(map[string]Validator, len(validatorAddresses)) for _, validatorAddress := range validatorAddresses { validator, ok, err := getValidator(ctx.Store, validatorAddress) if err != nil { @@ -527,6 +575,7 @@ func (p *Precompile) validators(ctx *precompiles.Context, method *abi.Method, ar } if ok && statusMatches(status, validator.Status) { filtered = append(filtered, validatorAddress) + matched[validatorAddress] = validator } } page, outNextKey, err := pageStrings(filtered, nextKey) @@ -535,13 +584,7 @@ func (p *Precompile) validators(ctx *precompiles.Context, method *abi.Method, ar } result := ValidatorsResponse{Validators: make([]Validator, 0, len(page)), NextKey: outNextKey} for _, validatorAddress := range page { - validator, ok, err := getValidator(ctx.Store, validatorAddress) - if err != nil { - return nil, err - } - if ok { - result.Validators = append(result.Validators, validator) - } + result.Validators = append(result.Validators, matched[validatorAddress]) } return method.Outputs.Pack(result) } @@ -832,19 +875,7 @@ func (p *Precompile) historicalInfo(ctx *precompiles.Context, method *abi.Method return nil, err } if !ok { - if height < 0 || uint64(height) != ctx.Block.Number { - return nil, errors.New("historical info not found") - } - if err := setHistoricalInfo(ctx.Store, ctx.Block.Number); err != nil { - return nil, err - } - info, ok, err = getHistoricalInfo(ctx.Store, height) - if err != nil { - return nil, err - } - if !ok { - return nil, errors.New("historical info not found") - } + return nil, errors.New("historical info not found") } return method.Outputs.Pack(info) } diff --git a/giga/evmonly/precompiles/staking/staking_test.go b/giga/evmonly/precompiles/staking/staking_test.go index 321a5dbbb9..ae5e5046aa 100644 --- a/giga/evmonly/precompiles/staking/staking_test.go +++ b/giga/evmonly/precompiles/staking/staking_test.go @@ -17,13 +17,18 @@ func TestPrecompileCreateDelegateAndQuery(t *testing.T) { require.NoError(t, err) caller := common.HexToAddress("0x0000000000000000000000000000000000000abc") + // Stakes are denominated in whole SEI (1e6 usei) so validators clear the + // powerReduction threshold and receive non-zero consensus power. + selfStakeUsei := big.NewInt(5_000_000) + delegateUsei := big.NewInt(2_000_000) + totalUsei := big.NewInt(7_000_000) store := newMemoryStore() logs := &memoryLogs{} balances := newMemoryBalances() ctx := &precompiles.Context{ Caller: caller, Address: address, - ApparentValue: new(big.Int).Mul(big.NewInt(5), useiToSwei), + ApparentValue: new(big.Int).Mul(selfStakeUsei, useiToSwei), Block: precompiles.BlockContext{Number: 7, Time: 100}, Store: store, Balances: balances, @@ -45,11 +50,11 @@ func TestPrecompileCreateDelegateAndQuery(t *testing.T) { require.NoError(t, err) requireBoolReturn(t, p, CreateValidatorMethod, ret, true) require.Len(t, logs.logs, 1) - require.Equal(t, new(big.Int).Mul(big.NewInt(5), useiToSwei), balances.balance(EscrowAddress())) + require.Equal(t, new(big.Int).Mul(selfStakeUsei, useiToSwei), balances.balance(EscrowAddress())) require.Zero(t, balances.balance(caller).Sign()) require.Zero(t, balances.balance(address).Sign()) - ctx.ApparentValue = new(big.Int).Mul(big.NewInt(2), useiToSwei) + ctx.ApparentValue = new(big.Int).Mul(delegateUsei, useiToSwei) input, err = p.abi.Pack(DelegateMethod, caller.Hex()) require.NoError(t, err) balances.add(address, ctx.ApparentValue) @@ -57,7 +62,7 @@ func TestPrecompileCreateDelegateAndQuery(t *testing.T) { require.NoError(t, err) requireBoolReturn(t, p, DelegateMethod, ret, true) require.Len(t, logs.logs, 3) - require.Equal(t, new(big.Int).Mul(big.NewInt(7), useiToSwei), balances.balance(EscrowAddress())) + require.Equal(t, new(big.Int).Mul(totalUsei, useiToSwei), balances.balance(EscrowAddress())) require.Zero(t, balances.balance(caller).Sign()) require.Zero(t, balances.balance(address).Sign()) @@ -70,6 +75,7 @@ func TestPrecompileCreateDelegateAndQuery(t *testing.T) { }) require.NoError(t, err) require.Len(t, updates, 1) + require.Equal(t, int64(7), updates[0].Power) ctx.ApparentValue = nil input, err = p.abi.Pack(DelegationMethod, caller, caller.Hex()) @@ -81,8 +87,11 @@ func TestPrecompileCreateDelegateAndQuery(t *testing.T) { } require.NoError(t, p.abi.UnpackIntoInterface(&delegationOut, DelegationMethod, ret)) delegation := delegationOut.Delegation - require.Equal(t, big.NewInt(7), delegation.Balance.Amount) + require.Equal(t, totalUsei, delegation.Balance.Amount) require.Equal(t, "usei", delegation.Balance.Denom) + // Shares are reported as an sdk.Dec (token count scaled by 10^decimals). + require.Equal(t, new(big.Int).Mul(totalUsei, sharesScalingFactor), delegation.Delegation.Shares) + require.Equal(t, big.NewInt(precision), delegation.Delegation.Decimals) require.Equal(t, caller.Hex(), delegation.Delegation.DelegatorAddress) require.Equal(t, caller.Hex(), delegation.Delegation.ValidatorAddress) @@ -95,7 +104,7 @@ func TestPrecompileCreateDelegateAndQuery(t *testing.T) { } require.NoError(t, p.abi.UnpackIntoInterface(&poolOut, PoolMethod, ret)) pool := poolOut.Pool - require.Equal(t, "7", pool.BondedTokens) + require.Equal(t, "7000000", pool.BondedTokens) require.Equal(t, "0", pool.NotBondedTokens) input, err = p.abi.Pack(ValidatorsMethod, "BOND_STATUS_BONDED", []byte{}) diff --git a/giga/evmonly/precompiles/staking/state.go b/giga/evmonly/precompiles/staking/state.go index b551ab14dd..ed9eddba3f 100644 --- a/giga/evmonly/precompiles/staking/state.go +++ b/giga/evmonly/precompiles/staking/state.go @@ -11,6 +11,10 @@ import ( "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles/util" ) +// sharesScalingFactor is 10^precision, matching the fixed-point scale Cosmos +// sdk.Dec uses to report delegation shares. +var sharesScalingFactor = new(big.Int).Exp(big.NewInt(10), big.NewInt(precision), nil) + func addDelegation(store precompiles.Store, delegator string, validator string, delta *big.Int) error { record, ok, err := getDelegation(store, delegator, validator) if err != nil { @@ -82,7 +86,10 @@ func delegationFromRecord(record delegationRecord) (Delegation, error) { }, Delegation: DelegationDetails{ DelegatorAddress: record.DelegatorAddress, - Shares: new(big.Int).Set(amount), + // Shares are reported as a Cosmos sdk.Dec, i.e. scaled by + // 10^precision, so callers recover the share count as + // shares / 10^decimals. With no slashing shares == tokens. + Shares: new(big.Int).Mul(amount, sharesScalingFactor), Decimals: big.NewInt(precision), ValidatorAddress: record.ValidatorAddress, }, @@ -187,6 +194,26 @@ func getRedelegation(store precompiles.Store, delegator string, srcValidator str return util.GetJSON[Redelegation](store, redelegationKey(delegator, srcValidator, dstValidator)) } +// hasReceivingRedelegation reports whether the delegator has an in-progress +// redelegation whose destination is validator, mirroring Cosmos +// HasReceivingRedelegation (used to reject transitive redelegations). +func hasReceivingRedelegation(store precompiles.Store, delegator string, validator string) (bool, error) { + ids, err := getStringList(store, redelegationsIndexKey()) + if err != nil { + return false, err + } + for _, id := range ids { + recordDelegator, _, dst, ok := splitRedelegationID(id) + if !ok { + continue + } + if recordDelegator == delegator && dst == validator { + return true, nil + } + } + return false, nil +} + func loadParams(store precompiles.Store) (Params, error) { params, ok, err := util.GetJSON[Params](store, paramsKey()) if err != nil { @@ -293,13 +320,58 @@ func setHistoricalInfo(store precompiles.Store, height uint64) error { info.Validators = append(info.Validators, validator) } } - return util.SetJSON(store, historicalInfoKey(info.Height), info) + if err := util.SetJSON(store, historicalInfoKey(info.Height), info); err != nil { + return err + } + return addStringListItem(store, historicalInfoIndexKey(), strconv.FormatInt(info.Height, 10)) } func getHistoricalInfo(store precompiles.Store, height int64) (HistoricalInfo, bool, error) { return util.GetJSON[HistoricalInfo](store, historicalInfoKey(height)) } +func deleteHistoricalInfo(store precompiles.Store, height int64) error { + store.Delete(historicalInfoKey(height)) + return removeStringListItem(store, historicalInfoIndexKey(), strconv.FormatInt(height, 10)) +} + +// trackHistoricalInfo records the current block's historical info and prunes +// entries beyond HistoricalEntries, mirroring Cosmos TrackHistoricalInfo (which +// runs in BeginBlock; the evm-only path only has an end-block hook). +func trackHistoricalInfo(store precompiles.Store, block precompiles.BlockContext) error { + params, err := loadParams(store) + if err != nil { + return err + } + if err := pruneHistoricalInfo(store, block.Number, params.HistoricalEntries); err != nil { + return err + } + if params.HistoricalEntries == 0 { + return nil + } + return setHistoricalInfo(store, block.Number) +} + +func pruneHistoricalInfo(store precompiles.Store, currentHeight uint64, historicalEntries uint32) error { + heights, err := getStringList(store, historicalInfoIndexKey()) + if err != nil { + return err + } + cutoff := saturatingInt64FromUint64(currentHeight) - int64(historicalEntries) + for _, heightStr := range heights { + height, err := strconv.ParseInt(heightStr, 10, 64) + if err != nil { + return err + } + if height <= cutoff { + if err := deleteHistoricalInfo(store, height); err != nil { + return err + } + } + } + return nil +} + func getStringList(store precompiles.Store, key []byte) ([]string, error) { items, ok, err := util.GetJSON[[]string](store, key) if err != nil || !ok { @@ -612,6 +684,10 @@ func historicalInfoKey(height int64) []byte { return []byte("historical/" + strconv.FormatInt(height, 10)) } +func historicalInfoIndexKey() []byte { + return []byte("historical/index") +} + func lastValidatorsIndexKey() []byte { return []byte("last-validators/index") } diff --git a/giga/evmonly/precompiles/util/helpers.go b/giga/evmonly/precompiles/util/helpers.go index ae2b2704a2..bbb58ebf96 100644 --- a/giga/evmonly/precompiles/util/helpers.go +++ b/giga/evmonly/precompiles/util/helpers.go @@ -47,16 +47,6 @@ func ValidatePositiveAmount(amount *big.Int, name string) error { return nil } -func ValidateDecimal(value string, name string) error { - if value == "" { - return fmt.Errorf("invalid %s", name) - } - if _, ok := new(big.Rat).SetString(value); !ok { - return fmt.Errorf("invalid %s", name) - } - return nil -} - func SaturatingCompletionTime(blockTime uint64, offset uint64) int64 { if blockTime > uint64(1<<63-1) || offset > uint64(1<<63-1)-blockTime { return int64(1<<63 - 1) From 778734a44132decb1b15f44184ebfd267e2f32fd Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Wed, 24 Jun 2026 12:37:25 +0800 Subject: [PATCH 25/27] address staking cursor review comments --- .../precompiles/staking/guardrails_test.go | 50 ++++++++++++++++++- giga/evmonly/precompiles/staking/helpers.go | 9 ++++ giga/evmonly/precompiles/staking/staking.go | 32 +++++++----- giga/evmonly/precompiles/staking/state.go | 19 +++++++ 4 files changed, 96 insertions(+), 14 deletions(-) diff --git a/giga/evmonly/precompiles/staking/guardrails_test.go b/giga/evmonly/precompiles/staking/guardrails_test.go index 90b672475e..ebaba619e9 100644 --- a/giga/evmonly/precompiles/staking/guardrails_test.go +++ b/giga/evmonly/precompiles/staking/guardrails_test.go @@ -2,6 +2,7 @@ package staking import ( "math/big" + "strings" "testing" "github.com/ethereum/go-ethereum/common" @@ -35,7 +36,7 @@ func newValidator(t *testing.T, p *Precompile, store *memoryStore, balances *mem t.Helper() err := runStaking(t, p, store, balances, operator, new(big.Int).Mul(big.NewInt(selfStakeUsei), useiToSwei), CreateValidatorMethod, - "01020304", + operator.Hex()[2:], "moniker", "0.100000000000000000", "0.200000000000000000", @@ -45,6 +46,53 @@ func newValidator(t *testing.T, p *Precompile, store *memoryStore, balances *mem require.NoError(t, err) } +func TestCreateValidatorRejectsDuplicateConsensusPubkey(t *testing.T) { + p, err := NewPrecompile() + require.NoError(t, err) + store := newMemoryStore() + balances := newMemoryBalances() + valA := common.HexToAddress("0x00000000000000000000000000000000000000a1") + valB := common.HexToAddress("0x00000000000000000000000000000000000000b2") + + require.NoError(t, runStaking(t, p, store, balances, valA, new(big.Int).Mul(big.NewInt(10), useiToSwei), + CreateValidatorMethod, "01020304", "validator-a", + "0.100000000000000000", "0.200000000000000000", "0.010000000000000000", big.NewInt(1))) + err = runStaking(t, p, store, balances, valB, new(big.Int).Mul(big.NewInt(10), useiToSwei), + CreateValidatorMethod, "01020304", "validator-b", + "0.100000000000000000", "0.200000000000000000", "0.010000000000000000", big.NewInt(1)) + require.ErrorIs(t, err, errDuplicateConsensusKey) +} + +func TestValidatorInputsAcceptLowercaseHex(t *testing.T) { + p, err := NewPrecompile() + require.NoError(t, err) + store := newMemoryStore() + balances := newMemoryBalances() + valA := common.HexToAddress("0x00000000000000000000000000000000000000a1") + valB := common.HexToAddress("0x00000000000000000000000000000000000000b2") + lowerValA := strings.ToLower(valA.Hex()) + lowerValB := strings.ToLower(valB.Hex()) + newValidator(t, p, store, balances, valA, 10) + newValidator(t, p, store, balances, valB, 10) + + require.NoError(t, runStaking(t, p, store, balances, valA, new(big.Int).Mul(big.NewInt(1), useiToSwei), DelegateMethod, lowerValA)) + require.NoError(t, runStaking(t, p, store, balances, valA, nil, RedelegateMethod, lowerValA, lowerValB, big.NewInt(1))) + require.NoError(t, runStaking(t, p, store, balances, valA, nil, UndelegateMethod, lowerValB, big.NewInt(1))) + + input, err := p.abi.Pack(ValidatorMethod, lowerValA) + require.NoError(t, err) + _, err = p.Run(&precompiles.Context{ + Caller: valA, + Address: address, + ApparentValue: nil, + Block: precompiles.BlockContext{Number: 1, Time: 100}, + Store: store, + Balances: balances, + Logs: &memoryLogs{}, + }, input) + require.NoError(t, err) +} + func TestRedelegateRejectsSelfRedelegation(t *testing.T) { p, err := NewPrecompile() require.NoError(t, err) diff --git a/giga/evmonly/precompiles/staking/helpers.go b/giga/evmonly/precompiles/staking/helpers.go index 3ca29e2260..be31cf08af 100644 --- a/giga/evmonly/precompiles/staking/helpers.go +++ b/giga/evmonly/precompiles/staking/helpers.go @@ -7,6 +7,8 @@ import ( "strconv" "strings" + "github.com/ethereum/go-ethereum/common" + "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles" ) @@ -34,6 +36,13 @@ func validateWritable(ctx *precompiles.Context) error { return nil } +func normalizeValidatorAddress(validatorAddress string) string { + if common.IsHexAddress(validatorAddress) { + return common.HexToAddress(validatorAddress).Hex() + } + return validatorAddress +} + func statusMatches(filter string, status int32) bool { if filter == "" { return true diff --git a/giga/evmonly/precompiles/staking/staking.go b/giga/evmonly/precompiles/staking/staking.go index 5240a8c4db..bae0f7ba7c 100644 --- a/giga/evmonly/precompiles/staking/staking.go +++ b/giga/evmonly/precompiles/staking/staking.go @@ -73,6 +73,7 @@ var ( errTransitiveRedelegation = errors.New("redelegation to this validator already in progress; first redelegation not complete") errMaxRedelegationEntries = errors.New("too many redelegation entries for (delegator, src-validator, dst-validator) tuple") errMaxUnbondingEntries = errors.New("too many unbonding delegation entries for (delegator, validator) tuple") + errDuplicateConsensusKey = errors.New("validator consensus pubkey already exists") errMinSelfDelegation = errors.New("minimum self delegation must be greater than the current value") errSelfDelegationTooLow = errors.New("minimum self delegation cannot be greater than the validator's self delegation") ) @@ -222,7 +223,7 @@ func (p *Precompile) delegate(ctx *precompiles.Context, method *abi.Method, args if err := util.ValidateArgsLength(args, 1); err != nil { return nil, err } - validatorAddress := args[0].(string) + validatorAddress := normalizeValidatorAddress(args[0].(string)) validator, ok, err := getValidator(ctx.Store, validatorAddress) if err != nil { return nil, err @@ -267,8 +268,8 @@ func (p *Precompile) redelegate(ctx *precompiles.Context, method *abi.Method, ar return nil, err } delegator := util.AddressString(ctx.Caller) - srcValidator := args[0].(string) - dstValidator := args[1].(string) + srcValidator := normalizeValidatorAddress(args[0].(string)) + dstValidator := normalizeValidatorAddress(args[1].(string)) amount := args[2].(*big.Int) if err := util.ValidatePositiveAmount(amount, "redelegation amount"); err != nil { return nil, err @@ -347,7 +348,7 @@ func (p *Precompile) undelegate(ctx *precompiles.Context, method *abi.Method, ar return nil, err } delegator := util.AddressString(ctx.Caller) - validatorAddress := args[0].(string) + validatorAddress := normalizeValidatorAddress(args[0].(string)) amount := args[1].(*big.Int) if err := util.ValidatePositiveAmount(amount, "undelegation amount"); err != nil { return nil, err @@ -433,6 +434,11 @@ func (p *Precompile) createValidator(ctx *precompiles.Context, method *abi.Metho } else if exists { return nil, errors.New("validator already exists") } + if _, exists, err := getValidatorByConsensusPubkey(ctx.Store, pubKey); err != nil { + return nil, err + } else if exists { + return nil, errDuplicateConsensusKey + } if err := transferPrecompileValueToEscrow(ctx); err != nil { return nil, err } @@ -538,7 +544,7 @@ func (p *Precompile) delegation(ctx *precompiles.Context, method *abi.Method, ar return nil, err } delegator := util.AddressString(args[0].(common.Address)) - validatorAddress := args[1].(string) + validatorAddress := normalizeValidatorAddress(args[1].(string)) record, ok, err := getDelegation(ctx.Store, delegator, validatorAddress) if err != nil { return nil, err @@ -596,7 +602,7 @@ func (p *Precompile) validator(ctx *precompiles.Context, method *abi.Method, arg if err := util.ValidateArgsLength(args, 1); err != nil { return nil, err } - validator, ok, err := getValidator(ctx.Store, args[0].(string)) + validator, ok, err := getValidator(ctx.Store, normalizeValidatorAddress(args[0].(string))) if err != nil { return nil, err } @@ -613,7 +619,7 @@ func (p *Precompile) validatorDelegations(ctx *precompiles.Context, method *abi. if err := util.ValidateArgsLength(args, 2); err != nil { return nil, err } - validatorAddress := args[0].(string) + validatorAddress := normalizeValidatorAddress(args[0].(string)) nextKey := args[1].([]byte) delegators, err := getStringList(ctx.Store, validatorDelegationsIndexKey(validatorAddress)) if err != nil { @@ -648,7 +654,7 @@ func (p *Precompile) validatorUnbondingDelegations(ctx *precompiles.Context, met if err := util.ValidateArgsLength(args, 2); err != nil { return nil, err } - validatorAddress := args[0].(string) + validatorAddress := normalizeValidatorAddress(args[0].(string)) nextKey := args[1].([]byte) delegators, err := getStringList(ctx.Store, validatorUnbondingsIndexKey(validatorAddress)) if err != nil { @@ -679,7 +685,7 @@ func (p *Precompile) unbondingDelegation(ctx *precompiles.Context, method *abi.M return nil, err } delegator := util.AddressString(args[0].(common.Address)) - validatorAddress := args[1].(string) + validatorAddress := normalizeValidatorAddress(args[1].(string)) record, ok, err := getUnbondingDelegation(ctx.Store, delegator, validatorAddress) if err != nil { return nil, err @@ -733,7 +739,7 @@ func (p *Precompile) delegatorValidator(ctx *precompiles.Context, method *abi.Me return nil, err } delegator := util.AddressString(args[0].(common.Address)) - validatorAddress := args[1].(string) + validatorAddress := normalizeValidatorAddress(args[1].(string)) if _, ok, err := getDelegation(ctx.Store, delegator, validatorAddress); err != nil { return nil, err } else if !ok { @@ -786,9 +792,9 @@ func (p *Precompile) redelegations(ctx *precompiles.Context, method *abi.Method, if err := util.ValidateArgsLength(args, 4); err != nil { return nil, err } - delegatorFilter := args[0].(string) - srcFilter := args[1].(string) - dstFilter := args[2].(string) + delegatorFilter := normalizeValidatorAddress(args[0].(string)) + srcFilter := normalizeValidatorAddress(args[1].(string)) + dstFilter := normalizeValidatorAddress(args[2].(string)) nextKey := args[3].([]byte) ids, err := getStringList(ctx.Store, redelegationsIndexKey()) if err != nil { diff --git a/giga/evmonly/precompiles/staking/state.go b/giga/evmonly/precompiles/staking/state.go index ed9eddba3f..11960ee2a5 100644 --- a/giga/evmonly/precompiles/staking/state.go +++ b/giga/evmonly/precompiles/staking/state.go @@ -1,6 +1,7 @@ package staking import ( + "encoding/hex" "errors" "math/big" "sort" @@ -100,6 +101,9 @@ func setValidator(store precompiles.Store, validator Validator) error { if err := util.SetJSON(store, validatorKey(validator.OperatorAddress), validator); err != nil { return err } + if err := util.SetJSON(store, validatorConsensusPubkeyKey(validator.ConsensusPubkey), validator.OperatorAddress); err != nil { + return err + } return addStringListItem(store, validatorsIndexKey(), validator.OperatorAddress) } @@ -107,8 +111,19 @@ func getValidator(store precompiles.Store, validatorAddress string) (Validator, return util.GetJSON[Validator](store, validatorKey(validatorAddress)) } +func getValidatorByConsensusPubkey(store precompiles.Store, pubKey []byte) (string, bool, error) { + return util.GetJSON[string](store, validatorConsensusPubkeyKey(pubKey)) +} + func removeValidator(store precompiles.Store, validatorAddress string) error { + validator, ok, err := getValidator(store, validatorAddress) + if err != nil { + return err + } store.Delete(validatorKey(validatorAddress)) + if ok { + store.Delete(validatorConsensusPubkeyKey(validator.ConsensusPubkey)) + } return removeStringListItem(store, validatorsIndexKey(), validatorAddress) } @@ -636,6 +651,10 @@ func validatorKey(validator string) []byte { return []byte("validator/" + validator) } +func validatorConsensusPubkeyKey(pubKey []byte) []byte { + return []byte("validator-consensus-pubkey/" + hex.EncodeToString(pubKey)) +} + func delegationKey(delegator string, validator string) []byte { return []byte("delegation/" + delegator + "/" + validator) } From 874948599b734e15d5c79a94006ce13d5f3afa3b Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Mon, 29 Jun 2026 20:34:19 +0800 Subject: [PATCH 26/27] run evmonly endblock on empty custom-precompile blocks --- giga/evmonly/executor.go | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/giga/evmonly/executor.go b/giga/evmonly/executor.go index 5368ef7961..d9ad18abf5 100644 --- a/giga/evmonly/executor.go +++ b/giga/evmonly/executor.go @@ -93,7 +93,7 @@ func (e *Executor) PrepareBlock(ctx context.Context, req BlockRequest) (Prepared func (e *Executor) ExecutePreparedBlock(ctx context.Context, req PreparedBlock) (*BlockResult, error) { var result *BlockResult var err error - if len(req.Txs) == 0 { + if len(req.Txs) == 0 && !e.hasCustomPrecompiles() { result, err = e.acquireBlockResult(ctx, 0) } else if e.useOCC(len(req.Txs)) { result, err = e.executeBlockOCC(ctx, req) @@ -148,14 +148,18 @@ func (e *Executor) sinkBlockResult(ctx context.Context, height uint64, result *B return nil } +func (e *Executor) hasCustomPrecompiles() bool { + if e.cfg.CustomPrecompiles == nil { + return false + } + return len(e.cfg.CustomPrecompiles.Addresses()) > 0 +} + 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 + return !e.hasCustomPrecompiles() } func (e *Executor) executeBlockSequential(ctx context.Context, req PreparedBlock) (*BlockResult, error) { From b2232eca08b8c9447486de74b274b500df1572f1 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Mon, 27 Jul 2026 21:30:10 +0800 Subject: [PATCH 27/27] [codex] add dynamic custom precompile gas meter (#3644) ## Summary Adds dynamic gas accounting for evm-only custom precompiles, with the staking precompile using the shared meter through the existing store, balance-transfer, and log boundaries. ## Changes - Add a reusable precompile gas meter for base gas, SLOAD/SSTORE-style storage costs, keccak slot derivation, native value transfers, and log emission. - Wrap `storageBackedStore`, `nativeBalanceTransfer`, and log emission in the custom precompile adapter so gas is charged based on the actual execution path. - Reduce staking `RequiredGas` to only base/input gas, avoiding double charging now that state access is dynamically metered. - Add executor coverage for dynamic store gas and update staking lifecycle tests to use realistic gas limits. ## Validation - `go test ./giga/evmonly/...` - `golangci-lint v2.8.0 run ./giga/evmonly/...` --- giga/evmonly/executor_test.go | 34 ++- giga/evmonly/precompile_adapter.go | 84 +++++-- giga/evmonly/precompile_gas.go | 231 ++++++++++++++++++++ giga/evmonly/precompiles/staking/staking.go | 11 +- 4 files changed, 333 insertions(+), 27 deletions(-) create mode 100644 giga/evmonly/precompile_gas.go diff --git a/giga/evmonly/executor_test.go b/giga/evmonly/executor_test.go index 9b70bd7f45..50bf94f4b1 100644 --- a/giga/evmonly/executor_test.go +++ b/giga/evmonly/executor_test.go @@ -869,12 +869,42 @@ func TestExecutorRegisteredCustomPrecompile(t *testing.T) { require.NoError(t, err) require.Len(t, result.Txs, 1) require.Equal(t, ethtypes.ReceiptStatusSuccessful, result.Txs[0].Status) + require.Greater(t, result.Txs[0].GasUsed, uint64(21_000+100)) require.NotEmpty(t, result.ChangeSet.Storage) state.ApplyChangeSet(result.ChangeSet) require.Equal(t, encodedStoredLength(2), state.GetState(customAddr, storeBaseSlot([]byte("seen")))) } +func TestExecutorRegisteredCustomPrecompileMetersDynamicStoreGas(t *testing.T) { + chainID := big.NewInt(713715) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + customAddr := common.HexToAddress("0x0000000000000000000000000000000000001005") + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(200_000_000_000_000)) + + rawTx := signLegacyTxWithGas(t, key, chainID, 0, &customAddr, big.NewInt(0), []byte{0x01}, 30_000) + executor := NewExecutor(Config{ + CustomPrecompiles: contractPrecompileRegistry{ + customAddr: storeWritePrecompile{}, + }, + }, 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, ethtypes.ReceiptStatusFailed, result.Txs[0].Status) + require.True(t, errors.Is(result.Txs[0].Err, vm.ErrOutOfGas)) + require.Empty(t, result.ChangeSet.Storage) +} + func TestExecutorStakingPrecompileForwardsPayableValue(t *testing.T) { chainID := big.NewInt(713715) key, err := crypto.GenerateKey() @@ -901,7 +931,7 @@ func TestExecutorStakingPrecompileForwardsPayableValue(t *testing.T) { ) require.NoError(t, err) value := sei(5) - rawTx := signLegacyTx(t, key, chainID, 0, &stakingAddr, value, input) + rawTx := signLegacyTxWithGas(t, key, chainID, 0, &stakingAddr, value, input, 8_000_000) executor := NewExecutor(Config{ CustomPrecompiles: registry, }, WithState(state)) @@ -953,7 +983,7 @@ func TestExecutorStakingDelegationLifecycleE2E(t *testing.T) { nonces := map[common.Address]uint64{} signStakingTx := func(key *ecdsa.PrivateKey, value *big.Int, input []byte) []byte { sender := crypto.PubkeyToAddress(key.PublicKey) - raw := signLegacyTx(t, key, chainID, nonces[sender], &stakingAddr, value, input) + raw := signLegacyTxWithGas(t, key, chainID, nonces[sender], &stakingAddr, value, input, 8_000_000) nonces[sender]++ return raw } diff --git a/giga/evmonly/precompile_adapter.go b/giga/evmonly/precompile_adapter.go index d023f88399..9a2348c166 100644 --- a/giga/evmonly/precompile_adapter.go +++ b/giga/evmonly/precompile_adapter.go @@ -36,23 +36,22 @@ func (p registeredCustomPrecompile) RequiredGas(input []byte) uint64 { } func (p registeredCustomPrecompile) Run(evm *vm.EVM, caller common.Address, _ common.Address, input []byte, value *big.Int, readOnly bool, isFromDelegateCall bool, _ *tracing.Hooks) ([]byte, error) { - return p.run(evm, caller, input, value, readOnly, isFromDelegateCall, 0) + return p.run(evm, caller, input, value, readOnly, isFromDelegateCall, 0, nil) } func (p registeredCustomPrecompile) RunAndCalculateGas(evm *vm.EVM, caller common.Address, _ common.Address, input []byte, suppliedGas uint64, value *big.Int, hooks *tracing.Hooks, readOnly bool, isFromDelegateCall bool) ([]byte, uint64, error) { - gasCost := p.RequiredGas(input) - if suppliedGas < gasCost { + meter := newPrecompileGasMeter(suppliedGas, hooks) + if !meter.charge(p.RequiredGas(input), tracing.GasChangeCallPrecompiledContract) { return nil, 0, vm.ErrOutOfGas } - remainingGas := suppliedGas - gasCost - if hooks != nil && hooks.OnGasChange != nil { - hooks.OnGasChange(suppliedGas, remainingGas, tracing.GasChangeCallPrecompiledContract) + ret, err := p.run(evm, caller, input, value, readOnly, isFromDelegateCall, meter.remainingGas(), meter) + if meter.err != nil { + return nil, meter.remainingGas(), meter.err } - ret, err := p.run(evm, caller, input, value, readOnly, isFromDelegateCall, remainingGas) - return ret, remainingGas, err + return ret, meter.remainingGas(), err } -func (p registeredCustomPrecompile) run(evm *vm.EVM, caller common.Address, input []byte, value *big.Int, readOnly bool, isFromDelegateCall bool, remainingGas uint64) ([]byte, error) { +func (p registeredCustomPrecompile) run(evm *vm.EVM, caller common.Address, input []byte, value *big.Int, readOnly bool, isFromDelegateCall bool, remainingGas uint64, meter *precompileGasMeter) ([]byte, error) { stateDB, ok := evm.StateDB.(*nativeStateDB) if !ok { return nil, errInvalidPrecompileStateDB @@ -65,9 +64,9 @@ func (p registeredCustomPrecompile) run(evm *vm.EVM, caller common.Address, inpu DelegateCall: isFromDelegateCall, GasRemaining: remainingGas, Block: evmPrecompileBlockContext(evm), - Store: storageBackedStore{db: stateDB, address: p.address}, - Balances: nativeBalanceTransfer{db: stateDB}, - Logs: stateDB, + Store: storageBackedStore{db: stateDB, address: p.address, meter: meter}, + Balances: nativeBalanceTransfer{db: stateDB, meter: meter}, + Logs: meteredLogSink{sink: stateDB, meter: meter}, } return p.contract.Run(ctx, input) } @@ -155,13 +154,17 @@ func evmPrecompileBlockContext(evm *vm.EVM) precompiles.BlockContext { } type nativeBalanceTransfer struct { - db *nativeStateDB + db *nativeStateDB + meter *precompileGasMeter } func (t nativeBalanceTransfer) Transfer(from common.Address, to common.Address, amount *big.Int) error { if amount == nil || amount.Sign() == 0 { return nil } + if t.meter != nil && !t.meter.chargeNativeTransfer(t.db, from, to, amount) { + return t.meter.err + } if t.db.err != nil { return t.db.err } @@ -207,9 +210,13 @@ const ( type storageBackedStore struct { db *nativeStateDB address common.Address + meter *precompileGasMeter } func (s storageBackedStore) Get(key []byte) ([]byte, bool) { + if !s.chargeStoreBaseSlot(key) { + return nil, false + } baseSlot := storeBaseSlot(key) length, ok := s.length(baseSlot) if !ok { @@ -221,13 +228,23 @@ func (s storageBackedStore) Get(key []byte) ([]byte, bool) { chunks := chunkCount(length) out := make([]byte, 0, int(chunks*32)) //nolint:gosec // length was bounded by max int above. for i := uint64(0); i < chunks; i++ { - chunk := s.db.GetState(s.address, storeChunkSlot(baseSlot, i)) + if !s.chargeStoreChunkSlot(baseSlot, i) { + return nil, false + } + chunkSlot := storeChunkSlot(baseSlot, i) + if !s.chargeSLoad(chunkSlot) { + return nil, false + } + chunk := s.db.GetState(s.address, chunkSlot) out = append(out, chunk.Bytes()...) } return out[:int(length)], true //nolint:gosec // length was bounded by max int above. } func (s storageBackedStore) Set(key []byte, value []byte) { + if !s.chargeStoreBaseSlot(key) { + return + } baseSlot := storeBaseSlot(key) oldLength, oldOK := s.length(baseSlot) oldChunks := uint64(0) @@ -236,6 +253,9 @@ func (s storageBackedStore) Set(key []byte, value []byte) { } newLength := uint64(len(value)) //nolint:gosec // slices cannot exceed max int. newChunks := chunkCount(newLength) + if !s.chargeSStore(baseSlot, encodedStoredLength(newLength)) { + return + } s.db.SetState(s.address, baseSlot, encodedStoredLength(newLength)) for i := uint64(0); i < newChunks; i++ { start := int(i * 32) //nolint:gosec // i is bounded by len(value) chunks. @@ -245,26 +265,56 @@ func (s storageBackedStore) Set(key []byte, value []byte) { } var chunk common.Hash copy(chunk[:], value[start:end]) - s.db.SetState(s.address, storeChunkSlot(baseSlot, i), chunk) + if !s.chargeStoreChunkSlot(baseSlot, i) { + return + } + chunkSlot := storeChunkSlot(baseSlot, i) + if !s.chargeSStore(chunkSlot, chunk) { + return + } + s.db.SetState(s.address, chunkSlot, chunk) } for i := newChunks; i < oldChunks; i++ { - s.db.SetState(s.address, storeChunkSlot(baseSlot, i), common.Hash{}) + if !s.chargeStoreChunkSlot(baseSlot, i) { + return + } + chunkSlot := storeChunkSlot(baseSlot, i) + if !s.chargeSStore(chunkSlot, common.Hash{}) { + return + } + s.db.SetState(s.address, chunkSlot, common.Hash{}) } } func (s storageBackedStore) Delete(key []byte) { + if !s.chargeStoreBaseSlot(key) { + return + } baseSlot := storeBaseSlot(key) length, ok := s.length(baseSlot) if !ok { return } for i := uint64(0); i < chunkCount(length); i++ { - s.db.SetState(s.address, storeChunkSlot(baseSlot, i), common.Hash{}) + if !s.chargeStoreChunkSlot(baseSlot, i) { + return + } + chunkSlot := storeChunkSlot(baseSlot, i) + if !s.chargeSStore(chunkSlot, common.Hash{}) { + return + } + s.db.SetState(s.address, chunkSlot, common.Hash{}) + } + if !s.chargeSStore(baseSlot, common.Hash{}) { + return } s.db.SetState(s.address, baseSlot, common.Hash{}) } func (s storageBackedStore) length(baseSlot common.Hash) (uint64, bool) { + if !s.chargeSLoad(baseSlot) { + return 0, false + } encoded := s.db.GetState(s.address, baseSlot) if encoded == (common.Hash{}) { return 0, false diff --git a/giga/evmonly/precompile_gas.go b/giga/evmonly/precompile_gas.go new file mode 100644 index 0000000000..0207474a08 --- /dev/null +++ b/giga/evmonly/precompile_gas.go @@ -0,0 +1,231 @@ +package evmonly + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" + "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/params" + + "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles" +) + +const maxGas = ^uint64(0) + +type precompileGasMeter struct { + remaining uint64 + hooks *tracing.Hooks + err error +} + +func newPrecompileGasMeter(suppliedGas uint64, hooks *tracing.Hooks) *precompileGasMeter { + return &precompileGasMeter{ + remaining: suppliedGas, + hooks: hooks, + } +} + +func (m *precompileGasMeter) remainingGas() uint64 { + if m == nil { + return 0 + } + return m.remaining +} + +func (m *precompileGasMeter) charge(gas uint64, reason tracing.GasChangeReason) bool { + if m == nil { + return true + } + if m.err != nil { + return false + } + if gas == 0 { + return true + } + if m.remaining < gas { + m.fail(vm.ErrOutOfGas, reason) + return false + } + old := m.remaining + m.remaining -= gas + m.emitGasChange(old, m.remaining, reason) + return true +} + +func (m *precompileGasMeter) fail(err error, reason tracing.GasChangeReason) { + if m.err == nil { + m.err = err + } + if m.remaining == 0 { + return + } + old := m.remaining + m.remaining = 0 + m.emitGasChange(old, 0, reason) +} + +func (m *precompileGasMeter) emitGasChange(old uint64, next uint64, reason tracing.GasChangeReason) { + if m.hooks != nil && m.hooks.OnGasChange != nil { + m.hooks.OnGasChange(old, next, reason) + } +} + +func (m *precompileGasMeter) chargeKeccak(size int) bool { + sizeU64 := uint64(size) //nolint:gosec // slice/string lengths are non-negative and bounded by max int. + words := wordCount(sizeU64) + return m.charge(gasAdd(params.Keccak256Gas, gasMul(params.Keccak256WordGas, words)), tracing.GasChangeCallPrecompiledContract) +} + +func (m *precompileGasMeter) chargeSLoad(db *nativeStateDB, addr common.Address, slot common.Hash) bool { + if _, slotPresent := db.SlotInAccessList(addr, slot); !slotPresent { + db.AddSlotToAccessList(addr, slot) + return m.charge(params.ColdSloadCostEIP2929, tracing.GasChangeCallStorageColdAccess) + } + return m.charge(params.WarmStorageReadCostEIP2929, tracing.GasChangeCallPrecompiledContract) +} + +func (m *precompileGasMeter) chargeSStore(db *nativeStateDB, addr common.Address, slot common.Hash, value common.Hash) bool { + if m.remaining <= params.SstoreSentryGasEIP2200 { + m.fail(vm.ErrOutOfGas, tracing.GasChangeCallPrecompiledContract) + return false + } + cost := uint64(0) + if _, slotPresent := db.SlotInAccessList(addr, slot); !slotPresent { + cost = params.ColdSloadCostEIP2929 + db.AddSlotToAccessList(addr, slot) + } + current := db.GetState(addr, slot) + if current == value { + return m.charge(gasAdd(cost, params.WarmStorageReadCostEIP2929), tracing.GasChangeCallPrecompiledContract) + } + original := db.GetCommittedState(addr, slot) + if original == current { + if original == (common.Hash{}) { + return m.charge(gasAdd(cost, params.SstoreSetGasEIP2200), tracing.GasChangeCallPrecompiledContract) + } + if value == (common.Hash{}) { + db.AddRefund(params.SstoreClearsScheduleRefundEIP3529) + } + return m.charge(gasAdd(cost, params.SstoreResetGasEIP2200-params.ColdSloadCostEIP2929), tracing.GasChangeCallPrecompiledContract) + } + m.adjustDirtySStoreRefund(db, current, original, value) + return m.charge(gasAdd(cost, params.WarmStorageReadCostEIP2929), tracing.GasChangeCallPrecompiledContract) +} + +func (m *precompileGasMeter) adjustDirtySStoreRefund(db *nativeStateDB, current common.Hash, original common.Hash, value common.Hash) { + if original != (common.Hash{}) { + if current == (common.Hash{}) { + db.SubRefund(params.SstoreClearsScheduleRefundEIP3529) + } else if value == (common.Hash{}) { + db.AddRefund(params.SstoreClearsScheduleRefundEIP3529) + } + } + if original != value { + return + } + if original == (common.Hash{}) { + db.AddRefund(params.SstoreSetGasEIP2200 - params.WarmStorageReadCostEIP2929) + return + } + db.AddRefund((params.SstoreResetGasEIP2200 - params.ColdSloadCostEIP2929) - params.WarmStorageReadCostEIP2929) +} + +func (m *precompileGasMeter) chargeNativeTransfer(db *nativeStateDB, from common.Address, to common.Address, amount *big.Int) bool { + if amount == nil || amount.Sign() == 0 { + return true + } + if !m.chargeAccountAccess(db, from) || !m.chargeAccountAccess(db, to) { + return false + } + if !m.charge(params.CallValueTransferGas, tracing.GasChangeCallPrecompiledContract) { + return false + } + if db.Empty(to) { + return m.charge(params.CallNewAccountGas, tracing.GasChangeCallPrecompiledContract) + } + return true +} + +func (m *precompileGasMeter) chargeAccountAccess(db *nativeStateDB, addr common.Address) bool { + if db.AddressInAccessList(addr) { + return m.charge(params.WarmStorageReadCostEIP2929, tracing.GasChangeCallPrecompiledContract) + } + db.AddAddressToAccessList(addr) + return m.charge(params.ColdAccountAccessCostEIP2929, tracing.GasChangeCallStorageColdAccess) +} + +func (m *precompileGasMeter) chargeLog(topics int, dataLen int) bool { + topicsGas := gasMul(params.LogTopicGas, uint64(topics)) //nolint:gosec // topic count is bounded by log construction. + dataGas := gasMul(params.LogDataGas, uint64(dataLen)) //nolint:gosec // log data length is bounded by memory. + return m.charge(gasAdd(params.LogGas, topicsGas, dataGas), tracing.GasChangeCallPrecompiledContract) +} + +type meteredLogSink struct { + sink precompiles.LogSink + meter *precompileGasMeter +} + +func (l meteredLogSink) AddLog(log *ethtypes.Log) { + if l.sink == nil || log == nil { + return + } + if l.meter != nil && !l.meter.chargeLog(len(log.Topics), len(log.Data)) { + return + } + l.sink.AddLog(log) +} + +func (s storageBackedStore) chargeStoreBaseSlot(key []byte) bool { + if s.meter == nil { + return true + } + return s.meter.chargeKeccak(len(storeLengthDomain) + len(key)) +} + +func (s storageBackedStore) chargeStoreChunkSlot(baseSlot common.Hash, index uint64) bool { + if s.meter == nil { + return true + } + return s.meter.chargeKeccak(len(storeChunkDomain) + len(baseSlot) + 8) +} + +func (s storageBackedStore) chargeSLoad(slot common.Hash) bool { + if s.meter == nil { + return true + } + return s.meter.chargeSLoad(s.db, s.address, slot) +} + +func (s storageBackedStore) chargeSStore(slot common.Hash, value common.Hash) bool { + if s.meter == nil { + return true + } + return s.meter.chargeSStore(s.db, s.address, slot, value) +} + +func wordCount(size uint64) uint64 { + if size == 0 { + return 0 + } + return (size + 31) / 32 +} + +func gasAdd(values ...uint64) uint64 { + total := uint64(0) + for _, value := range values { + if maxGas-total < value { + return maxGas + } + total += value + } + return total +} + +func gasMul(left uint64, right uint64) uint64 { + if left != 0 && right > maxGas/left { + return maxGas + } + return left * right +} diff --git a/giga/evmonly/precompiles/staking/staking.go b/giga/evmonly/precompiles/staking/staking.go index bae0f7ba7c..e2985bebb5 100644 --- a/giga/evmonly/precompiles/staking/staking.go +++ b/giga/evmonly/precompiles/staking/staking.go @@ -41,8 +41,7 @@ const ( StakingAddress = "0x0000000000000000000000000000000000001005" unknownMethodGas uint64 = 3000 - readGas uint64 = 3000 - writeGas uint64 = 20000 + baseGas uint64 = 700 inputByteGas uint64 = 16 bondDenom = "usei" @@ -135,15 +134,11 @@ func (p *Precompile) ABI() abi.ABI { } func (p *Precompile) RequiredGas(input []byte) uint64 { - method, _, err := p.prepare(input) + _, _, err := p.prepare(input) if err != nil { return unknownMethodGas } - gas := readGas - if isTransaction(method.Name) { - gas = writeGas - } - return gas + inputByteGas*uint64(len(input)) //nolint:gosec // input length is bounded by memory. + return baseGas + inputByteGas*uint64(len(input)) //nolint:gosec // input length is bounded by memory. } func (p *Precompile) Run(ctx *precompiles.Context, input []byte) ([]byte, error) {