From 3211ddc70c7f58d327270193f38b06043861a6b9 Mon Sep 17 00:00:00 2001 From: evgeniy-scherbina Date: Fri, 3 May 2024 12:22:15 -0400 Subject: [PATCH] Initialize precompiles --- x/evm/genesis.go | 5 + x/evm/keeper/precompile.go | 176 ++++++++++++++++ x/evm/keeper/precompile_test.go | 348 ++++++++++++++++++++++++++++++++ 3 files changed, 529 insertions(+) create mode 100644 x/evm/keeper/precompile.go create mode 100644 x/evm/keeper/precompile_test.go diff --git a/x/evm/genesis.go b/x/evm/genesis.go index 992d53c9fd..f1fde3f3d4 100644 --- a/x/evm/genesis.go +++ b/x/evm/genesis.go @@ -48,6 +48,11 @@ func InitGenesis( panic(err) } + err = k.SyncEnabledPrecompiles(ctx, keeper.HexToAddresses(data.Params.GetEnabledPrecompiles())) + if err != nil { + panic(fmt.Errorf("can't sync enabled precompiles: %v", err)) + } + err = k.SetParams(ctx, data.Params) if err != nil { panic(fmt.Errorf("error setting params %s", err)) diff --git a/x/evm/keeper/precompile.go b/x/evm/keeper/precompile.go new file mode 100644 index 0000000000..b63c4650e4 --- /dev/null +++ b/x/evm/keeper/precompile.go @@ -0,0 +1,176 @@ +package keeper + +import ( + "bytes" + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/common" + + "github.com/evmos/ethermint/x/evm/statedb" +) + +const PrecompileNonce uint64 = 1 + +var PrecompileCode = []byte{0x1} + +type StateDB interface { + GetNonce(addr common.Address) uint64 + GetCode(addr common.Address) []byte + SetNonce(common.Address, uint64) + SetCode(common.Address, []byte) +} + +// InitializationConfig contains lists of contracts which has to be validated, initialized and uninitialized correspondingly. +type InitializationConfig struct { + ValidateInitialized []common.Address + ValidateUninitialized []common.Address + Initialize []common.Address + Uninitialize []common.Address +} + +// SyncEnabledPrecompiles is a keeper wrapper over the SyncEnabledPrecompiles function, which does most of the work. +func (k *Keeper) SyncEnabledPrecompiles(ctx sdk.Context, enabledPrecompiles []common.Address) error { + txConfig := statedb.NewEmptyTxConfig(common.BytesToHash(ctx.HeaderHash().Bytes())) + stateDB := statedb.New(ctx, k, txConfig) + + oldParams := k.GetParams(ctx) + + err := SyncEnabledPrecompiles(stateDB, HexToAddresses(oldParams.EnabledPrecompiles), enabledPrecompiles) + if err != nil { + return err + } + + if err := stateDB.Commit(); err != nil { + return err + } + + return nil +} + +// SyncEnabledPrecompiles takes enabled precompiles from old state and new state and performs following steps: +// - determines a list of contracts that must be validated and validates their state +// - determines a list of contracts that must be initialized and initializes them +// - determines a list of contracts that must be uninitialized and uninitializes them +func SyncEnabledPrecompiles(stateDB StateDB, old []common.Address, new []common.Address) error { + cfg := DetermineInitializationConfig(old, new) + return ApplyInitializationConfig(stateDB, cfg) +} + +// DetermineInitializationConfig takes enabled precompiles from old state and new state and determines lists of contracts +// which has to be validated, initialized and uninitialized correspondingly. +func DetermineInitializationConfig(old []common.Address, new []common.Address) *InitializationConfig { + return &InitializationConfig{ + ValidateInitialized: old, + ValidateUninitialized: SetDifference(new, old), + Initialize: SetDifference(new, old), + Uninitialize: SetDifference(old, new), + } +} + +// ApplyInitializationConfig performs precompiles initialization based on InitializationConfig. +func ApplyInitializationConfig(stateDB StateDB, cfg *InitializationConfig) error { + if err := ValidatePrecompilesInitialized(stateDB, cfg.ValidateInitialized); err != nil { + return err + } + if err := ValidatePrecompilesUninitialized(stateDB, cfg.ValidateUninitialized); err != nil { + return err + } + + InitializePrecompiles(stateDB, cfg.Initialize) + UninitializePrecompiles(stateDB, cfg.Uninitialize) + + return nil +} + +// ValidatePrecompilesInitialized validates that precompiles at specified addresses are initialized. +func ValidatePrecompilesInitialized(stateDB StateDB, addrs []common.Address) error { + for _, addr := range addrs { + nonce := stateDB.GetNonce(addr) + code := stateDB.GetCode(addr) + + ok := nonce == PrecompileNonce && bytes.Equal(code, PrecompileCode) + if !ok { + return fmt.Errorf("precompile %v is not initialized, nonce: %v, code: %v", addr, nonce, code) + } + } + + return nil +} + +// ValidatePrecompilesUninitialized validates that precompiles at specified addresses are uninitialized. +func ValidatePrecompilesUninitialized(stateDB StateDB, addrs []common.Address) error { + for _, addr := range addrs { + nonce := stateDB.GetNonce(addr) + code := stateDB.GetCode(addr) + + ok := nonce == 0 && bytes.Equal(code, nil) + if !ok { + return fmt.Errorf("precompile %v is initialized, nonce: %v, code: %v", addr, nonce, code) + } + } + + return nil +} + +// InitializePrecompiles initializes list of precompiles at specified addresses. +// Initialization of precompile sets non-zero nonce and non-empty code at specified address to resemble behavior of +// regular smart contract. +func InitializePrecompiles(stateDB StateDB, addrs []common.Address) { + for _, addr := range addrs { + // Set the nonce of the precompile's address (as is done when a contract is created) to ensure + // that it is marked as non-empty and will not be cleaned up when the statedb is finalized. + stateDB.SetNonce(addr, PrecompileNonce) + // Set the code of the precompile's address to a non-zero length byte slice to ensure that the precompile + // can be called from within Solidity contracts. Solidity adds a check before invoking a contract to ensure + // that it does not attempt to invoke a non-existent contract. + stateDB.SetCode(addr, PrecompileCode) + } +} + +// UninitializePrecompiles uninitializes list of precompiles at specified addresses. +// Uninitialization of precompile sets zero nonce and empty code at specified address. +func UninitializePrecompiles(stateDB StateDB, addrs []common.Address) { + for _, addr := range addrs { + stateDB.SetNonce(addr, 0) + stateDB.SetCode(addr, nil) + } +} + +func HexToAddresses(hexAddrs []string) []common.Address { + addrs := make([]common.Address, len(hexAddrs)) + for i, hexAddr := range hexAddrs { + addrs[i] = common.HexToAddress(hexAddr) + } + + return addrs +} + +func AddressesToHex(addrs []common.Address) []string { + hexAddrs := make([]string, len(addrs)) + for i, addr := range addrs { + hexAddrs[i] = addr.Hex() + } + + return hexAddrs +} + +// SetDifference returns difference between two sets, example can be: +// a : {1, 2, 3} +// b : {1, 3} +// diff: {2} +func SetDifference(a []common.Address, b []common.Address) []common.Address { + bMap := make(map[common.Address]struct{}, len(b)) + for _, elem := range b { + bMap[elem] = struct{}{} + } + + diff := make([]common.Address, 0) + for _, elem := range a { + if _, ok := bMap[elem]; !ok { + diff = append(diff, elem) + } + } + + return diff +} diff --git a/x/evm/keeper/precompile_test.go b/x/evm/keeper/precompile_test.go new file mode 100644 index 0000000000..e8d2012454 --- /dev/null +++ b/x/evm/keeper/precompile_test.go @@ -0,0 +1,348 @@ +package keeper_test + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + + "github.com/evmos/ethermint/x/evm/keeper" +) + +var ( + addr1 = common.HexToAddress("0x1000000000000000000000000000000000000000") + addr2 = common.HexToAddress("0x2000000000000000000000000000000000000000") + addr3 = common.HexToAddress("0x3000000000000000000000000000000000000000") +) + +type account struct { + nonce uint64 + code []byte +} + +func newAccount() *account { + return &account{} +} + +type stateDB struct { + accounts map[common.Address]*account +} + +func newStateDB() *stateDB { + return &stateDB{ + accounts: make(map[common.Address]*account, 0), + } +} + +func (s *stateDB) GetNonce(addr common.Address) uint64 { + account := s.getOrNewAccount(addr) + return account.nonce +} + +func (s *stateDB) GetCode(addr common.Address) []byte { + account := s.getOrNewAccount(addr) + return account.code +} + +func (s *stateDB) SetNonce(addr common.Address, nonce uint64) { + account := s.getOrNewAccount(addr) + account.nonce = nonce +} + +func (s *stateDB) SetCode(addr common.Address, code []byte) { + account := s.getOrNewAccount(addr) + account.code = code +} + +func (s *stateDB) getOrNewAccount(addr common.Address) *account { + _, ok := s.accounts[addr] + if !ok { + s.accounts[addr] = newAccount() + } + + return s.accounts[addr] +} + +// TestSyncEnabledPrecompiles is built using such approach: +// test case #0 - performs S0 -> S1 state transition +// test case #1 - performs S1 -> S2 state transition +// test case #n - performs Sn -> Sn+1 state transition +// it means order of test cases matters +func (suite *KeeperTestSuite) TestSyncEnabledPrecompiles() { + testCases := []struct { + name string + // enabled precompiles from old state + old []common.Address + // enabled precompiles from new state + new []common.Address + // precompiles which must be uninitialized after corresponding test case + uninitialized []common.Address + }{ + { + name: "enable addr1 and addr2", + old: []common.Address{}, + new: []common.Address{addr1, addr2}, + uninitialized: []common.Address{addr3}, + }, + { + name: "enable addr3, and disable the rest", + old: []common.Address{addr1, addr2}, + new: []common.Address{addr3}, + uninitialized: []common.Address{addr1, addr2}, + }, + { + name: "no changes", + old: []common.Address{addr3}, + new: []common.Address{addr3}, + uninitialized: []common.Address{addr1, addr2}, + }, + { + name: "enable all precompiles", + old: []common.Address{addr3}, + new: []common.Address{addr1, addr2, addr3}, + uninitialized: []common.Address{}, + }, + { + name: "disable all precompiles", + old: []common.Address{addr1, addr2, addr3}, + new: []common.Address{}, + uninitialized: []common.Address{addr1, addr2, addr3}, + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + err := suite.app.EvmKeeper.SyncEnabledPrecompiles(suite.ctx, tc.new) + suite.Require().NoError(err) + + err = keeper.ValidatePrecompilesInitialized(suite.StateDB(), tc.new) + suite.Require().NoError(err) + + err = keeper.ValidatePrecompilesUninitialized(suite.StateDB(), tc.uninitialized) + suite.Require().NoError(err) + + params := suite.app.EvmKeeper.GetParams(suite.ctx) + params.EnabledPrecompiles = keeper.AddressesToHex(tc.new) + err = suite.app.EvmKeeper.SetParams(suite.ctx, params) + suite.Require().NoError(err) + }) + } +} + +// TestSyncEnabledPrecompiles is built using such approach: +// test case #0 - performs S0 -> S1 state transition +// test case #1 - performs S1 -> S2 state transition +// test case #n - performs Sn -> Sn+1 state transition +// it means order of test cases matters +// stateDB is reused across all test-cases +func TestSyncEnabledPrecompiles(t *testing.T) { + stateDB := newStateDB() + + testCases := []struct { + name string + old []common.Address + new []common.Address + uninitialized []common.Address + }{ + { + name: "enable addr1 and addr2", + old: []common.Address{}, + new: []common.Address{addr1, addr2}, + uninitialized: []common.Address{addr3}, + }, + { + name: "enable addr3, and disable the rest", + old: []common.Address{addr1, addr2}, + new: []common.Address{addr3}, + uninitialized: []common.Address{addr1, addr2}, + }, + { + name: "no changes", + old: []common.Address{addr3}, + new: []common.Address{addr3}, + uninitialized: []common.Address{addr1, addr2}, + }, + { + name: "enable all precompiles", + old: []common.Address{addr3}, + new: []common.Address{addr1, addr2, addr3}, + uninitialized: []common.Address{}, + }, + { + name: "disable all precompiles", + old: []common.Address{addr1, addr2, addr3}, + new: []common.Address{}, + uninitialized: []common.Address{addr1, addr2, addr3}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := keeper.SyncEnabledPrecompiles(stateDB, tc.old, tc.new) + require.NoError(t, err) + + err = keeper.ValidatePrecompilesInitialized(stateDB, tc.new) + require.NoError(t, err) + + err = keeper.ValidatePrecompilesUninitialized(stateDB, tc.uninitialized) + require.NoError(t, err) + }) + } +} + +func TestDetermineInitializationConfig(t *testing.T) { + testCases := []struct { + name string + old []common.Address + new []common.Address + cfg *keeper.InitializationConfig + }{ + { + name: "enable addr1 and addr2", + old: []common.Address{}, + new: []common.Address{addr1, addr2}, + cfg: &keeper.InitializationConfig{ + ValidateInitialized: []common.Address{}, + ValidateUninitialized: []common.Address{addr1, addr2}, + Initialize: []common.Address{addr1, addr2}, + Uninitialize: []common.Address{}, + }, + }, + { + name: "enable addr3, and disable the rest", + old: []common.Address{addr1, addr2}, + new: []common.Address{addr3}, + cfg: &keeper.InitializationConfig{ + ValidateInitialized: []common.Address{addr1, addr2}, + ValidateUninitialized: []common.Address{addr3}, + Initialize: []common.Address{addr3}, + Uninitialize: []common.Address{addr1, addr2}, + }, + }, + { + name: "no changes", + old: []common.Address{addr3}, + new: []common.Address{addr3}, + cfg: &keeper.InitializationConfig{ + ValidateInitialized: []common.Address{addr3}, + ValidateUninitialized: []common.Address{}, + Initialize: []common.Address{}, + Uninitialize: []common.Address{}, + }, + }, + { + name: "enable all precompiles", + old: []common.Address{addr3}, + new: []common.Address{addr1, addr2, addr3}, + cfg: &keeper.InitializationConfig{ + ValidateInitialized: []common.Address{addr3}, + ValidateUninitialized: []common.Address{addr1, addr2}, + Initialize: []common.Address{addr1, addr2}, + Uninitialize: []common.Address{}, + }, + }, + { + name: "disable all precompiles", + old: []common.Address{addr1, addr2, addr3}, + new: []common.Address{}, + cfg: &keeper.InitializationConfig{ + ValidateInitialized: []common.Address{addr1, addr2, addr3}, + ValidateUninitialized: []common.Address{}, + Initialize: []common.Address{}, + Uninitialize: []common.Address{addr1, addr2, addr3}, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + cfg := keeper.DetermineInitializationConfig(tc.old, tc.new) + require.Equal(t, tc.cfg, cfg) + }) + } +} + +func TestSyncEnabledPrecompilesHelpers(t *testing.T) { + t.Run("initialize precompiles", func(t *testing.T) { + stateDB := newStateDB() + + require.Equal(t, uint64(0), stateDB.GetNonce(addr1)) + require.Equal(t, []byte(nil), stateDB.GetCode(addr1)) + + keeper.InitializePrecompiles(stateDB, []common.Address{addr1}) + + require.Equal(t, keeper.PrecompileNonce, stateDB.GetNonce(addr1)) + require.Equal(t, keeper.PrecompileCode, stateDB.GetCode(addr1)) + }) + + t.Run("uninitialize precompiles", func(t *testing.T) { + stateDB := newStateDB() + + keeper.InitializePrecompiles(stateDB, []common.Address{addr1}) + require.Equal(t, keeper.PrecompileNonce, stateDB.GetNonce(addr1)) + require.Equal(t, keeper.PrecompileCode, stateDB.GetCode(addr1)) + + keeper.UninitializePrecompiles(stateDB, []common.Address{addr1}) + require.Equal(t, uint64(0), stateDB.GetNonce(addr1)) + require.Equal(t, []byte(nil), stateDB.GetCode(addr1)) + }) + + t.Run("validate precompiles initialized", func(t *testing.T) { + stateDB := newStateDB() + + err := keeper.ValidatePrecompilesInitialized(stateDB, []common.Address{addr1}) + require.ErrorContains(t, err, "is not initialized") + + keeper.InitializePrecompiles(stateDB, []common.Address{addr1}) + + err = keeper.ValidatePrecompilesInitialized(stateDB, []common.Address{addr1}) + require.NoError(t, err) + }) + + t.Run("validate precompiles uninitialized", func(t *testing.T) { + stateDB := newStateDB() + + err := keeper.ValidatePrecompilesUninitialized(stateDB, []common.Address{addr1}) + require.NoError(t, err) + + keeper.InitializePrecompiles(stateDB, []common.Address{addr1}) + + err = keeper.ValidatePrecompilesUninitialized(stateDB, []common.Address{addr1}) + require.ErrorContains(t, err, "is initialized") + }) +} + +func TestSetDifference(t *testing.T) { + testCases := []struct { + name string + a []common.Address + b []common.Address + diff []common.Address + }{ + { + name: "A and B intersect, but diff isn't empty", + a: []common.Address{addr1, addr2}, + b: []common.Address{addr1, addr3}, + diff: []common.Address{addr2}, + }, + { + name: "A and B don't intersect, diff isn't empty", + a: []common.Address{addr1}, + b: []common.Address{addr2, addr3}, + diff: []common.Address{addr1}, + }, + { + name: "A is a subset of B, diff is empty", + a: []common.Address{addr1, addr2}, + b: []common.Address{addr1, addr2, addr3}, + diff: []common.Address{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + diff := keeper.SetDifference(tc.a, tc.b) + require.Equal(t, tc.diff, diff) + }) + } +}