diff --git a/giga/evmonly/README.md b/giga/evmonly/README.md new file mode 100644 index 0000000000..4945662901 --- /dev/null +++ b/giga/evmonly/README.md @@ -0,0 +1,157 @@ +# EVM-only giga execution + +This package contains the EVM-only execution boundary for the final-form giga +path. It is intentionally separate from the current Cosmos-backed giga wiring in +`app/app.go`. + +The target execution model is based on the `sei-v3` executor: + +- raw transaction bytes are Ethereum RLP transactions, not Cosmos SDK txs +- state layout is EVM-native: balance, storage, code, and nonce are keyed by + EVM addresses and hashes +- block execution can overlap with parsing or persistence work for nearby blocks +- hot execution should not allocate `sdk.Context`, `sdk.Tx`, + `MsgEVMTransaction`, Cosmos messages, or Cosmos coins + +The current implementation executes raw RLP transactions with go-ethereum +against an EVM-native state backend, then returns a changeset plus Ethereum +receipts. 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 + +The `evmonly` package currently provides: + +- sequential execution of the ordered block transaction list +- RLP decoding and sender recovery through go-ethereum signers +- go-ethereum `core.ApplyMessage` execution against an SDK-free `vm.StateDB` +- key-addressable state reads for balance, nonce, code, and storage +- deterministic post-block `StateChangeSet` construction +- optional executor-internal OCC for non-conflicting transaction sets +- Ethereum receipt construction with logs, bloom, gas, tx hash, block metadata, + contract address, and effective gas price +- a map-backed `MemoryState` for tests and early integration +- fail-closed custom precompile placeholders 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. + +## Executor interface + +The boundary is: + +```go +ExecuteBlock(context.Context, BlockRequest) (*BlockResult, error) +``` + +For callers that can pipeline stateless work across blocks, the concrete +executor also supports: + +```go +PrepareBlock(context.Context, BlockRequest) (PreparedBlock, error) +ExecutePreparedBlock(context.Context, PreparedBlock) (*BlockResult, error) +``` + +`PrepareBlock` decodes transaction RLP and recovers senders. This work does not +touch state, so block N+1 can be prepared while block N is still executing. +`ExecuteBlock` remains the convenience path and performs prepare then execute in +one call. + +The executor should be commit-neutral. It executes an ordered EVM block and +returns the state writes and receipts produced by that block. The caller owns +durable persistence, state commitment, block indexing, and receipt publication. +The concrete `Executor` accepts a `StateReader` backend through `WithState(...)`; +callers can persist the returned `ChangeSet` with a matching `StateWriter`. + +A non-nil `error` means block validation failed and the caller must not commit a +partial output. EVM call failures inside an otherwise valid transaction are +represented in `Receipts` and `Txs` with failed status. + +## Input block format + +`BlockRequest` is the expected input: + +- `Context` contains block-constant EVM fields: + - block number + - timestamp + - block gas limit + - chain ID + - base fee + - blob base fee, when enabled + - coinbase + - parent hash + - current block hash + - prevRandao +- `Txs` is the canonical, already-ordered transaction list for the block. +- Each tx is raw Ethereum transaction RLP bytes. +- There is no Cosmos SDK tx envelope, `MsgEVMTransaction`, ante wrapper, memo, + fee object, account address mapping object, or Cosmos gas meter in the input. + +The runtime or consensus layer is responsible for choosing the block order and +providing the block context. The executor is responsible for parsing tx RLP, +recovering senders, validating EVM nonce/fee/intrinsic-gas rules, executing EVM +state transitions, and producing deterministic outputs. + +`BlockHash` is used for receipts and log metadata. The current `BLOCKHASH` +opcode callback only exposes `ParentHash`; older historical hashes require a +runtime-provided hash source in a later integration step. + +## Output format + +`BlockResult` contains two primary outputs: + +- `ChangeSet`: the EVM-native state writes produced by the block. +- `Receipts`: Ethereum receipts for the executed transactions. + +`ChangeSet` is expressed as post-block values, not deltas: + +- `Balances`: final balance for each changed EVM address +- `Nonces`: final nonce for each changed EVM address +- `Code`: final bytecode updates or deletions +- `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 + +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. + +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. + +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 + +- 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. +- 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/config.go b/giga/evmonly/config.go new file mode 100644 index 0000000000..c79dbef842 --- /dev/null +++ b/giga/evmonly/config.go @@ -0,0 +1,38 @@ +package evmonly + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/params" + + "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles" +) + +// Config captures the sei-v3 executor knobs needed by the EVM-only path. +type Config struct { + DisableNonceCheck bool + DisableGasPriceCheck bool + MinGasPrice *big.Int + ChainConfig *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 { + return Config{ + MinGasPrice: big.NewInt(1_000_000_000), + } +} + +func (c Config) WithDefaults() Config { + defaults := DefaultConfig() + if c.MinGasPrice == nil { + c.MinGasPrice = new(big.Int).Set(defaults.MinGasPrice) + } + return c +} diff --git a/giga/evmonly/executor.go b/giga/evmonly/executor.go new file mode 100644 index 0000000000..d9ad18abf5 --- /dev/null +++ b/giga/evmonly/executor.go @@ -0,0 +1,379 @@ +package evmonly + +import ( + "context" + "fmt" + "math" + "math/big" + "runtime" + + "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" +) + +// Executor runs raw EVM transactions against an EVM-native state backend. +type Executor struct { + cfg Config + state StateReader + resultSink ResultSink + occPool *occWorkerPool + resultPool *blockResultPool +} + +type Option func(*Executor) + +func WithState(state StateReader) Option { + return func(e *Executor) { + if state != nil { + e.state = state + } + } +} + +func WithResultSink(sink ResultSink) Option { + return func(e *Executor) { + e.resultSink = sink + } +} + +func NewExecutor(cfg Config, opts ...Option) *Executor { + e := &Executor{ + cfg: cfg.WithDefaults(), + state: NewMemoryState(), + resultPool: newBlockResultPool(cfg.BlockResultPoolSize), + } + if e.cfg.OCCWorkers > 1 { + e.occPool = newOCCWorkerPool(e.cfg.OCCWorkers) + runtime.SetFinalizer(e, (*Executor).Close) + } + for _, opt := range opts { + opt(e) + } + return e +} + +func (e *Executor) Close() { + if e == nil || e.occPool == nil { + return + } + runtime.SetFinalizer(e, nil) + e.occPool.Close() + e.occPool = nil +} + +func (e *Executor) Config() Config { + return e.cfg +} + +func (e *Executor) ExecuteBlock(ctx context.Context, req BlockRequest) (*BlockResult, error) { + prepared, err := e.PrepareBlock(ctx, req) + if err != nil { + return nil, err + } + return e.ExecutePreparedBlock(ctx, prepared) +} + +func (e *Executor) PrepareBlock(ctx context.Context, req BlockRequest) (PreparedBlock, error) { + chainConfig := e.chainConfig(req.Context) + 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 && !e.hasCustomPrecompiles() { + result, err = e.acquireBlockResult(ctx, 0) + } else if e.useOCC(len(req.Txs)) { + result, err = e.executeBlockOCC(ctx, req) + } else { + result, err = e.executeBlockSequential(ctx, req) + } + if err != nil { + return nil, err + } + if err := e.sinkBlockResult(ctx, req.Context.Number, result); err != nil { + result.Release() + return nil, err + } + return result, nil +} + +func (e *Executor) acquireBlockResult(ctx context.Context, txCapacity int) (*BlockResult, error) { + if e.resultPool == nil || !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) + } + if err := e.resultSink.StoreReceipts(ctx, height, result.Receipts); err != nil { + return fmt.Errorf("store receipts for block %d: %w", height, err) + } + 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 + } + return !e.hasCustomPrecompiles() +} + +func (e *Executor) executeBlockSequential(ctx context.Context, req PreparedBlock) (*BlockResult, error) { + chainConfig := e.chainConfig(req.Context) + + stateDB := newNativeStateDB(e.state) + blockCtx := buildBlockContext(req.Context) + evm := vm.NewEVM(blockCtx, stateDB, chainConfig, vm.Config{}, customPrecompileMap(e.cfg.CustomPrecompiles)) + stateDB.SetEVM(evm) + + gasLimit := req.Context.GasLimit + if gasLimit == 0 { + gasLimit = math.MaxUint64 + } + gasPool := new(core.GasPool).AddGas(gasLimit) + baseFee := cloneBig(req.Context.BaseFee) + + result, err := e.acquireBlockResult(ctx, len(req.Txs)) + if err != nil { + return nil, err + } + ok := false + defer func() { + if !ok { + result.Release() + } + }() + var txIndexUint uint + for txIndex, p := range req.Txs { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + txResult, receipt, err := e.executeTx(evm, stateDB, gasPool, req.Context, p, txIndex, txIndexUint, baseFee) + if err != nil { + return nil, fmt.Errorf("execute tx %d %s: %w", txIndex, p.Tx.Hash(), err) + } + txResult.CumulativeGasUsed = result.GasUsed + txResult.GasUsed + receipt.CumulativeGasUsed = txResult.CumulativeGasUsed + result.Txs = append(result.Txs, txResult) + result.Receipts = append(result.Receipts, receipt) + result.GasUsed += txResult.GasUsed + txIndexUint++ + } + 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) + ok = true + return result, nil +} + +func (e *Executor) executeTx( + evm *vm.EVM, + stateDB *nativeStateDB, + gasPool *core.GasPool, + block BlockContext, + p PreparedTx, + txIndex int, + txIndexUint uint, + baseFee *big.Int, +) (TxResult, *ethtypes.Receipt, error) { + tx := p.Tx + if !e.cfg.DisableGasPriceCheck && e.cfg.MinGasPrice != nil { + // MinGasPrice is block-validity policy; unlike EVM call failures, it + // does not produce a receipt for an otherwise invalid block. + if effectiveGasPrice(tx, baseFee).Cmp(e.cfg.MinGasPrice) < 0 { + return TxResult{Hash: tx.Hash(), Sender: p.Sender, To: tx.To(), Err: errInsufficientGasPrice}, + nil, + errInsufficientGasPrice + } + } + + msg := transactionToPreparedMessage(p, baseFee) + msg.SkipNonceChecks = e.cfg.DisableNonceCheck + + stateDB.setTxContext(tx.Hash(), txIndex, txIndexUint) + logStart := len(stateDB.logs) + evm.SetTxContext(core.NewEVMTxContext(msg)) + execResult, err := core.ApplyMessage(evm, msg, gasPool) + if err != nil { + return TxResult{Hash: tx.Hash(), Sender: p.Sender, To: tx.To(), Err: err}, nil, err + } + stateDB.clearSnapshots() + stateDB.Finalise(true) + + txLogs := append([]*ethtypes.Log(nil), stateDB.logs[logStart:]...) + for _, log := range txLogs { + log.BlockNumber = block.Number + log.BlockHash = block.BlockHash + log.TxHash = tx.Hash() + log.TxIndex = txIndexUint + } + + status := ethtypes.ReceiptStatusSuccessful + if execResult.Failed() { + status = ethtypes.ReceiptStatusFailed + } + receipt := ðtypes.Receipt{ + Type: tx.Type(), + Status: status, + Logs: txLogs, + TxHash: tx.Hash(), + GasUsed: execResult.UsedGas, + EffectiveGasPrice: effectiveGasPrice(tx, baseFee), + BlockHash: block.BlockHash, + BlockNumber: new(big.Int).SetUint64(block.Number), + TransactionIndex: txIndexUint, + } + if tx.To() == nil { + receipt.ContractAddress = crypto.CreateAddress(p.Sender, tx.Nonce()) + } + receipt.Bloom = ethtypes.CreateBloom(receipt) + + txResult := TxResult{ + Hash: tx.Hash(), + Sender: p.Sender, + To: tx.To(), + ContractAddress: receipt.ContractAddress, + Status: status, + GasUsed: execResult.UsedGas, + EffectiveGasPrice: new(big.Int).Set(receipt.EffectiveGasPrice), + Logs: txLogs, + Err: execResult.Err, + } + return txResult, receipt, nil +} + +func transactionToPreparedMessage(p PreparedTx, baseFee *big.Int) *core.Message { + tx := p.Tx + msg := &core.Message{ + From: p.Sender, + Nonce: tx.Nonce(), + GasLimit: tx.Gas(), + GasPrice: new(big.Int).Set(tx.GasPrice()), + GasFeeCap: new(big.Int).Set(tx.GasFeeCap()), + GasTipCap: new(big.Int).Set(tx.GasTipCap()), + To: tx.To(), + Value: tx.Value(), + Data: tx.Data(), + AccessList: tx.AccessList(), + SetCodeAuthorizations: tx.SetCodeAuthorizations(), + SkipNonceChecks: false, + SkipFromEOACheck: false, + BlobHashes: tx.BlobHashes(), + BlobGasFeeCap: tx.BlobGasFeeCap(), + } + if baseFee != nil { + msg.GasPrice = msg.GasPrice.Add(msg.GasTipCap, baseFee) + if msg.GasPrice.Cmp(msg.GasFeeCap) > 0 { + msg.GasPrice = msg.GasFeeCap + } + } + return msg +} + +func buildBlockContext(ctx BlockContext) vm.BlockContext { + prevRandao := ctx.PrevRandao + baseFee := 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 { + if ctx.Number > 0 && n == ctx.Number-1 { + return ctx.ParentHash + } + return common.Hash{} + }, + Coinbase: ctx.Coinbase, + GasLimit: gasLimit, + BlockNumber: new(big.Int).SetUint64(ctx.Number), + Time: ctx.Time, + Difficulty: new(big.Int), + BaseFee: baseFee, + BlobBaseFee: blobBaseFee, + Random: &prevRandao, + } +} + +func (e *Executor) chainConfig(ctx BlockContext) *params.ChainConfig { + var cfg params.ChainConfig + if e.cfg.ChainConfig != nil { + cfg = *e.cfg.ChainConfig + } else { + cfg = *params.AllDevChainProtocolChanges + } + if ctx.ChainID != nil { + cfg.ChainID = new(big.Int).Set(ctx.ChainID) + } else if cfg.ChainID != nil { + cfg.ChainID = new(big.Int).Set(cfg.ChainID) + } else { + cfg.ChainID = big.NewInt(1) + } + return &cfg +} + +func effectiveGasPrice(tx *ethtypes.Transaction, baseFee *big.Int) *big.Int { + if baseFee == nil { + return tx.GasPrice() + } + if tx.Type() == ethtypes.DynamicFeeTxType || tx.Type() == ethtypes.BlobTxType || tx.Type() == ethtypes.SetCodeTxType { + return new(big.Int).Add(baseFee, tx.EffectiveGasTipValue(baseFee)) + } + return tx.GasPrice() +} + +var errInsufficientGasPrice = fmt.Errorf("insufficient gas price") diff --git a/giga/evmonly/executor_test.go b/giga/evmonly/executor_test.go new file mode 100644 index 0000000000..50bf94f4b1 --- /dev/null +++ b/giga/evmonly/executor_test.go @@ -0,0 +1,1461 @@ +package evmonly + +import ( + "context" + "crypto/ecdsa" + "errors" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/params" + "github.com/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 + +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 +} + +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{}) + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{}) + + require.NoError(t, err) + require.NotNil(t, result) +} + +func TestExecutorTransferTx(t *testing.T) { + chainID := big.NewInt(713715) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + recipient := common.HexToAddress("0x00000000000000000000000000000000000000a1") + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(200_000_000_000_000)) + + rawTx := signLegacyTx(t, key, chainID, 0, &recipient, big.NewInt(7), nil) + executor := NewExecutor(Config{}, WithState(state)) + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: blockContext(chainID), + Txs: [][]byte{rawTx}, + }) + + require.NoError(t, err) + require.Len(t, result.Txs, 1) + require.Len(t, result.Receipts, 1) + require.Equal(t, ethtypes.ReceiptStatusSuccessful, result.Txs[0].Status) + require.Equal(t, uint64(21_000), result.GasUsed) + require.NotEmpty(t, result.ChangeSet.Balances) + + state.ApplyChangeSet(result.ChangeSet) + require.Equal(t, big.NewInt(7), state.GetBalance(recipient)) + require.Equal(t, uint64(1), state.GetNonce(sender)) +} + +func TestExecutorInvokesResultSink(t *testing.T) { + chainID := big.NewInt(713715) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + recipient := common.HexToAddress("0x00000000000000000000000000000000000000a7") + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(200_000_000_000_000)) + sink := &recordingResultSink{} + + rawTx := signLegacyTx(t, key, chainID, 0, &recipient, big.NewInt(7), nil) + executor := NewExecutor(Config{}, WithState(state), WithResultSink(sink)) + ctx := blockContext(chainID) + ctx.Number = 77 + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: ctx, + Txs: [][]byte{rawTx}, + }) + + require.NoError(t, err) + require.Len(t, sink.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 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() + 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 TestExecutorOCCNonConflictingTransfersMatchSequential(t *testing.T) { + chainID := big.NewInt(713715) + txCount := 12 + rawTxs := make([][]byte, 0, txCount) + senders := make([]common.Address, 0, txCount) + recipients := make([]common.Address, 0, txCount) + seqState := NewMemoryState() + occState := NewMemoryState() + + for i := 0; i < txCount; i++ { + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + recipient := common.BigToAddress(big.NewInt(int64(10_000 + i))) + senders = append(senders, sender) + recipients = append(recipients, recipient) + seqState.SetBalance(sender, big.NewInt(1_000_000)) + occState.SetBalance(sender, big.NewInt(1_000_000)) + rawTxs = append(rawTxs, signLegacyTxWithGasPrice(t, key, chainID, 0, &recipient, big.NewInt(7), nil, 100_000, big.NewInt(0))) + } + + cfg := Config{MinGasPrice: big.NewInt(0)} + seqExecutor := NewExecutor(cfg, WithState(seqState)) + occExecutor := NewExecutor(Config{MinGasPrice: big.NewInt(0), OCCWorkers: 4}, WithState(occState)) + req := BlockRequest{Context: blockContext(chainID), Txs: rawTxs} + + seqResult, err := seqExecutor.ExecuteBlock(context.Background(), req) + require.NoError(t, err) + occResult, err := occExecutor.ExecuteBlock(context.Background(), req) + require.NoError(t, err) + + require.Equal(t, seqResult.GasUsed, occResult.GasUsed) + require.Len(t, occResult.Txs, txCount) + require.Len(t, occResult.Receipts, txCount) + require.True(t, occResult.OCCStats.Attempted) + require.False(t, occResult.OCCStats.Fallback) + require.Zero(t, occResult.OCCStats.ConflictCount) + for i := range txCount { + require.Equal(t, seqResult.Txs[i].Hash, occResult.Txs[i].Hash) + require.Equal(t, seqResult.Txs[i].Status, occResult.Txs[i].Status) + require.Equal(t, seqResult.Receipts[i].CumulativeGasUsed, occResult.Receipts[i].CumulativeGasUsed) + } + + seqState.ApplyChangeSet(seqResult.ChangeSet) + occState.ApplyChangeSet(occResult.ChangeSet) + for i := range txCount { + require.Equal(t, seqState.GetBalance(senders[i]), occState.GetBalance(senders[i])) + require.Equal(t, seqState.GetNonce(senders[i]), occState.GetNonce(senders[i])) + require.Equal(t, seqState.GetBalance(recipients[i]), occState.GetBalance(recipients[i])) + } +} + +func TestExecutorOCCConflictingTransfersMatchSequential(t *testing.T) { + chainID := big.NewInt(713715) + txCount := 8 + recipient := testAddress(0xdd) + rawTxs := make([][]byte, 0, txCount) + seqState := NewMemoryState() + occState := NewMemoryState() + + for i := 0; i < txCount; i++ { + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + seqState.SetBalance(sender, big.NewInt(1_000_000)) + occState.SetBalance(sender, big.NewInt(1_000_000)) + rawTxs = append(rawTxs, signLegacyTxWithGasPrice(t, key, chainID, 0, &recipient, big.NewInt(3), nil, 100_000, big.NewInt(0))) + } + + req := BlockRequest{Context: blockContext(chainID), Txs: rawTxs} + seqResult, err := NewExecutor(Config{MinGasPrice: big.NewInt(0)}, WithState(seqState)).ExecuteBlock(context.Background(), req) + require.NoError(t, err) + occResult, err := NewExecutor(Config{MinGasPrice: big.NewInt(0), OCCWorkers: 4}, WithState(occState)).ExecuteBlock(context.Background(), req) + require.NoError(t, err) + + seqState.ApplyChangeSet(seqResult.ChangeSet) + occState.ApplyChangeSet(occResult.ChangeSet) + require.Equal(t, seqResult.GasUsed, occResult.GasUsed) + require.True(t, occResult.OCCStats.Attempted) + require.True(t, occResult.OCCStats.Fallback) + require.Equal(t, "conflict", occResult.OCCStats.FallbackReason) + require.Greater(t, occResult.OCCStats.ConflictCount, uint64(0)) + require.NotEmpty(t, occResult.OCCStats.ConflictSamples) + foundRecipientBalanceConflict := false + for _, conflict := range occResult.OCCStats.ConflictSamples { + if conflict.Kind == "balance" && conflict.Address == recipient { + foundRecipientBalanceConflict = true + require.Greater(t, conflict.Count, uint64(0)) + } + } + require.True(t, foundRecipientBalanceConflict) + require.Equal(t, seqState.GetBalance(recipient), occState.GetBalance(recipient)) + require.Equal(t, big.NewInt(int64(txCount*3)), occState.GetBalance(recipient)) +} + +func TestExecutorReceiptAndLogMetadata(t *testing.T) { + chainID := big.NewInt(713715) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + recipient := testAddress(0xa5) + logContract := testAddress(0xc2) + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000_000_000_000)) + state.SetCode(logContract, log0Code()) + + transfer := signLegacyTx(t, key, chainID, 0, &recipient, big.NewInt(3), nil) + emitLog := signLegacyTx(t, key, chainID, 1, &logContract, big.NewInt(0), nil) + transferTx := decodeTx(t, transfer) + emitLogTx := decodeTx(t, emitLog) + ctx := blockContext(chainID) + ctx.Number = 42 + ctx.BlockHash = testHash(0x42) + executor := NewExecutor(Config{}, WithState(state)) + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: ctx, + Txs: [][]byte{transfer, emitLog}, + }) + + require.NoError(t, err) + require.Len(t, result.Txs, 2) + require.Len(t, result.Receipts, 2) + + require.Equal(t, transferTx.Hash(), result.Receipts[0].TxHash) + require.Equal(t, uint(0), result.Receipts[0].TransactionIndex) + require.Equal(t, ctx.BlockHash, result.Receipts[0].BlockHash) + require.Equal(t, new(big.Int).SetUint64(ctx.Number), result.Receipts[0].BlockNumber) + require.Equal(t, result.Txs[0].GasUsed, result.Receipts[0].CumulativeGasUsed) + + require.Equal(t, emitLogTx.Hash(), result.Receipts[1].TxHash) + require.Equal(t, uint(1), result.Receipts[1].TransactionIndex) + require.Equal(t, result.GasUsed, result.Receipts[1].CumulativeGasUsed) + require.Len(t, result.Receipts[1].Logs, 1) + log := result.Receipts[1].Logs[0] + require.Equal(t, logContract, log.Address) + require.Equal(t, ctx.Number, log.BlockNumber) + require.Equal(t, ctx.BlockHash, log.BlockHash) + require.Equal(t, emitLogTx.Hash(), log.TxHash) + require.Equal(t, uint(1), log.TxIndex) + require.Equal(t, uint(0), log.Index) + + state.ApplyChangeSet(result.ChangeSet) + require.Equal(t, big.NewInt(3), state.GetBalance(recipient)) + require.Equal(t, uint64(2), state.GetNonce(sender)) +} + +func TestExecutorEVMFailureProducesReceiptAndContinues(t *testing.T) { + chainID := big.NewInt(713715) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + oogContract := testAddress(0xc3) + recipient := testAddress(0xa6) + keySlot := testHash(0x01) + value := testHash(0x02) + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000_000_000_000)) + state.SetCode(oogContract, storeCode(keySlot, value)) + + oogCall := signLegacyTxWithGas(t, key, chainID, 0, &oogContract, big.NewInt(0), nil, 22_000) + laterTransfer := signLegacyTx(t, key, chainID, 1, &recipient, big.NewInt(5), nil) + executor := NewExecutor(Config{}, WithState(state)) + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: blockContext(chainID), + Txs: [][]byte{oogCall, laterTransfer}, + }) + + require.NoError(t, err) + require.Len(t, result.Txs, 2) + require.Equal(t, ethtypes.ReceiptStatusFailed, result.Txs[0].Status) + require.True(t, errors.Is(result.Txs[0].Err, vm.ErrOutOfGas)) + require.Equal(t, uint64(22_000), result.Txs[0].GasUsed) + require.Equal(t, ethtypes.ReceiptStatusSuccessful, result.Txs[1].Status) + require.Equal(t, result.GasUsed, result.Receipts[1].CumulativeGasUsed) + + state.ApplyChangeSet(result.ChangeSet) + require.Equal(t, common.Hash{}, state.GetState(oogContract, keySlot)) + require.Equal(t, big.NewInt(5), state.GetBalance(recipient)) + require.Equal(t, uint64(2), state.GetNonce(sender)) +} + +func TestExecutorValidationFailuresAbortBlock(t *testing.T) { + chainID := big.NewInt(713715) + recipient := testAddress(0xa7) + + t.Run("nonce too high", func(t *testing.T) { + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000_000_000_000)) + rawTx := signLegacyTx(t, key, chainID, 1, &recipient, big.NewInt(1), nil) + executor := NewExecutor(Config{}, WithState(state)) + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: blockContext(chainID), + Txs: [][]byte{rawTx}, + }) + + require.Error(t, err) + require.True(t, errors.Is(err, core.ErrNonceTooHigh)) + require.Nil(t, result) + require.Equal(t, uint64(0), state.GetNonce(sender)) + require.Equal(t, big.NewInt(0), state.GetBalance(recipient)) + }) + + t.Run("nonce too low", func(t *testing.T) { + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000_000_000_000)) + state.SetNonce(sender, 1) + rawTx := signLegacyTx(t, key, chainID, 0, &recipient, big.NewInt(1), nil) + executor := NewExecutor(Config{}, WithState(state)) + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: blockContext(chainID), + Txs: [][]byte{rawTx}, + }) + + require.Error(t, err) + require.True(t, errors.Is(err, core.ErrNonceTooLow)) + require.Nil(t, result) + require.Equal(t, uint64(1), state.GetNonce(sender)) + require.Equal(t, big.NewInt(0), state.GetBalance(recipient)) + }) + + t.Run("insufficient balance", func(t *testing.T) { + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1)) + rawTx := signLegacyTx(t, key, chainID, 0, &recipient, big.NewInt(1), nil) + executor := NewExecutor(Config{}, WithState(state)) + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: blockContext(chainID), + Txs: [][]byte{rawTx}, + }) + + require.Error(t, err) + require.True(t, errors.Is(err, core.ErrInsufficientFunds)) + require.Nil(t, result) + require.Equal(t, uint64(0), state.GetNonce(sender)) + require.Equal(t, big.NewInt(0), state.GetBalance(recipient)) + }) + + t.Run("min gas price", func(t *testing.T) { + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000_000_000_000)) + rawTx := signLegacyTxWithGasPrice(t, key, chainID, 0, &recipient, big.NewInt(1), nil, 100_000, big.NewInt(1)) + executor := NewExecutor(Config{ + MinGasPrice: big.NewInt(2), + }, WithState(state)) + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: blockContext(chainID), + Txs: [][]byte{rawTx}, + }) + + require.Error(t, err) + require.True(t, errors.Is(err, errInsufficientGasPrice)) + require.Nil(t, result) + require.Equal(t, uint64(0), state.GetNonce(sender)) + require.Equal(t, big.NewInt(0), state.GetBalance(recipient)) + }) + + t.Run("fee cap below base fee", func(t *testing.T) { + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000_000_000_000)) + rawTx := signDynamicFeeTxWithFees( + t, + key, + chainID, + 0, + &recipient, + big.NewInt(1), + nil, + big.NewInt(testGasPriceWei), + big.NewInt(testGasPriceWei), + 100_000, + ) + executor := NewExecutor(Config{ + DisableGasPriceCheck: true, + }, WithState(state)) + ctx := blockContext(chainID) + ctx.BaseFee = big.NewInt(2 * testGasPriceWei) + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: ctx, + Txs: [][]byte{rawTx}, + }) + + require.Error(t, err) + require.True(t, errors.Is(err, core.ErrFeeCapTooLow)) + require.Nil(t, result) + require.Equal(t, uint64(0), state.GetNonce(sender)) + require.Equal(t, big.NewInt(0), state.GetBalance(recipient)) + }) + + t.Run("intrinsic gas too low", func(t *testing.T) { + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000_000_000_000)) + rawTx := signLegacyTxWithGas(t, key, chainID, 0, &recipient, big.NewInt(1), nil, 20_000) + executor := NewExecutor(Config{}, WithState(state)) + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: blockContext(chainID), + Txs: [][]byte{rawTx}, + }) + + require.Error(t, err) + require.True(t, errors.Is(err, core.ErrIntrinsicGas)) + require.Nil(t, result) + require.Equal(t, uint64(0), state.GetNonce(sender)) + require.Equal(t, big.NewInt(0), state.GetBalance(recipient)) + }) + + t.Run("block gas exhausted", func(t *testing.T) { + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000_000_000_000)) + firstTransfer := signLegacyTxWithGas(t, key, chainID, 0, &recipient, big.NewInt(1), nil, 21_000) + secondTransfer := signLegacyTxWithGas(t, key, chainID, 1, &recipient, big.NewInt(1), nil, 21_000) + executor := NewExecutor(Config{}, WithState(state)) + ctx := blockContext(chainID) + ctx.GasLimit = 30_000 + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: ctx, + Txs: [][]byte{firstTransfer, secondTransfer}, + }) + + require.Error(t, err) + require.True(t, errors.Is(err, core.ErrGasLimitReached)) + require.Nil(t, result) + require.Equal(t, uint64(0), state.GetNonce(sender)) + require.Equal(t, big.NewInt(0), state.GetBalance(recipient)) + }) +} + +func TestExecutorRejectsBadSignatureBeforeExecution(t *testing.T) { + chainID := big.NewInt(713715) + recipient := testAddress(0xa8) + + t.Run("wrong chain id", func(t *testing.T) { + wrongChainID := big.NewInt(1) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(1_000_000_000_000_000)) + rawTx := signLegacyTx(t, key, wrongChainID, 0, &recipient, big.NewInt(1), nil) + executor := NewExecutor(Config{}, WithState(state)) + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: blockContext(chainID), + Txs: [][]byte{rawTx}, + }) + + require.Error(t, err) + require.True(t, errors.Is(err, ethtypes.ErrInvalidChainId)) + require.Nil(t, result) + require.Equal(t, uint64(0), state.GetNonce(sender)) + require.Equal(t, big.NewInt(0), state.GetBalance(recipient)) + }) + + t.Run("invalid signature values", func(t *testing.T) { + state := NewMemoryState() + rawTx := legacyTxWithSignatureValues( + t, + 0, + &recipient, + big.NewInt(1), + nil, + 100_000, + big.NewInt(testGasPriceWei), + new(big.Int).Add(big.NewInt(35), new(big.Int).Mul(big.NewInt(2), chainID)), + new(big.Int), + new(big.Int), + ) + executor := NewExecutor(Config{}, WithState(state)) + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: blockContext(chainID), + Txs: [][]byte{rawTx}, + }) + + require.Error(t, err) + require.True(t, errors.Is(err, ethtypes.ErrInvalidSig)) + require.Nil(t, result) + require.Equal(t, big.NewInt(0), state.GetBalance(recipient)) + }) +} + +func TestExecutorCreatesContractThenUpdatesStorage(t *testing.T) { + chainID := big.NewInt(713715) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + storageKey := testHash(0x11) + storageValue := testHash(0x22) + runtime := storeCode(storageKey, storageValue) + contractAddr := crypto.CreateAddress(sender, 0) + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(2_000_000_000_000_000)) + + createContract := signLegacyTxWithGas(t, key, chainID, 0, nil, big.NewInt(0), initCode(runtime), 300_000) + callContract := signLegacyTx(t, key, chainID, 1, &contractAddr, big.NewInt(0), nil) + executor := NewExecutor(Config{}, WithState(state)) + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: blockContext(chainID), + Txs: [][]byte{createContract, callContract}, + }) + + require.NoError(t, err) + require.Len(t, result.Receipts, 2) + require.Equal(t, ethtypes.ReceiptStatusSuccessful, result.Txs[0].Status) + require.Equal(t, contractAddr, result.Txs[0].ContractAddress) + require.Equal(t, contractAddr, result.Receipts[0].ContractAddress) + require.Equal(t, ethtypes.ReceiptStatusSuccessful, result.Txs[1].Status) + + state.ApplyChangeSet(result.ChangeSet) + require.Equal(t, runtime, state.GetCode(contractAddr)) + require.Equal(t, storageValue, state.GetState(contractAddr, storageKey)) + require.Equal(t, uint64(2), state.GetNonce(sender)) +} + +func TestExecutorCreateSelfDestructThenTransferSameAddress(t *testing.T) { + chainID := big.NewInt(713715) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + beneficiary := testAddress(0xb2) + runtime := selfDestructCode(beneficiary) + contractAddr := crypto.CreateAddress(sender, 0) + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(2_000_000_000_000_000)) + + createContract := signLegacyTxWithGas(t, key, chainID, 0, nil, big.NewInt(0), initCode(runtime), 300_000) + destroyContract := signLegacyTx(t, key, chainID, 1, &contractAddr, big.NewInt(0), nil) + transferToDestroyed := signLegacyTx(t, key, chainID, 2, &contractAddr, big.NewInt(9), nil) + executor := NewExecutor(Config{ + ChainConfig: legacySelfDestructChainConfig(chainID), + }, WithState(state)) + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: blockContext(chainID), + Txs: [][]byte{createContract, destroyContract, transferToDestroyed}, + }) + + require.NoError(t, err) + require.Len(t, result.Receipts, 3) + for _, txResult := range result.Txs { + require.Equal(t, ethtypes.ReceiptStatusSuccessful, txResult.Status) + } + + state.ApplyChangeSet(result.ChangeSet) + require.Empty(t, state.GetCode(contractAddr)) + require.Equal(t, big.NewInt(9), state.GetBalance(contractAddr)) + require.Equal(t, uint64(0), state.GetNonce(contractAddr)) + require.Equal(t, big.NewInt(0), state.GetBalance(beneficiary)) + require.Equal(t, uint64(3), state.GetNonce(sender)) +} + +func TestExecutorEIP6780CreateFlagExpiresAfterTx(t *testing.T) { + chainID := big.NewInt(713715) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + beneficiary := testAddress(0xb3) + runtime := selfDestructCode(beneficiary) + contractAddr := crypto.CreateAddress(sender, 0) + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(2_000_000_000_000_000)) + + createContract := signLegacyTxWithGas(t, key, chainID, 0, nil, big.NewInt(0), initCode(runtime), 300_000) + selfDestructAfterCreateTx := signLegacyTx(t, key, chainID, 1, &contractAddr, big.NewInt(0), nil) + executor := NewExecutor(Config{}, WithState(state)) + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: blockContext(chainID), + Txs: [][]byte{createContract, selfDestructAfterCreateTx}, + }) + + require.NoError(t, err) + require.Len(t, result.Receipts, 2) + for _, txResult := range result.Txs { + require.Equal(t, ethtypes.ReceiptStatusSuccessful, txResult.Status) + } + + state.ApplyChangeSet(result.ChangeSet) + require.Equal(t, runtime, state.GetCode(contractAddr)) + require.Equal(t, uint64(1), state.GetNonce(contractAddr)) + require.Equal(t, big.NewInt(0), state.GetBalance(beneficiary)) +} + +func TestExecutorFinalisesAfterEachTx(t *testing.T) { + chainID := big.NewInt(713715) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + contract := common.HexToAddress("0x00000000000000000000000000000000000000c1") + beneficiary := common.HexToAddress("0x00000000000000000000000000000000000000b1") + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(500_000_000_000_000)) + state.SetCode(contract, selfDestructCode(beneficiary)) + + firstCall := signLegacyTx(t, key, chainID, 0, &contract, big.NewInt(0), nil) + secondCall := signLegacyTx(t, key, chainID, 1, &contract, big.NewInt(5), nil) + executor := NewExecutor(Config{ + ChainConfig: legacySelfDestructChainConfig(chainID), + }, WithState(state)) + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: blockContext(chainID), + Txs: [][]byte{firstCall, secondCall}, + }) + + require.NoError(t, err) + require.Len(t, result.Receipts, 2) + + state.ApplyChangeSet(result.ChangeSet) + require.Empty(t, state.GetCode(contract)) + require.Equal(t, big.NewInt(5), state.GetBalance(contract)) + require.Equal(t, big.NewInt(0), state.GetBalance(beneficiary)) +} + +func TestPrepareClearsTransientStorage(t *testing.T) { + stateDB := newNativeStateDB(NewMemoryState()) + addr := common.HexToAddress("0x00000000000000000000000000000000000000a3") + key := common.HexToHash("0x01") + value := common.HexToHash("0x02") + + stateDB.SetTransientState(addr, key, value) + require.Equal(t, value, stateDB.GetTransientState(addr, key)) + + stateDB.Prepare(params.Rules{}, addr, common.Address{}, nil, nil, nil) + + require.Equal(t, common.Hash{}, stateDB.GetTransientState(addr, key)) +} + +func TestSnapshotRevertRestoresBaseState(t *testing.T) { + addr := common.HexToAddress("0x00000000000000000000000000000000000000a4") + key := common.HexToHash("0x01") + value := common.HexToHash("0x02") + + state := NewMemoryState() + state.SetState(addr, key, value) + stateDB := newNativeStateDB(state) + stateDB.GetBalance(addr) + + snapshot := stateDB.Snapshot() + require.Equal(t, value, stateDB.GetState(addr, key)) + stateDB.RevertToSnapshot(snapshot) + + require.Empty(t, stateDB.ChangeSet().Storage) +} + +func 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) + + stateDB.Finalise(true) + + require.Zero(t, stateDB.GetRefund()) +} + +func TestExecutorCustomPrecompilePlaceholder(t *testing.T) { + chainID := big.NewInt(713715) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + customAddr := common.HexToAddress("0x0000000000000000000000000000000000001001") + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(200_000_000_000_000)) + + rawTx := signLegacyTx(t, key, chainID, 0, &customAddr, big.NewInt(0), []byte{0x01}) + executor := NewExecutor(Config{ + CustomPrecompiles: staticPrecompileRegistry{addr: customAddr}, + }, WithState(state)) + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: blockContext(chainID), + Txs: [][]byte{rawTx}, + }) + + require.NoError(t, err) + require.Len(t, result.Txs, 1) + require.Len(t, result.Receipts, 1) + require.Equal(t, ethtypes.ReceiptStatusFailed, result.Txs[0].Status) + require.True(t, errors.Is(result.Txs[0].Err, precompiles.ErrCustomPrecompilesOpen)) +} + +func 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.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() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + stakingAddr := common.HexToAddress(stakingprecompile.StakingAddress) + + state := NewMemoryState() + initialBalance := sei(100) + 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 := sei(5) + rawTx := signLegacyTxWithGas(t, key, chainID, 0, &stakingAddr, value, input, 8_000_000) + 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 := sei(1000) + 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 := signLegacyTxWithGas(t, key, chainID, nonces[sender], &stakingAddr, value, input, 8_000_000) + 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 := sei(10) + destinationSelfStake := sei(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, "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, + "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, "15000000", "0") + requireStakingValidator(t, state, source, "10000000", "10000000", 3) + requireStakingValidator(t, state, destination, "5000000", "5000000", 3) + + delegationValue := sei(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, "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_000_000) + 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, "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{ + 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, "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, "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, "2000000", undelegationCompletion) + + undelegationMaturityResult := executeBlockAndApply(t, executor, state, blockContextAt(chainID, 7, uint64(undelegationCompletion)), nil) + require.Empty(t, undelegationMaturityResult.ValidatorUpdates) + addExpectedBalance(expectedBalances, delegator, sei(2)) + addExpectedBalance(expectedBalances, escrowAddr, new(big.Int).Neg(sei(2))) + requireNativeBalances(t, state, expectedBalances) + 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) +} + +func signLegacyTx(t *testing.T, key *ecdsa.PrivateKey, chainID *big.Int, nonce uint64, to *common.Address, value *big.Int, data []byte) []byte { + t.Helper() + return signLegacyTxWithGas(t, key, chainID, nonce, to, value, data, 100_000) +} + +func signLegacyTxWithGas(t *testing.T, key *ecdsa.PrivateKey, chainID *big.Int, nonce uint64, to *common.Address, value *big.Int, data []byte, gas uint64) []byte { + t.Helper() + return signLegacyTxWithGasPrice(t, key, chainID, nonce, to, value, data, gas, big.NewInt(testGasPriceWei)) +} + +func signLegacyTxWithGasPrice(t *testing.T, key *ecdsa.PrivateKey, chainID *big.Int, nonce uint64, to *common.Address, value *big.Int, data []byte, gas uint64, gasPrice *big.Int) []byte { + t.Helper() + tx := ethtypes.NewTx(ðtypes.LegacyTx{ + Nonce: nonce, + GasPrice: new(big.Int).Set(gasPrice), + Gas: gas, + To: to, + Value: value, + Data: data, + }) + signed, err := ethtypes.SignTx(tx, ethtypes.LatestSignerForChainID(chainID), key) + require.NoError(t, err) + raw, err := signed.MarshalBinary() + require.NoError(t, err) + return raw +} + +func legacyTxWithSignatureValues(t *testing.T, nonce uint64, to *common.Address, value *big.Int, data []byte, gas uint64, gasPrice *big.Int, v *big.Int, r *big.Int, s *big.Int) []byte { + t.Helper() + tx := ethtypes.NewTx(ðtypes.LegacyTx{ + Nonce: nonce, + GasPrice: new(big.Int).Set(gasPrice), + Gas: gas, + To: to, + Value: value, + Data: data, + V: new(big.Int).Set(v), + R: new(big.Int).Set(r), + S: new(big.Int).Set(s), + }) + raw, err := tx.MarshalBinary() + require.NoError(t, err) + return raw +} + +func decodeTx(t *testing.T, raw []byte) *ethtypes.Transaction { + t.Helper() + var tx ethtypes.Transaction + require.NoError(t, tx.UnmarshalBinary(raw)) + return &tx +} + +func signDynamicFeeTx(t *testing.T, key *ecdsa.PrivateKey, chainID *big.Int, nonce uint64, to *common.Address, value *big.Int, data []byte) []byte { + t.Helper() + return signDynamicFeeTxWithFees( + t, + key, + chainID, + nonce, + to, + value, + data, + big.NewInt(testGasPriceWei), + big.NewInt(testGasPriceWei), + 100_000, + ) +} + +func signDynamicFeeTxWithFees(t *testing.T, key *ecdsa.PrivateKey, chainID *big.Int, nonce uint64, to *common.Address, value *big.Int, data []byte, gasTipCap *big.Int, gasFeeCap *big.Int, gas uint64) []byte { + t.Helper() + tx := ethtypes.NewTx(ðtypes.DynamicFeeTx{ + ChainID: chainID, + Nonce: nonce, + GasTipCap: new(big.Int).Set(gasTipCap), + GasFeeCap: new(big.Int).Set(gasFeeCap), + Gas: gas, + To: to, + Value: value, + Data: data, + }) + signed, err := ethtypes.SignTx(tx, ethtypes.LatestSignerForChainID(chainID), key) + require.NoError(t, err) + raw, err := signed.MarshalBinary() + require.NoError(t, err) + return raw +} + +func blockContext(chainID *big.Int) BlockContext { + return BlockContext{ + Number: 1, + Time: 1, + GasLimit: 30_000_000, + ChainID: chainID, + BaseFee: big.NewInt(0), + Coinbase: common.HexToAddress("0x00000000000000000000000000000000000000cb"), + } +} + +func 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 +} + +// 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) { + 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, + HomesteadBlock: big.NewInt(0), + DAOForkBlock: nil, + DAOForkSupport: false, + EIP150Block: big.NewInt(0), + EIP155Block: big.NewInt(0), + EIP158Block: big.NewInt(0), + ByzantiumBlock: big.NewInt(0), + ConstantinopleBlock: big.NewInt(0), + PetersburgBlock: big.NewInt(0), + IstanbulBlock: big.NewInt(0), + BerlinBlock: big.NewInt(0), + LondonBlock: big.NewInt(0), + } +} + +func selfDestructCode(beneficiary common.Address) []byte { + code := append([]byte{0x73}, beneficiary.Bytes()...) + return append(code, 0xff) +} + +func log0Code() []byte { + return []byte{0x60, 0x00, 0x60, 0x00, 0xa0, 0x00} +} + +func storeCode(key, value common.Hash) []byte { + code := append([]byte{0x7f}, value.Bytes()...) + code = append(code, 0x7f) + code = append(code, key.Bytes()...) + return append(code, 0x55, 0x00) +} + +func initCode(runtime []byte) []byte { + if len(runtime) > 255 { + panic("test runtime too large") + } + runtimeLen := byte(len(runtime)) //nolint:gosec // bounded by the check above. + code := []byte{ + 0x60, runtimeLen, + 0x60, 0x0c, + 0x60, 0x00, + 0x39, + 0x60, runtimeLen, + 0x60, 0x00, + 0xf3, + } + return append(code, runtime...) +} + +func testAddress(suffix byte) common.Address { + return common.BytesToAddress([]byte{suffix}) +} + +func testHash(suffix byte) common.Hash { + return common.BytesToHash([]byte{suffix}) +} + +type staticPrecompileRegistry struct { + addr common.Address +} + +func (r staticPrecompileRegistry) Get(addr common.Address) (precompiles.Contract, bool) { + return nil, addr == r.addr +} + +func (r staticPrecompileRegistry) Addresses() []common.Address { + return []common.Address{r.addr} +} + +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/occ.go b/giga/evmonly/occ.go new file mode 100644 index 0000000000..30654c46fd --- /dev/null +++ b/giga/evmonly/occ.go @@ -0,0 +1,443 @@ +package evmonly + +import ( + "bytes" + "context" + "fmt" + "math" + "math/big" + "sort" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/params" +) + +type occTxExecution struct { + txResult TxResult + receipt *ethtypes.Receipt + changeSet StateChangeSet + readSet map[stateAccessKey]struct{} + writeSet map[stateAccessKey]struct{} + gasUsed uint64 +} + +type occTxRange struct { + start int + end int +} + +func (e *Executor) executeBlockOCC(ctx context.Context, req PreparedBlock) (*BlockResult, error) { + chainConfig := e.chainConfig(req.Context) + blockCtx := buildBlockContext(req.Context) + baseFee := cloneBig(req.Context.BaseFee) + gasLimit := req.Context.GasLimit + if gasLimit == 0 { + gasLimit = math.MaxUint64 + } + + workers := e.cfg.OCCWorkers + txCount := len(req.Txs) + if workers > txCount { + workers = txCount + } + results := make([]occTxExecution, txCount) + chunkSize := occChunkSize(txCount, workers) + pool := e.occPool + if pool == nil { + pool = newOCCWorkerPool(workers) + defer pool.Close() + } + if err := pool.Run(ctx, occRanges(txCount, chunkSize), func(workerCtx context.Context, txRange occTxRange) error { + for idx := 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 + }); err != nil { + return nil, err + } + validation := validateOCCResults(results, gasLimit) + if !validation.valid { + result, err := e.executeBlockSequential(ctx, req) + if err != nil { + return nil, err + } + result.OCCStats = validation.stats(true) + return result, nil + } + result, err := e.mergeOCCResults(ctx, results) + if err != nil { + return nil, err + } + result.OCCStats = validation.stats(false) + return result, nil +} + +func occRanges(txCount int, chunkSize int) []occTxRange { + if chunkSize <= 0 { + chunkSize = 1 + } + ranges := make([]occTxRange, 0, (txCount+chunkSize-1)/chunkSize) + 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 + } + targetChunks := workers * 8 + chunkSize := (txCount + targetChunks - 1) / targetChunks + if chunkSize < 16 { + return 16 + } + if chunkSize > 256 { + return 256 + } + return chunkSize +} + +func (e *Executor) executeTxSpeculative( + ctx context.Context, + req PreparedBlock, + txIndex int, + chainConfig *params.ChainConfig, + blockCtx vm.BlockContext, + baseFee *big.Int, + gasLimit uint64, +) (occTxExecution, error) { + p := req.Txs[txIndex] + stateDB := newNativeStateDB(e.state) + stateDB.enableAccessTracking() + evm := vm.NewEVM(blockCtx, stateDB, chainConfig, vm.Config{}, nil) + stateDB.SetEVM(evm) + gasPool := new(core.GasPool).AddGas(gasLimit) + txResult, receipt, err := e.executeTx( + evm, + stateDB, + gasPool, + req.Context, + p, + txIndex, + uint(txIndex), + baseFee, + ) + if err != nil { + return occTxExecution{}, fmt.Errorf("execute tx %d %s: %w", txIndex, p.Tx.Hash(), err) + } + readSet, writeSet := stateDB.accessSets() + var changeSet StateChangeSet + stateDB.ChangeSetInto(&changeSet) + return occTxExecution{ + txResult: txResult, + receipt: receipt, + changeSet: changeSet, + readSet: readSet, + writeSet: writeSet, + gasUsed: txResult.GasUsed, + }, nil +} + +type occValidationResult struct { + valid bool + fallbackReason string + conflictCount uint64 + conflicts map[occConflictAggregationKey]uint64 +} + +type occConflictAggregationKey struct { + access string + kind stateAccessKind + address common.Address + slot common.Hash +} + +const ( + occFallbackReasonConflict = "conflict" + occFallbackReasonGasLimit = "gas_limit" + occFallbackReasonGasOverflow = "gas_overflow" +) + +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 { + validation.valid = false + validation.fallbackReason = occFallbackReasonGasOverflow + return validation + } + totalGas += result.gasUsed + if totalGas > gasLimit { + validation.valid = false + validation.fallbackReason = occFallbackReasonGasLimit + return validation + } + validation.addConflicts("read", writes, result.readSet) + validation.addConflicts("write", writes, result.writeSet) + writes.addAll(result.writeSet) + } + if validation.conflictCount > 0 { + validation.valid = false + validation.fallbackReason = occFallbackReasonConflict + } + return validation +} + +func (r *occValidationResult) addConflicts(access string, writes *stateAccessIndex, set map[stateAccessKey]struct{}) { + for key := range set { + if !writes.conflictsWith(key) { + continue + } + if r.conflicts == nil { + r.conflicts = map[occConflictAggregationKey]uint64{} + } + r.conflictCount++ + r.conflicts[occConflictAggregationKey{ + access: access, + kind: key.kind, + address: key.address, + slot: key.slot, + }]++ + } +} + +func (r occValidationResult) stats(fallback bool) OCCStats { + stats := OCCStats{ + Attempted: true, + Fallback: fallback, + FallbackReason: r.fallbackReason, + ConflictCount: r.conflictCount, + } + if len(r.conflicts) == 0 { + return stats + } + keys := make([]occConflictAggregationKey, 0, len(r.conflicts)) + for key := range r.conflicts { + keys = append(keys, key) + } + sort.Slice(keys, func(i, j int) bool { + left, right := keys[i], keys[j] + if left.access != right.access { + return left.access < right.access + } + if left.kind != right.kind { + return left.kind < right.kind + } + if cmp := bytes.Compare(left.address[:], right.address[:]); cmp != 0 { + return cmp < 0 + } + return bytes.Compare(left.slot[:], right.slot[:]) < 0 + }) + for _, key := range keys { + stats.ConflictSamples = append(stats.ConflictSamples, OCCConflictCount{ + Access: key.access, + Kind: key.kind.String(), + Address: key.address, + Slot: key.slot, + Count: r.conflicts[key], + }) + } + return stats +} + +func (k stateAccessKind) String() string { + switch k { + case stateAccessAccount: + return "account" + case stateAccessBalance: + return "balance" + case stateAccessNonce: + return "nonce" + case stateAccessCode: + return "code" + case stateAccessStorage: + return "storage" + default: + return "unknown" + } +} + +func (e *Executor) mergeOCCResults(ctx context.Context, results []occTxExecution) (*BlockResult, error) { + blockResult, err := e.acquireBlockResult(ctx, len(results)) + if err != nil { + return nil, err + } + blockResult.prepareIndexedResults(len(results)) + var logIndex uint + for i, result := range results { + blockResult.GasUsed += result.gasUsed + result.txResult.CumulativeGasUsed = blockResult.GasUsed + result.receipt.CumulativeGasUsed = blockResult.GasUsed + for _, log := range result.receipt.Logs { + log.Index = logIndex + logIndex++ + } + blockResult.Txs[i] = result.txResult + blockResult.Receipts[i] = result.receipt + } + mergeChangeSetsInto(results, &blockResult.ChangeSet) + return blockResult, nil +} + +type stateAccessIndex struct { + exact map[stateAccessKey]struct{} + account map[common.Address]struct{} + touched map[common.Address]struct{} +} + +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{}{} + // 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{}{} + } + } +} + +type storageChangeKey struct { + address common.Address + key common.Hash +} + +func mergeChangeSets(results []occTxExecution) StateChangeSet { + var merged StateChangeSet + mergeChangeSetsInto(results, &merged) + return merged +} + +func mergeChangeSetsInto(results []occTxExecution, merged *StateChangeSet) { + merged.resetForReuse() + balances := map[common.Address]*big.Int{} + nonces := map[common.Address]uint64{} + code := map[common.Address]CodeChange{} + 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 + } + } + + 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]) + } +} + +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/occ_pool.go b/giga/evmonly/occ_pool.go new file mode 100644 index 0000000000..0678968a96 --- /dev/null +++ b/giga/evmonly/occ_pool.go @@ -0,0 +1,119 @@ +package evmonly + +import ( + "context" + "fmt" + "sync" +) + +type occWorkerPool struct { + jobs chan occPoolJob + stop chan struct{} + closed chan struct{} + once sync.Once +} + +type occPoolJob struct { + ctx context.Context + txRange occTxRange + run func(context.Context, occTxRange) error + + done *sync.WaitGroup + cancel context.CancelFunc + errOnce *sync.Once + err *error +} + +func newOCCWorkerPool(workers int) *occWorkerPool { + p := &occWorkerPool{ + jobs: make(chan occPoolJob, workers*2), + stop: make(chan struct{}), + closed: make(chan struct{}), + } + var workerWG sync.WaitGroup + workerWG.Add(workers) + for range workers { + go func() { + defer workerWG.Done() + p.runWorker() + }() + } + go func() { + workerWG.Wait() + close(p.closed) + }() + return p +} + +func (p *occWorkerPool) runWorker() { + for { + select { + case <-p.stop: + return + case job := <-p.jobs: + p.runJob(job) + } + } +} + +func (p *occWorkerPool) runJob(job occPoolJob) { + defer job.done.Done() + if err := job.ctx.Err(); err != nil { + return + } + if err := job.run(job.ctx, job.txRange); err != nil { + job.errOnce.Do(func() { + *job.err = err + job.cancel() + }) + } +} + +func (p *occWorkerPool) Run(ctx context.Context, ranges []occTxRange, run func(context.Context, occTxRange) error) error { + jobCtx, cancel := context.WithCancel(ctx) + defer cancel() + + var done sync.WaitGroup + var err error + var errOnce sync.Once +dispatch: + for _, txRange := range ranges { + done.Add(1) + job := occPoolJob{ + ctx: jobCtx, + txRange: txRange, + run: run, + done: &done, + cancel: cancel, + errOnce: &errOnce, + err: &err, + } + select { + case p.jobs <- job: + case <-jobCtx.Done(): + done.Done() + break dispatch + case <-p.stop: + done.Done() + return fmt.Errorf("OCC worker pool is closed") + } + } + done.Wait() + if err != nil { + return err + } + if ctxErr := ctx.Err(); ctxErr != nil { + return ctxErr + } + return nil +} + +func (p *occWorkerPool) Close() { + if p == nil { + return + } + p.once.Do(func() { + close(p.stop) + <-p.closed + }) +} diff --git a/giga/evmonly/parser.go b/giga/evmonly/parser.go new file mode 100644 index 0000000000..99b366fc61 --- /dev/null +++ b/giga/evmonly/parser.go @@ -0,0 +1,38 @@ +package evmonly + +import ( + "context" + "fmt" + + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" +) + +func parseBlockTxs(ctx context.Context, txs [][]byte, signer ethtypes.Signer) ([]PreparedTx, error) { + parsed := make([]PreparedTx, len(txs)) + for i, raw := range txs { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + tx, sender, err := parseTx(raw, signer) + if err != nil { + return nil, fmt.Errorf("parse tx %d: %w", i, err) + } + parsed[i] = PreparedTx{Tx: tx, Sender: sender} + } + return parsed, nil +} + +func parseTx(raw []byte, signer ethtypes.Signer) (*ethtypes.Transaction, common.Address, error) { + var tx ethtypes.Transaction + if err := tx.UnmarshalBinary(raw); err != nil { + return nil, common.Address{}, err + } + sender, err := ethtypes.Sender(signer, &tx) + if err != nil { + return nil, common.Address{}, err + } + return &tx, sender, nil +} diff --git a/giga/evmonly/precompile_adapter.go b/giga/evmonly/precompile_adapter.go new file mode 100644 index 0000000000..9a2348c166 --- /dev/null +++ b/giga/evmonly/precompile_adapter.go @@ -0,0 +1,352 @@ +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, 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) { + meter := newPrecompileGasMeter(suppliedGas, hooks) + if !meter.charge(p.RequiredGas(input), tracing.GasChangeCallPrecompiledContract) { + return nil, 0, vm.ErrOutOfGas + } + ret, err := p.run(evm, caller, input, value, readOnly, isFromDelegateCall, meter.remainingGas(), meter) + if meter.err != nil { + return nil, meter.remainingGas(), meter.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, meter *precompileGasMeter) ([]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, meter: meter}, + Balances: nativeBalanceTransfer{db: stateDB, meter: meter}, + Logs: meteredLogSink{sink: stateDB, meter: meter}, + } + 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 + 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 + } + 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 + 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 { + 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++ { + 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) + if oldOK { + oldChunks = chunkCount(oldLength) + } + 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. + end := start + 32 + if end > len(value) { + end = len(value) + } + var chunk common.Hash + copy(chunk[:], value[start:end]) + 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++ { + 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++ { + 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 + } + 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/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/context.go b/giga/evmonly/precompiles/context.go new file mode 100644 index 0000000000..390566ff5f --- /dev/null +++ b/giga/evmonly/precompiles/context.go @@ -0,0 +1,91 @@ +package precompiles + +import ( + "errors" + "math/big" + + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" +) + +var ErrCustomPrecompilesOpen = errors.New("evm-only custom precompiles are not implemented") + +// Registry resolves native custom precompiles for the EVM-only path. +type Registry interface { + Get(common.Address) (Contract, bool) + Addresses() []common.Address +} + +// Contract is the sdk.Context-free custom precompile interface. +type Contract interface { + RequiredGas(input []byte) uint64 + Run(*Context, []byte) ([]byte, error) +} + +// 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 { + Caller common.Address + Address common.Address + ApparentValue *big.Int + ReadOnly bool + DelegateCall bool + GasRemaining uint64 + Block BlockContext + 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 + Time uint64 + ChainID *big.Int + BaseFee *big.Int + BlobBaseFee *big.Int + Coinbase common.Address + PrevRandao common.Hash +} + +// 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. +type LogSink interface { + AddLog(*ethtypes.Log) +} 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/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 new file mode 100644 index 0000000000..2a916a1ea4 --- /dev/null +++ b/giga/evmonly/precompiles/staking/endblock.go @@ -0,0 +1,370 @@ +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 + } + if err := trackHistoricalInfo(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 = saturatingInt64FromUint64(block.Number) + 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] + blockTime := saturatingInt64FromUint64(ctx.Block.Time) + for _, entry := range record.Entries { + if entry.CompletionTime > blockTime { + 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] + completionTime := saturatingInt64FromUint64(blockTime) + for _, entry := range record.Entries { + if entry.CompletionTime > completionTime { + 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/guardrails_test.go b/giga/evmonly/precompiles/staking/guardrails_test.go new file mode 100644 index 0000000000..ebaba619e9 --- /dev/null +++ b/giga/evmonly/precompiles/staking/guardrails_test.go @@ -0,0 +1,208 @@ +package staking + +import ( + "math/big" + "strings" + "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, + operator.Hex()[2:], + "moniker", + "0.100000000000000000", + "0.200000000000000000", + "0.010000000000000000", + big.NewInt(1), + ) + 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) + 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/helpers.go b/giga/evmonly/precompiles/staking/helpers.go new file mode 100644 index 0000000000..be31cf08af --- /dev/null +++ b/giga/evmonly/precompiles/staking/helpers.go @@ -0,0 +1,72 @@ +package staking + +import ( + "errors" + "fmt" + "math/big" + "strconv" + "strings" + + "github.com/ethereum/go-ethereum/common" + + "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 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 + } + 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..e2985bebb5 --- /dev/null +++ b/giga/evmonly/precompiles/staking/staking.go @@ -0,0 +1,904 @@ +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 + baseGas uint64 = 700 + inputByteGas uint64 = 16 + + bondDenom = "usei" + precision int64 = 18 + pageLimit = 100 +) + +const ( + bondStatusUnspecified int32 = 0 + bondStatusUnbonded int32 = 1 + bondStatusUnbonding int32 = 2 + bondStatusBonded int32 = 3 + + // powerReduction matches Cosmos sdk.DefaultPowerReduction so consensus + // power is denominated in whole SEI (1e6 usei == 1 power). + powerReduction int64 = 1_000_000 +) + +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") + 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") + 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") +) + +//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 { + _, _, err := p.prepare(input) + if err != nil { + return unknownMethodGas + } + 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) { + 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 := normalizeValidatorAddress(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 := 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 + } + if srcValidator == dstValidator { + return nil, errSelfRedelegation + } + 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) + } + 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 + } + 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 + } + 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 := normalizeValidatorAddress(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 + } + 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 + } + 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 + } + 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") + } + params, err := loadParams(ctx.Store) + if err != nil { + return nil, err + } + if err := validateInitialCommission(commissionRate, commissionMaxRate, commissionMaxChangeRate, params.MinCommissionRate); 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 _, 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 + } + 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: saturatingInt64FromUint64(ctx.Block.Time), + 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 != "" { + 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 { + 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 := normalizeValidatorAddress(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)) + matched := make(map[string]Validator, 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) + matched[validatorAddress] = validator + } + } + 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 { + result.Validators = append(result.Validators, matched[validatorAddress]) + } + 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, normalizeValidatorAddress(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 := normalizeValidatorAddress(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 := normalizeValidatorAddress(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 := normalizeValidatorAddress(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 := normalizeValidatorAddress(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 := 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 { + 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 { + 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..ae5e5046aa --- /dev/null +++ b/giga/evmonly/precompiles/staking/staking_test.go @@ -0,0 +1,304 @@ +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") + // 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(selfStakeUsei, 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(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(delegateUsei, 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(totalUsei, 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) + require.Equal(t, int64(7), updates[0].Power) + + 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, 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) + + 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, "7000000", 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..11960ee2a5 --- /dev/null +++ b/giga/evmonly/precompiles/staking/state.go @@ -0,0 +1,772 @@ +package staking + +import ( + "encoding/hex" + "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" +) + +// 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 { + 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 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, + }, + }, nil +} + +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) +} + +func getValidator(store precompiles.Store, validatorAddress string) (Validator, bool, error) { + 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) +} + +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: saturatingInt64FromUint64(creationHeight), + 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)) +} + +// 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 { + 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) + } + } + 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 { + 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] + completionTime := saturatingInt64FromUint64(blockTime) + for _, id := range ids { + time, err := parseTimeQueueID(id) + if err != nil { + return nil, err + } + if time <= completionTime { + 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 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 <= completionTime && height <= completionHeight { + 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 validatorConsensusPubkeyKey(pubKey []byte) []byte { + return []byte("validator-consensus-pubkey/" + hex.EncodeToString(pubKey)) +} + +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 historicalInfoIndexKey() []byte { + return []byte("historical/index") +} + +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..bbb58ebf96 --- /dev/null +++ b/giga/evmonly/precompiles/util/helpers.go @@ -0,0 +1,59 @@ +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 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 new file mode 100644 index 0000000000..d8fcc17c76 --- /dev/null +++ b/giga/evmonly/result_pool.go @@ -0,0 +1,127 @@ +package evmonly + +import ( + "context" + "sync" + "sync/atomic" + + ethtypes "github.com/ethereum/go-ethereum/core/types" +) + +type blockResultPool struct { + free chan *BlockResult +} + +type blockResultLease struct { + pool *blockResultPool + result *BlockResult + refs atomic.Int32 +} + +func newBlockResultPool(size int) *blockResultPool { + if size <= 0 { + return nil + } + p := &blockResultPool{free: make(chan *BlockResult, size)} + for range size { + p.free <- &BlockResult{} + } + return p +} + +func (p *blockResultPool) acquire(ctx context.Context, txCapacity int) (*BlockResult, error) { + if p == nil { + result := &BlockResult{} + result.prepareForBlock(txCapacity) + return result, nil + } + select { + case result := <-p.free: + result.prepareForBlock(txCapacity) + lease := &blockResultLease{pool: p, result: result} + lease.refs.Store(1) + result.lease = lease + return result, nil + case <-ctx.Done(): + return nil, ctx.Err() + } +} + +func (l *blockResultLease) retain() func() { + if l == nil { + return func() {} + } + l.refs.Add(1) + var once sync.Once + return func() { + once.Do(l.release) + } +} + +func (l *blockResultLease) release() { + if l == nil { + return + } + if l.refs.Add(-1) != 0 { + return + } + result := l.result + result.resetForPool() + l.pool.free <- result +} + +func (r *BlockResult) retain() func() { + if r == nil || r.lease == nil { + return func() {} + } + return r.lease.retain() +} + +func (r *BlockResult) prepareForBlock(txCapacity int) { + r.resetForPool() + if cap(r.Txs) < txCapacity { + r.Txs = make([]TxResult, 0, txCapacity) + } + if cap(r.Receipts) < txCapacity { + r.Receipts = make(ethtypes.Receipts, 0, txCapacity) + } +} + +func (r *BlockResult) prepareIndexedResults(txCount int) { + if cap(r.Txs) < txCount { + r.Txs = make([]TxResult, txCount) + } else { + r.Txs = r.Txs[:txCount] + clear(r.Txs) + } + if cap(r.Receipts) < txCount { + r.Receipts = make(ethtypes.Receipts, txCount) + } else { + r.Receipts = r.Receipts[:txCount] + clear(r.Receipts) + } +} + +func (r *BlockResult) resetForPool() { + r.ChangeSet.resetForReuse() + clear(r.ValidatorUpdates) + r.ValidatorUpdates = r.ValidatorUpdates[:0] + clear(r.Txs) + r.Txs = r.Txs[:0] + clear(r.Receipts) + r.Receipts = r.Receipts[:0] + r.GasUsed = 0 + r.OCCStats = OCCStats{} + r.lease = nil +} + +func (cs *StateChangeSet) resetForReuse() { + clear(cs.Balances) + cs.Balances = cs.Balances[:0] + clear(cs.Nonces) + cs.Nonces = cs.Nonces[:0] + clear(cs.Code) + cs.Code = cs.Code[:0] + clear(cs.Storage) + cs.Storage = cs.Storage[:0] +} 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...) +} diff --git a/giga/evmonly/state_db.go b/giga/evmonly/state_db.go new file mode 100644 index 0000000000..e91fcd6ed1 --- /dev/null +++ b/giga/evmonly/state_db.go @@ -0,0 +1,903 @@ +package evmonly + +import ( + "bytes" + "errors" + "math/big" + "sort" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/stateless" + "github.com/ethereum/go-ethereum/core/tracing" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/params" + ethutils "github.com/ethereum/go-ethereum/trie/utils" + "github.com/holiman/uint256" +) + +var errInsufficientBalance = errors.New("insufficient balance") + +type nativeStateDB struct { + source StateReader + + accounts map[common.Address]*nativeAccount + base map[common.Address]*nativeAccount + + refund uint64 + logs []*ethtypes.Log + preimages map[common.Hash][]byte + + accessList accessList + transientStates map[common.Address]map[common.Hash]common.Hash + finaliseAddrs map[common.Address]struct{} + journal []nativeJournalEntry + snapshots []nativeSnapshot + readSet map[stateAccessKey]struct{} + writeSet map[stateAccessKey]struct{} + + txHash common.Hash + txIndex int + txIndexUint uint + err error + evm *vm.EVM +} + +type nativeAccount struct { + Balance *uint256.Int + Nonce uint64 + Code []byte + Storage map[common.Hash]common.Hash + SelfDestructed bool + Created bool +} + +type nativeSnapshot struct { + journalLen int + refund uint64 + logsLen int + accessList accessList + transientStates map[common.Address]map[common.Hash]common.Hash + finaliseAddrs map[common.Address]struct{} + preimages map[common.Hash][]byte + journaledAddrs map[common.Address]struct{} + err error +} + +type nativeJournalKind uint8 + +const ( + nativeJournalAccount nativeJournalKind = iota +) + +type nativeJournalEntry struct { + kind nativeJournalKind + address common.Address + account *nativeAccount +} + +type accessList struct { + addresses map[common.Address]struct{} + slots map[common.Address]map[common.Hash]struct{} +} + +type stateAccessKind uint8 + +const ( + stateAccessAccount stateAccessKind = iota + stateAccessBalance + stateAccessNonce + stateAccessCode + stateAccessStorage +) + +type stateAccessKey struct { + kind stateAccessKind + address common.Address + slot common.Hash +} + +func newNativeStateDB(source StateReader) *nativeStateDB { + if source == nil { + source = NewMemoryState() + } + return &nativeStateDB{ + source: source, + accounts: map[common.Address]*nativeAccount{}, + base: map[common.Address]*nativeAccount{}, + preimages: map[common.Hash][]byte{}, + accessList: newAccessList(), + transientStates: map[common.Address]map[common.Hash]common.Hash{}, + finaliseAddrs: map[common.Address]struct{}{}, + } +} + +func (s *nativeStateDB) ChangeSet() StateChangeSet { + var changes StateChangeSet + s.ChangeSetInto(&changes) + return changes +} + +func (s *nativeStateDB) ChangeSetInto(changes *StateChangeSet) { + changes.resetForReuse() + addresses := make([]common.Address, 0, len(s.accounts)) + for addr := range s.accounts { + addresses = append(addresses, addr) + } + sort.Slice(addresses, func(i, j int) bool { + return bytes.Compare(addresses[i][:], addresses[j][:]) < 0 + }) + + for _, addr := range addresses { + acct := s.accounts[addr] + base := s.baseAccount(addr) + + if !acct.Balance.Eq(base.Balance) { + changes.Balances = append(changes.Balances, BalanceChange{ + Address: addr, + Balance: acct.Balance.ToBig(), + }) + } + if acct.Nonce != base.Nonce { + changes.Nonces = append(changes.Nonces, NonceChange{ + Address: addr, + Nonce: acct.Nonce, + }) + } + if !bytes.Equal(acct.Code, base.Code) { + changes.Code = append(changes.Code, CodeChange{ + Address: addr, + Code: cloneBytes(acct.Code), + Delete: len(acct.Code) == 0, + }) + } + storageKeys := storageKeyUnion(base.Storage, acct.Storage) + for _, key := range storageKeys { + oldValue := base.Storage[key] + newValue := acct.Storage[key] + if oldValue == newValue { + continue + } + changes.Storage = append(changes.Storage, StorageChange{ + Address: addr, + Key: key, + Value: newValue, + Delete: newValue == (common.Hash{}), + }) + } + } +} + +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) { + acct := s.account(addr) + s.recordAccount(addr) + s.markWrite(stateAccessKey{kind: stateAccessAccount, address: addr}) + acct.Created = true + s.markForFinalise(addr) +} + +func (s *nativeStateDB) SubBalance(addr common.Address, amount *uint256.Int, _ tracing.BalanceChangeReason) uint256.Int { + prev := *s.GetBalance(addr) + if amount == nil || amount.IsZero() { + return prev + } + acct := s.account(addr) + if acct.Balance.Cmp(amount) < 0 { + s.err = errInsufficientBalance + return prev + } + s.recordAccount(addr) + s.markWrite(stateAccessKey{kind: stateAccessBalance, address: addr}) + acct.Balance.Sub(acct.Balance, amount) + return prev +} + +func (s *nativeStateDB) AddBalance(addr common.Address, amount *uint256.Int, _ tracing.BalanceChangeReason) uint256.Int { + prev := *s.GetBalance(addr) + if amount == nil || amount.IsZero() { + return prev + } + acct := s.account(addr) + s.recordAccount(addr) + s.markWrite(stateAccessKey{kind: stateAccessBalance, address: addr}) + acct.Balance.Add(acct.Balance, amount) + return prev +} + +func (s *nativeStateDB) GetBalance(addr common.Address) *uint256.Int { + s.markRead(stateAccessKey{kind: stateAccessBalance, address: addr}) + return s.account(addr).Balance.Clone() +} + +func (s *nativeStateDB) SetBalance(addr common.Address, balance *uint256.Int, _ tracing.BalanceChangeReason) { + acct := s.account(addr) + s.recordAccount(addr) + s.markWrite(stateAccessKey{kind: stateAccessBalance, address: addr}) + if balance == nil { + acct.Balance = uint256.NewInt(0) + return + } + acct.Balance = balance.Clone() +} + +func (s *nativeStateDB) GetNonce(addr common.Address) uint64 { + s.markRead(stateAccessKey{kind: stateAccessNonce, address: addr}) + return s.account(addr).Nonce +} + +func (s *nativeStateDB) SetNonce(addr common.Address, nonce uint64, _ tracing.NonceChangeReason) { + acct := s.account(addr) + s.recordAccount(addr) + s.markWrite(stateAccessKey{kind: stateAccessNonce, address: addr}) + acct.Nonce = nonce +} + +func (s *nativeStateDB) GetCodeHash(addr common.Address) common.Hash { + code := s.GetCode(addr) + if len(code) == 0 { + return common.Hash{} + } + return crypto.Keccak256Hash(code) +} + +func (s *nativeStateDB) GetCode(addr common.Address) []byte { + s.markRead(stateAccessKey{kind: stateAccessCode, address: addr}) + return cloneBytes(s.account(addr).Code) +} + +func (s *nativeStateDB) SetCode(addr common.Address, code []byte) []byte { + acct := s.account(addr) + prev := cloneBytes(acct.Code) + s.recordAccount(addr) + s.markWrite(stateAccessKey{kind: stateAccessCode, address: addr}) + acct.Code = cloneBytes(code) + return prev +} + +func (s *nativeStateDB) GetCodeSize(addr common.Address) int { + s.markRead(stateAccessKey{kind: stateAccessCode, address: addr}) + return len(s.account(addr).Code) +} + +func (s *nativeStateDB) AddRefund(gas uint64) { + s.refund += gas +} + +func (s *nativeStateDB) SubRefund(gas uint64) { + if gas > s.refund { + panic("refund counter underflow") + } + s.refund -= gas +} + +func (s *nativeStateDB) GetRefund() uint64 { + return s.refund +} + +func (s *nativeStateDB) GetCommittedState(addr common.Address, key common.Hash) common.Hash { + s.markRead(stateAccessKey{kind: stateAccessStorage, address: addr, slot: key}) + 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] +} + +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] + s.recordAccount(addr) + s.markWrite(stateAccessKey{kind: stateAccessStorage, address: addr, slot: key}) + if value == (common.Hash{}) { + delete(acct.Storage, key) + } else { + acct.Storage[key] = value + } + return prev +} + +func (s *nativeStateDB) SetStorage(addr common.Address, states map[common.Hash]common.Hash) { + acct := s.account(addr) + s.recordAccount(addr) + s.markWrite(stateAccessKey{kind: stateAccessAccount, address: addr}) + 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() + s.recordAccount(addr) + s.markWrite(stateAccessKey{kind: stateAccessAccount, address: addr}) + acct.Balance.Clear() + acct.SelfDestructed = true + s.markForFinalise(addr) + return prev +} + +func (s *nativeStateDB) SelfDestruct6780(addr common.Address) (uint256.Int, bool) { + acct := s.account(addr) + if !acct.Created { + return *acct.Balance.Clone(), false + } + return s.SelfDestruct(addr), true +} + +func (s *nativeStateDB) HasSelfDestructed(addr common.Address) bool { + return s.account(addr).SelfDestructed +} + +func (s *nativeStateDB) Exist(addr common.Address) bool { + s.markRead(stateAccessKey{kind: stateAccessAccount, address: addr}) + acct := s.account(addr) + return acct.SelfDestructed || acct.Nonce != 0 || !acct.Balance.IsZero() || len(acct.Code) != 0 +} + +func (s *nativeStateDB) Empty(addr common.Address) bool { + s.markRead(stateAccessKey{kind: stateAccessAccount, address: addr}) + acct := s.account(addr) + return acct.Nonce == 0 && acct.Balance.IsZero() && len(acct.Code) == 0 +} + +func (s *nativeStateDB) AddressInAccessList(addr common.Address) bool { + _, ok := s.accessList.addresses[addr] + return ok +} + +func (s *nativeStateDB) SlotInAccessList(addr common.Address, slot common.Hash) (bool, bool) { + _, addressOk := s.accessList.addresses[addr] + if !addressOk { + return false, false + } + slots, ok := s.accessList.slots[addr] + if !ok { + return true, false + } + _, slotOk := slots[slot] + return true, slotOk +} + +func (s *nativeStateDB) AddAddressToAccessList(addr common.Address) { + s.accessList.addresses[addr] = struct{}{} +} + +func (s *nativeStateDB) AddSlotToAccessList(addr common.Address, slot common.Hash) { + s.AddAddressToAccessList(addr) + slots, ok := s.accessList.slots[addr] + if !ok { + slots = map[common.Hash]struct{}{} + s.accessList.slots[addr] = slots + } + slots[slot] = struct{}{} +} + +func (s *nativeStateDB) Prepare(_ params.Rules, sender, coinbase common.Address, dest *common.Address, precompiles []common.Address, txAccesses ethtypes.AccessList) { + s.accessList.reset() + clearNestedHashMaps(s.transientStates) + s.AddAddressToAccessList(sender) + s.AddAddressToAccessList(coinbase) + if dest != nil { + s.AddAddressToAccessList(*dest) + } + for _, addr := range precompiles { + s.AddAddressToAccessList(addr) + } + for _, tuple := range txAccesses { + s.AddAddressToAccessList(tuple.Address) + for _, key := range tuple.StorageKeys { + s.AddSlotToAccessList(tuple.Address, key) + } + } +} + +func (s *nativeStateDB) PointCache() *ethutils.PointCache { + return nil +} + +func (s *nativeStateDB) Snapshot() int { + id := len(s.snapshots) + s.snapshots = append(s.snapshots, nativeSnapshot{ + journalLen: len(s.journal), + refund: s.refund, + logsLen: len(s.logs), + accessList: cloneAccessList(s.accessList), + transientStates: cloneTransientStates(s.transientStates), + finaliseAddrs: cloneAddressSet(s.finaliseAddrs), + preimages: clonePreimages(s.preimages), + journaledAddrs: map[common.Address]struct{}{}, + err: s.err, + }) + return id +} + +func (s *nativeStateDB) RevertToSnapshot(id int) { + if id < 0 || id >= len(s.snapshots) { + panic("invalid state snapshot") + } + snapshot := s.snapshots[id] + for i := len(s.journal) - 1; i >= snapshot.journalLen; i-- { + s.journal[i].revert(s) + } + s.journal = s.journal[:snapshot.journalLen] + s.refund = snapshot.refund + s.logs = s.logs[:snapshot.logsLen] + s.accessList = cloneAccessList(snapshot.accessList) + s.transientStates = cloneTransientStates(snapshot.transientStates) + s.finaliseAddrs = cloneAddressSet(snapshot.finaliseAddrs) + s.preimages = clonePreimages(snapshot.preimages) + s.err = snapshot.err + s.snapshots = s.snapshots[:id] +} + +func (s *nativeStateDB) AddLog(log *ethtypes.Log) { + log.TxHash = s.txHash + log.TxIndex = s.txIndexUint + log.Index = uint(len(s.logs)) + s.logs = append(s.logs, log) +} + +func (s *nativeStateDB) AddPreimage(hash common.Hash, preimage []byte) { + s.preimages[hash] = cloneBytes(preimage) +} + +func (s *nativeStateDB) Witness() *stateless.Witness { + return nil +} + +func (s *nativeStateDB) AccessEvents() *vm.AccessEvents { + return nil +} + +func (s *nativeStateDB) Finalise(bool) { + for addr := range s.finaliseAddrs { + acct := s.account(addr) + if acct.SelfDestructed { + s.recordAccount(addr) + acct.Code = nil + acct.Storage = map[common.Hash]common.Hash{} + acct.Nonce = 0 + acct.SelfDestructed = false + } + if acct.Created { + s.recordAccount(addr) + acct.Created = false + } + } + clear(s.finaliseAddrs) + s.refund = 0 +} + +func (s *nativeStateDB) Error() error { + return s.err +} + +func (s *nativeStateDB) Commit(uint64, bool, bool) (common.Hash, error) { + return common.Hash{}, s.err +} + +func (s *nativeStateDB) SetTxContext(hash common.Hash, index int) { + s.txHash = hash + s.txIndex = index +} + +func (s *nativeStateDB) setTxContext(hash common.Hash, index int, indexUint uint) { + s.txHash = hash + s.txIndex = index + s.txIndexUint = indexUint +} + +func (s *nativeStateDB) Copy() vm.StateDB { + cp := &nativeStateDB{ + source: s.source, + accounts: cloneAccounts(s.accounts), + base: cloneAccounts(s.base), + refund: s.refund, + logs: append([]*ethtypes.Log(nil), s.logs...), + preimages: clonePreimages(s.preimages), + accessList: cloneAccessList(s.accessList), + transientStates: cloneTransientStates(s.transientStates), + finaliseAddrs: cloneAddressSet(s.finaliseAddrs), + journal: cloneJournal(s.journal), + snapshots: cloneSnapshots(s.snapshots), + readSet: cloneAccessSet(s.readSet), + writeSet: cloneAccessSet(s.writeSet), + txHash: s.txHash, + txIndex: s.txIndex, + txIndexUint: s.txIndexUint, + err: s.err, + evm: s.evm, + } + return cp +} + +func (s *nativeStateDB) IntermediateRoot(bool) common.Hash { + return common.Hash{} +} + +func (s *nativeStateDB) GetLogs(common.Hash, uint64, common.Hash) []*ethtypes.Log { + return s.Logs() +} + +func (s *nativeStateDB) TxIndex() int { + return s.txIndex +} + +func (s *nativeStateDB) Preimages() map[common.Hash][]byte { + return clonePreimages(s.preimages) +} + +func (s *nativeStateDB) Logs() []*ethtypes.Log { + return append([]*ethtypes.Log(nil), s.logs...) +} + +func (s *nativeStateDB) SetEVM(evm *vm.EVM) { + s.evm = evm +} + +func (s *nativeStateDB) enableAccessTracking() { + if s.readSet == nil { + s.readSet = map[stateAccessKey]struct{}{} + } else { + clear(s.readSet) + } + if s.writeSet == nil { + s.writeSet = map[stateAccessKey]struct{}{} + } else { + clear(s.writeSet) + } +} + +func (s *nativeStateDB) accessSets() (map[stateAccessKey]struct{}, map[stateAccessKey]struct{}) { + return cloneAccessSet(s.readSet), cloneAccessSet(s.writeSet) +} + +func (s *nativeStateDB) markRead(key stateAccessKey) { + if s.readSet != nil { + s.readSet[key] = struct{}{} + } +} + +func (s *nativeStateDB) markWrite(key stateAccessKey) { + if s.writeSet != nil { + s.writeSet[key] = struct{}{} + } +} + +func (s *nativeStateDB) recordAccount(addr common.Address) { + if len(s.snapshots) == 0 { + return + } + snapshot := &s.snapshots[len(s.snapshots)-1] + if _, ok := snapshot.journaledAddrs[addr]; ok { + return + } + snapshot.journaledAddrs[addr] = struct{}{} + s.journal = append(s.journal, nativeJournalEntry{ + kind: nativeJournalAccount, + address: addr, + account: s.account(addr).clone(), + }) +} + +func (e nativeJournalEntry) revert(s *nativeStateDB) { + switch e.kind { + case nativeJournalAccount: + s.accounts[e.address] = e.account.clone() + default: + panic("unknown native state journal entry") + } +} + +func (s *nativeStateDB) markForFinalise(addr common.Address) { + s.finaliseAddrs[addr] = struct{}{} +} + +func (s *nativeStateDB) clearSnapshots() { + clear(s.journal) + s.journal = s.journal[:0] + clear(s.snapshots) + s.snapshots = s.snapshots[:0] +} + +func (s *nativeStateDB) 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 { + if acct, ok := s.accounts[addr]; ok { + return acct + } + base, ok := s.base[addr] + if !ok { + base = s.loadAccount(addr) + s.base[addr] = base.clone() + } + s.accounts[addr] = base.clone() + return s.accounts[addr] +} + +func (s *nativeStateDB) baseAccount(addr common.Address) *nativeAccount { + if acct, ok := s.base[addr]; ok { + return acct + } + acct := s.loadAccount(addr) + s.base[addr] = acct.clone() + return s.base[addr] +} + +func (s *nativeStateDB) ensureStorage(addr common.Address, key common.Hash) { + base := s.baseAccount(addr) + 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 (al *accessList) reset() { + if al.addresses == nil { + al.addresses = map[common.Address]struct{}{} + } else { + clear(al.addresses) + } + if al.slots == nil { + al.slots = map[common.Address]map[common.Hash]struct{}{} + return + } + for _, slots := range al.slots { + clear(slots) + } + clear(al.slots) +} + +func cloneAccessList(al accessList) accessList { + cp := newAccessList() + for addr := range al.addresses { + cp.addresses[addr] = struct{}{} + } + for addr, slots := range al.slots { + cp.slots[addr] = map[common.Hash]struct{}{} + for slot := range slots { + cp.slots[addr][slot] = struct{}{} + } + } + return cp +} + +func cloneAccounts(accounts map[common.Address]*nativeAccount) map[common.Address]*nativeAccount { + cp := make(map[common.Address]*nativeAccount, len(accounts)) + for addr, acct := range accounts { + cp[addr] = acct.clone() + } + return cp +} + +func cloneTransientStates(states map[common.Address]map[common.Hash]common.Hash) map[common.Address]map[common.Hash]common.Hash { + cp := make(map[common.Address]map[common.Hash]common.Hash, len(states)) + for addr, slots := range states { + cp[addr] = map[common.Hash]common.Hash{} + for key, value := range slots { + cp[addr][key] = value + } + } + return cp +} + +func 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 { + cp[hash] = cloneBytes(preimage) + } + 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 { + cp[i] = nativeJournalEntry{ + kind: entry.kind, + address: entry.address, + account: entry.account.clone(), + } + } + return cp +} + +func cloneSnapshots(snapshots []nativeSnapshot) []nativeSnapshot { + cp := make([]nativeSnapshot, len(snapshots)) + for i, snapshot := range snapshots { + cp[i] = nativeSnapshot{ + journalLen: snapshot.journalLen, + refund: snapshot.refund, + logsLen: snapshot.logsLen, + accessList: cloneAccessList(snapshot.accessList), + transientStates: cloneTransientStates(snapshot.transientStates), + finaliseAddrs: cloneAddressSet(snapshot.finaliseAddrs), + preimages: clonePreimages(snapshot.preimages), + journaledAddrs: cloneAddressSet(snapshot.journaledAddrs), + 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 +} diff --git a/giga/evmonly/types.go b/giga/evmonly/types.go new file mode 100644 index 0000000000..6b017f87a9 --- /dev/null +++ b/giga/evmonly/types.go @@ -0,0 +1,165 @@ +package evmonly + +import ( + "context" + "math/big" + + "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. +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 + 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 { + Context BlockContext + 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 + 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 + ValidatorUpdates []ValidatorUpdate + Txs []TxResult + Receipts ethtypes.Receipts + GasUsed uint64 + OCCStats OCCStats + + lease *blockResultLease +} + +// Release returns a pooled BlockResult to its executor-owned pool. It is a +// no-op for results that were not allocated from a pool. +func (r *BlockResult) Release() { + if r == nil || r.lease == nil { + return + } + lease := r.lease + r.lease = nil + lease.release() +} + +type ValidatorUpdate = precompiles.ValidatorUpdate + +// OCCStats reports optimistic concurrency control behavior for a block. +type OCCStats struct { + Attempted bool + Fallback bool + FallbackReason string + ConflictCount uint64 + ConflictSamples []OCCConflictCount +} + +// OCCConflictCount aggregates conflicts by the access key that forced OCC to +// fall back to sequential execution. +type OCCConflictCount struct { + Access string + Kind string + Address common.Address + Slot common.Hash + Count uint64 +} + +// StateChangeSet is the deterministic EVM-native state output for a block. +// Values are post-block values, not deltas. +type StateChangeSet struct { + Balances []BalanceChange + Nonces []NonceChange + Code []CodeChange + 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 +}