evm: fix non-determinism (#352)
* evm: fix non-determinism * fixes * typo * fix tests * use Storage slice * more fixes * lint * merge development * additional tests
This commit is contained in:
parent
42fc796595
commit
90f39390bc
@ -768,7 +768,7 @@ func TestEth_ExportAccount(t *testing.T) {
|
|||||||
require.Equal(t, "0x1122334455667788990011223344556677889900", account.Address.Hex())
|
require.Equal(t, "0x1122334455667788990011223344556677889900", account.Address.Hex())
|
||||||
require.Equal(t, big.NewInt(0), account.Balance)
|
require.Equal(t, big.NewInt(0), account.Balance)
|
||||||
require.Equal(t, hexutil.Bytes(nil), account.Code)
|
require.Equal(t, hexutil.Bytes(nil), account.Code)
|
||||||
require.Equal(t, []types.GenesisStorage(nil), account.Storage)
|
require.Equal(t, types.Storage(nil), account.Storage)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEth_ExportAccount_WithStorage(t *testing.T) {
|
func TestEth_ExportAccount_WithStorage(t *testing.T) {
|
||||||
@ -812,7 +812,7 @@ func TestEth_ExportAccount_WithStorage(t *testing.T) {
|
|||||||
require.Equal(t, addr, strings.ToLower(account.Address.Hex()))
|
require.Equal(t, addr, strings.ToLower(account.Address.Hex()))
|
||||||
require.Equal(t, big.NewInt(0), account.Balance)
|
require.Equal(t, big.NewInt(0), account.Balance)
|
||||||
require.Equal(t, hexutil.Bytes(bytecode), account.Code)
|
require.Equal(t, hexutil.Bytes(bytecode), account.Code)
|
||||||
require.NotEqual(t, []types.GenesisStorage(nil), account.Storage)
|
require.NotEqual(t, types.Storage(nil), account.Storage)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEth_GetBlockByNumber(t *testing.T) {
|
func TestEth_GetBlockByNumber(t *testing.T) {
|
||||||
|
@ -1,40 +1,12 @@
|
|||||||
package types
|
package types
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
ethcmn "github.com/ethereum/go-ethereum/common"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// Code & Storage
|
// Code
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
type (
|
// Code is account Code type alias
|
||||||
// Code is account Code type alias
|
type Code []byte
|
||||||
Code []byte
|
|
||||||
// Storage is account storage type alias
|
|
||||||
Storage map[ethcmn.Hash]ethcmn.Hash
|
|
||||||
)
|
|
||||||
|
|
||||||
func (c Code) String() string {
|
func (c Code) String() string {
|
||||||
return string(c)
|
return string(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Storage) String() (str string) {
|
|
||||||
for key, value := range c {
|
|
||||||
str += fmt.Sprintf("%X : %X\n", key, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy returns a copy of storage.
|
|
||||||
func (c Storage) Copy() Storage {
|
|
||||||
cpy := make(Storage)
|
|
||||||
for key, value := range c {
|
|
||||||
cpy[key] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
return cpy
|
|
||||||
}
|
|
||||||
|
@ -59,9 +59,9 @@ func ExportGenesis(ctx sdk.Context, k Keeper, ak types.AccountKeeper) GenesisSta
|
|||||||
|
|
||||||
addr := common.BytesToAddress(ethAccount.GetAddress().Bytes())
|
addr := common.BytesToAddress(ethAccount.GetAddress().Bytes())
|
||||||
|
|
||||||
var storage []types.GenesisStorage
|
var storage types.Storage
|
||||||
err = k.CommitStateDB.ForEachStorage(addr, func(key, value common.Hash) bool {
|
err = k.CommitStateDB.ForEachStorage(addr, func(key, value common.Hash) bool {
|
||||||
storage = append(storage, types.NewGenesisStorage(key, value))
|
storage = append(storage, types.NewState(key, value))
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -44,10 +44,10 @@ func (suite *EvmTestSuite) TestContractExportImport() {
|
|||||||
suite.T().Logf("contract addr 0x%x", address)
|
suite.T().Logf("contract addr 0x%x", address)
|
||||||
|
|
||||||
// clear keeper code and re-initialize
|
// clear keeper code and re-initialize
|
||||||
suite.app.EvmKeeper.CommitStateDB.WithContext(suite.ctx).SetCode(address, nil)
|
suite.app.EvmKeeper.SetCode(suite.ctx, address, nil)
|
||||||
_ = evm.InitGenesis(suite.ctx, suite.app.EvmKeeper, genState)
|
_ = evm.InitGenesis(suite.ctx, suite.app.EvmKeeper, genState)
|
||||||
|
|
||||||
resCode := suite.app.EvmKeeper.CommitStateDB.WithContext(suite.ctx).GetCode(address)
|
resCode := suite.app.EvmKeeper.GetCode(suite.ctx, address)
|
||||||
suite.Require().Equal(deployedEnsFactoryCode, resCode)
|
suite.Require().Equal(deployedEnsFactoryCode, resCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -201,9 +201,9 @@ func queryAccount(ctx sdk.Context, path []string, keeper Keeper) ([]byte, error)
|
|||||||
func queryExportAccount(ctx sdk.Context, path []string, keeper Keeper) ([]byte, error) {
|
func queryExportAccount(ctx sdk.Context, path []string, keeper Keeper) ([]byte, error) {
|
||||||
addr := ethcmn.HexToAddress(path[1])
|
addr := ethcmn.HexToAddress(path[1])
|
||||||
|
|
||||||
var storage []types.GenesisStorage
|
var storage types.Storage
|
||||||
err := keeper.CommitStateDB.ForEachStorage(addr, func(key, value ethcmn.Hash) bool {
|
err := keeper.CommitStateDB.ForEachStorage(addr, func(key, value ethcmn.Hash) bool {
|
||||||
storage = append(storage, types.NewGenesisStorage(key, value))
|
storage = append(storage, types.NewState(key, value))
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
12
x/evm/types/errors.go
Normal file
12
x/evm/types/errors.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NOTE: We can't use 1 since that error code is reserved for internal errors.
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrInvalidState returns an error resulting from an invalid Storage State.
|
||||||
|
ErrInvalidState = sdkerrors.Register(ModuleName, 2, "invalid storage state")
|
||||||
|
)
|
@ -17,21 +17,14 @@ type (
|
|||||||
TxsLogs []TransactionLogs `json:"txs_logs"`
|
TxsLogs []TransactionLogs `json:"txs_logs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenesisStorage represents the GenesisAccount Storage map as single key value
|
|
||||||
// pairs. This is to prevent non determinism at genesis initialization or export.
|
|
||||||
GenesisStorage struct {
|
|
||||||
Key ethcmn.Hash `json:"key"`
|
|
||||||
Value ethcmn.Hash `json:"value"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// GenesisAccount defines an account to be initialized in the genesis state.
|
// GenesisAccount defines an account to be initialized in the genesis state.
|
||||||
// Its main difference between with Geth's GenesisAccount is that it uses a custom
|
// Its main difference between with Geth's GenesisAccount is that it uses a custom
|
||||||
// storage type and that it doesn't contain the private key field.
|
// storage type and that it doesn't contain the private key field.
|
||||||
GenesisAccount struct {
|
GenesisAccount struct {
|
||||||
Address ethcmn.Address `json:"address"`
|
Address ethcmn.Address `json:"address"`
|
||||||
Balance *big.Int `json:"balance"`
|
Balance *big.Int `json:"balance"`
|
||||||
Code hexutil.Bytes `json:"code,omitempty"`
|
Code hexutil.Bytes `json:"code,omitempty"`
|
||||||
Storage []GenesisStorage `json:"storage,omitempty"`
|
Storage Storage `json:"storage,omitempty"`
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -50,26 +43,7 @@ func (ga GenesisAccount) Validate() error {
|
|||||||
return errors.New("code bytes cannot be empty")
|
return errors.New("code bytes cannot be empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
seenStorage := make(map[string]bool)
|
return ga.Storage.Validate()
|
||||||
for i, state := range ga.Storage {
|
|
||||||
if seenStorage[state.Key.String()] {
|
|
||||||
return fmt.Errorf("duplicate state key %d", i)
|
|
||||||
}
|
|
||||||
if bytes.Equal(state.Key.Bytes(), ethcmn.Hash{}.Bytes()) {
|
|
||||||
return fmt.Errorf("state %d key hash cannot be empty", i)
|
|
||||||
}
|
|
||||||
// NOTE: state value can be empty
|
|
||||||
seenStorage[state.Key.String()] = true
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewGenesisStorage creates a new GenesisStorage instance
|
|
||||||
func NewGenesisStorage(key, value ethcmn.Hash) GenesisStorage {
|
|
||||||
return GenesisStorage{
|
|
||||||
Key: key,
|
|
||||||
Value: value,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultGenesisState sets default evm genesis state with empty accounts.
|
// DefaultGenesisState sets default evm genesis state with empty accounts.
|
||||||
|
@ -24,8 +24,8 @@ func TestValidateGenesisAccount(t *testing.T) {
|
|||||||
Address: ethcmn.BytesToAddress([]byte{1, 2, 3, 4, 5}),
|
Address: ethcmn.BytesToAddress([]byte{1, 2, 3, 4, 5}),
|
||||||
Balance: big.NewInt(1),
|
Balance: big.NewInt(1),
|
||||||
Code: []byte{1, 2, 3},
|
Code: []byte{1, 2, 3},
|
||||||
Storage: []GenesisStorage{
|
Storage: Storage{
|
||||||
NewGenesisStorage(ethcmn.BytesToHash([]byte{1, 2, 3}), ethcmn.BytesToHash([]byte{1, 2, 3})),
|
NewState(ethcmn.BytesToHash([]byte{1, 2, 3}), ethcmn.BytesToHash([]byte{1, 2, 3})),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
@ -63,31 +63,6 @@ func TestValidateGenesisAccount(t *testing.T) {
|
|||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"empty storage key bytes",
|
|
||||||
GenesisAccount{
|
|
||||||
Address: ethcmn.BytesToAddress([]byte{1, 2, 3, 4, 5}),
|
|
||||||
Balance: big.NewInt(1),
|
|
||||||
Code: []byte{1, 2, 3},
|
|
||||||
Storage: []GenesisStorage{
|
|
||||||
{Key: ethcmn.Hash{}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"duplicated storage key",
|
|
||||||
GenesisAccount{
|
|
||||||
Address: ethcmn.BytesToAddress([]byte{1, 2, 3, 4, 5}),
|
|
||||||
Balance: big.NewInt(1),
|
|
||||||
Code: []byte{1, 2, 3},
|
|
||||||
Storage: []GenesisStorage{
|
|
||||||
{Key: ethcmn.BytesToHash([]byte{1, 2, 3})},
|
|
||||||
{Key: ethcmn.BytesToHash([]byte{1, 2, 3})},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
@ -124,7 +99,7 @@ func TestValidateGenesis(t *testing.T) {
|
|||||||
Address: ethcmn.BytesToAddress([]byte{1, 2, 3, 4, 5}),
|
Address: ethcmn.BytesToAddress([]byte{1, 2, 3, 4, 5}),
|
||||||
Balance: big.NewInt(1),
|
Balance: big.NewInt(1),
|
||||||
Code: []byte{1, 2, 3},
|
Code: []byte{1, 2, 3},
|
||||||
Storage: []GenesisStorage{
|
Storage: Storage{
|
||||||
{Key: ethcmn.BytesToHash([]byte{1, 2, 3})},
|
{Key: ethcmn.BytesToHash([]byte{1, 2, 3})},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -169,16 +144,16 @@ func TestValidateGenesis(t *testing.T) {
|
|||||||
Address: ethcmn.BytesToAddress([]byte{1, 2, 3, 4, 5}),
|
Address: ethcmn.BytesToAddress([]byte{1, 2, 3, 4, 5}),
|
||||||
Balance: big.NewInt(1),
|
Balance: big.NewInt(1),
|
||||||
Code: []byte{1, 2, 3},
|
Code: []byte{1, 2, 3},
|
||||||
Storage: []GenesisStorage{
|
Storage: Storage{
|
||||||
NewGenesisStorage(ethcmn.BytesToHash([]byte{1, 2, 3}), ethcmn.BytesToHash([]byte{1, 2, 3})),
|
NewState(ethcmn.BytesToHash([]byte{1, 2, 3}), ethcmn.BytesToHash([]byte{1, 2, 3})),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Address: ethcmn.BytesToAddress([]byte{1, 2, 3, 4, 5}),
|
Address: ethcmn.BytesToAddress([]byte{1, 2, 3, 4, 5}),
|
||||||
Balance: big.NewInt(1),
|
Balance: big.NewInt(1),
|
||||||
Code: []byte{1, 2, 3},
|
Code: []byte{1, 2, 3},
|
||||||
Storage: []GenesisStorage{
|
Storage: Storage{
|
||||||
NewGenesisStorage(ethcmn.BytesToHash([]byte{1, 2, 3}), ethcmn.BytesToHash([]byte{1, 2, 3})),
|
NewState(ethcmn.BytesToHash([]byte{1, 2, 3}), ethcmn.BytesToHash([]byte{1, 2, 3})),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -193,7 +168,7 @@ func TestValidateGenesis(t *testing.T) {
|
|||||||
Address: ethcmn.BytesToAddress([]byte{1, 2, 3, 4, 5}),
|
Address: ethcmn.BytesToAddress([]byte{1, 2, 3, 4, 5}),
|
||||||
Balance: big.NewInt(1),
|
Balance: big.NewInt(1),
|
||||||
Code: []byte{1, 2, 3},
|
Code: []byte{1, 2, 3},
|
||||||
Storage: []GenesisStorage{
|
Storage: Storage{
|
||||||
{Key: ethcmn.BytesToHash([]byte{1, 2, 3})},
|
{Key: ethcmn.BytesToHash([]byte{1, 2, 3})},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -243,7 +218,7 @@ func TestValidateGenesis(t *testing.T) {
|
|||||||
Address: ethcmn.BytesToAddress([]byte{1, 2, 3, 4, 5}),
|
Address: ethcmn.BytesToAddress([]byte{1, 2, 3, 4, 5}),
|
||||||
Balance: big.NewInt(1),
|
Balance: big.NewInt(1),
|
||||||
Code: []byte{1, 2, 3},
|
Code: []byte{1, 2, 3},
|
||||||
Storage: []GenesisStorage{
|
Storage: Storage{
|
||||||
{Key: ethcmn.BytesToHash([]byte{1, 2, 3})},
|
{Key: ethcmn.BytesToHash([]byte{1, 2, 3})},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -22,14 +22,24 @@ type journalEntry interface {
|
|||||||
// commit. These are tracked to be able to be reverted in case of an execution
|
// commit. These are tracked to be able to be reverted in case of an execution
|
||||||
// exception or revertal request.
|
// exception or revertal request.
|
||||||
type journal struct {
|
type journal struct {
|
||||||
entries []journalEntry // Current changes tracked by the journal
|
entries []journalEntry // Current changes tracked by the journal
|
||||||
dirties map[ethcmn.Address]int // Dirty accounts and the number of changes
|
dirties []dirty // Dirty accounts and the number of changes
|
||||||
|
addressToJournalIndex map[ethcmn.Address]int // map from address to the index of the dirties slice
|
||||||
|
}
|
||||||
|
|
||||||
|
// dirty represents a single key value pair of the journal dirties, where the
|
||||||
|
// key correspons to the account address and the value to the number of
|
||||||
|
// changes for that account.
|
||||||
|
type dirty struct {
|
||||||
|
address ethcmn.Address
|
||||||
|
changes int
|
||||||
}
|
}
|
||||||
|
|
||||||
// newJournal create a new initialized journal.
|
// newJournal create a new initialized journal.
|
||||||
func newJournal() *journal {
|
func newJournal() *journal {
|
||||||
return &journal{
|
return &journal{
|
||||||
dirties: make(map[ethcmn.Address]int),
|
dirties: []dirty{},
|
||||||
|
addressToJournalIndex: make(map[ethcmn.Address]int),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,7 +47,7 @@ func newJournal() *journal {
|
|||||||
func (j *journal) append(entry journalEntry) {
|
func (j *journal) append(entry journalEntry) {
|
||||||
j.entries = append(j.entries, entry)
|
j.entries = append(j.entries, entry)
|
||||||
if addr := entry.dirtied(); addr != nil {
|
if addr := entry.dirtied(); addr != nil {
|
||||||
j.dirties[*addr]++
|
j.addDirty(*addr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,8 +60,9 @@ func (j *journal) revert(statedb *CommitStateDB, snapshot int) {
|
|||||||
|
|
||||||
// Drop any dirty tracking induced by the change
|
// Drop any dirty tracking induced by the change
|
||||||
if addr := j.entries[i].dirtied(); addr != nil {
|
if addr := j.entries[i].dirtied(); addr != nil {
|
||||||
if j.dirties[*addr]--; j.dirties[*addr] == 0 {
|
j.substractDirty(*addr)
|
||||||
delete(j.dirties, *addr)
|
if j.getDirty(*addr) == 0 {
|
||||||
|
j.deleteDirty(*addr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -62,7 +73,7 @@ func (j *journal) revert(statedb *CommitStateDB, snapshot int) {
|
|||||||
// otherwise suggest it as clean. This method is an ugly hack to handle the RIPEMD
|
// otherwise suggest it as clean. This method is an ugly hack to handle the RIPEMD
|
||||||
// precompile consensus exception.
|
// precompile consensus exception.
|
||||||
func (j *journal) dirty(addr ethcmn.Address) {
|
func (j *journal) dirty(addr ethcmn.Address) {
|
||||||
j.dirties[addr]++
|
j.addDirty(addr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// length returns the current number of entries in the journal.
|
// length returns the current number of entries in the journal.
|
||||||
@ -70,6 +81,56 @@ func (j *journal) length() int {
|
|||||||
return len(j.entries)
|
return len(j.entries)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getDirty returns the dirty count for a given address. If the address is not
|
||||||
|
// found it returns 0.
|
||||||
|
func (j *journal) getDirty(addr ethcmn.Address) int {
|
||||||
|
idx, found := j.addressToJournalIndex[addr]
|
||||||
|
if !found {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return j.dirties[idx].changes
|
||||||
|
}
|
||||||
|
|
||||||
|
// addDirty adds 1 to the dirty count of an address. If the dirty entry is not
|
||||||
|
// found it creates it.
|
||||||
|
func (j *journal) addDirty(addr ethcmn.Address) {
|
||||||
|
idx, found := j.addressToJournalIndex[addr]
|
||||||
|
if !found {
|
||||||
|
j.dirties = append(j.dirties, dirty{address: addr, changes: 0})
|
||||||
|
idx = len(j.dirties) - 1
|
||||||
|
j.addressToJournalIndex[addr] = idx
|
||||||
|
}
|
||||||
|
|
||||||
|
j.dirties[idx].changes++
|
||||||
|
}
|
||||||
|
|
||||||
|
// substractDirty subtracts 1 to the dirty count of an address. It performs a
|
||||||
|
// no-op if the address is not found.
|
||||||
|
func (j *journal) substractDirty(addr ethcmn.Address) {
|
||||||
|
idx, found := j.addressToJournalIndex[addr]
|
||||||
|
if !found {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if j.dirties[idx].changes == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
j.dirties[idx].changes--
|
||||||
|
}
|
||||||
|
|
||||||
|
// deleteDirty deletes a dirty entry from the jounal's dirties slice. If the
|
||||||
|
// entry is not found it performs a no-op.
|
||||||
|
func (j *journal) deleteDirty(addr ethcmn.Address) {
|
||||||
|
idx, found := j.addressToJournalIndex[addr]
|
||||||
|
if !found {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
j.dirties = append(j.dirties[:idx], j.dirties[idx+1:]...)
|
||||||
|
delete(j.addressToJournalIndex, addr)
|
||||||
|
}
|
||||||
|
|
||||||
type (
|
type (
|
||||||
// Changes to the account trie.
|
// Changes to the account trie.
|
||||||
createObjectChange struct {
|
createObjectChange struct {
|
||||||
@ -128,8 +189,14 @@ type (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (ch createObjectChange) revert(s *CommitStateDB) {
|
func (ch createObjectChange) revert(s *CommitStateDB) {
|
||||||
delete(s.stateObjects, *ch.account)
|
|
||||||
delete(s.stateObjectsDirty, *ch.account)
|
delete(s.stateObjectsDirty, *ch.account)
|
||||||
|
idx, exists := s.addressToObjectIndex[*ch.account]
|
||||||
|
if !exists {
|
||||||
|
// perform no-op
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// remove from the slice
|
||||||
|
s.stateObjects = append(s.stateObjects[:idx], s.stateObjects[idx+1:]...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ch createObjectChange) dirtied() *ethcmn.Address {
|
func (ch createObjectChange) dirtied() *ethcmn.Address {
|
||||||
|
@ -228,7 +228,8 @@ func (suite *JournalTestSuite) TestJournal_append_revert() {
|
|||||||
suite.Require().Equal(suite.journal.length(), i+1, tc.name)
|
suite.Require().Equal(suite.journal.length(), i+1, tc.name)
|
||||||
if tc.entry.dirtied() != nil {
|
if tc.entry.dirtied() != nil {
|
||||||
dirtyCount++
|
dirtyCount++
|
||||||
suite.Require().Equal(dirtyCount, suite.journal.dirties[suite.address], tc.name)
|
|
||||||
|
suite.Require().Equal(dirtyCount, suite.journal.getDirty(suite.address), tc.name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -236,18 +237,18 @@ func (suite *JournalTestSuite) TestJournal_append_revert() {
|
|||||||
suite.journal.revert(suite.stateDB, 0)
|
suite.journal.revert(suite.stateDB, 0)
|
||||||
|
|
||||||
// verify the dirty entry has been deleted
|
// verify the dirty entry has been deleted
|
||||||
count, ok := suite.journal.dirties[suite.address]
|
idx, ok := suite.journal.addressToJournalIndex[suite.address]
|
||||||
suite.Require().False(ok)
|
suite.Require().False(ok)
|
||||||
suite.Require().Zero(count)
|
suite.Require().Zero(idx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *JournalTestSuite) TestJournal_dirty() {
|
func (suite *JournalTestSuite) TestJournal_dirty() {
|
||||||
// dirty entry hasn't been set
|
// dirty entry hasn't been set
|
||||||
count, ok := suite.journal.dirties[suite.address]
|
idx, ok := suite.journal.addressToJournalIndex[suite.address]
|
||||||
suite.Require().False(ok)
|
suite.Require().False(ok)
|
||||||
suite.Require().Zero(count)
|
suite.Require().Zero(idx)
|
||||||
|
|
||||||
// update dirty count
|
// update dirty count
|
||||||
suite.journal.dirty(suite.address)
|
suite.journal.dirty(suite.address)
|
||||||
suite.Require().Equal(1, suite.journal.dirties[suite.address])
|
suite.Require().Equal(1, suite.journal.getDirty(suite.address))
|
||||||
}
|
}
|
||||||
|
@ -24,57 +24,63 @@ var (
|
|||||||
emptyCodeHash = ethcrypto.Keccak256(nil)
|
emptyCodeHash = ethcrypto.Keccak256(nil)
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
// StateObject interface for interacting with state object
|
||||||
// StateObject interface for interacting with state object
|
type StateObject interface {
|
||||||
StateObject interface {
|
GetCommittedState(db ethstate.Database, key ethcmn.Hash) ethcmn.Hash
|
||||||
GetCommittedState(db ethstate.Database, key ethcmn.Hash) ethcmn.Hash
|
GetState(db ethstate.Database, key ethcmn.Hash) ethcmn.Hash
|
||||||
GetState(db ethstate.Database, key ethcmn.Hash) ethcmn.Hash
|
SetState(db ethstate.Database, key, value ethcmn.Hash)
|
||||||
SetState(db ethstate.Database, key, value ethcmn.Hash)
|
|
||||||
|
|
||||||
Code(db ethstate.Database) []byte
|
Code(db ethstate.Database) []byte
|
||||||
SetCode(codeHash ethcmn.Hash, code []byte)
|
SetCode(codeHash ethcmn.Hash, code []byte)
|
||||||
CodeHash() []byte
|
CodeHash() []byte
|
||||||
|
|
||||||
AddBalance(amount *big.Int)
|
AddBalance(amount *big.Int)
|
||||||
SubBalance(amount *big.Int)
|
SubBalance(amount *big.Int)
|
||||||
SetBalance(amount *big.Int)
|
SetBalance(amount *big.Int)
|
||||||
|
|
||||||
Balance() *big.Int
|
Balance() *big.Int
|
||||||
ReturnGas(gas *big.Int)
|
ReturnGas(gas *big.Int)
|
||||||
Address() ethcmn.Address
|
Address() ethcmn.Address
|
||||||
|
|
||||||
SetNonce(nonce uint64)
|
SetNonce(nonce uint64)
|
||||||
Nonce() uint64
|
Nonce() uint64
|
||||||
}
|
}
|
||||||
// stateObject represents an Ethereum account which is being modified.
|
|
||||||
|
// stateObject represents an Ethereum account which is being modified.
|
||||||
|
//
|
||||||
|
// The usage pattern is as follows:
|
||||||
|
// First you need to obtain a state object.
|
||||||
|
// Account values can be accessed and modified through the object.
|
||||||
|
// Finally, call CommitTrie to write the modified storage trie into a database.
|
||||||
|
type stateObject struct {
|
||||||
|
code types.Code // contract bytecode, which gets set when code is loaded
|
||||||
|
|
||||||
|
// State objects are used by the consensus core and VM which are
|
||||||
|
// unable to deal with database-level errors. Any error that occurs
|
||||||
|
// during a database read is memoized here and will eventually be returned
|
||||||
|
// by StateDB.Commit.
|
||||||
|
originStorage Storage // Storage cache of original entries to dedup rewrites
|
||||||
|
dirtyStorage Storage // Storage entries that need to be flushed to disk
|
||||||
|
|
||||||
|
// DB error
|
||||||
|
dbErr error
|
||||||
|
stateDB *CommitStateDB
|
||||||
|
account *types.EthAccount
|
||||||
|
balance sdk.Int
|
||||||
|
|
||||||
|
keyToOriginStorageIndex map[ethcmn.Hash]int
|
||||||
|
keyToDirtyStorageIndex map[ethcmn.Hash]int
|
||||||
|
|
||||||
|
address ethcmn.Address
|
||||||
|
|
||||||
|
// cache flags
|
||||||
//
|
//
|
||||||
// The usage pattern is as follows:
|
// When an object is marked suicided it will be delete from the trie during
|
||||||
// First you need to obtain a state object.
|
// the "update" phase of the state transition.
|
||||||
// Account values can be accessed and modified through the object.
|
dirtyCode bool // true if the code was updated
|
||||||
// Finally, call CommitTrie to write the modified storage trie into a database.
|
suicided bool
|
||||||
stateObject struct {
|
deleted bool
|
||||||
code types.Code // contract bytecode, which gets set when code is loaded
|
}
|
||||||
// DB error.
|
|
||||||
// State objects are used by the consensus core and VM which are
|
|
||||||
// unable to deal with database-level errors. Any error that occurs
|
|
||||||
// during a database read is memoized here and will eventually be returned
|
|
||||||
// by StateDB.Commit.
|
|
||||||
dbErr error
|
|
||||||
stateDB *CommitStateDB
|
|
||||||
account *types.EthAccount
|
|
||||||
balance sdk.Int
|
|
||||||
originStorage types.Storage // Storage cache of original entries to dedup rewrites
|
|
||||||
dirtyStorage types.Storage // Storage entries that need to be flushed to disk
|
|
||||||
address ethcmn.Address
|
|
||||||
// cache flags
|
|
||||||
//
|
|
||||||
// When an object is marked suicided it will be delete from the trie during
|
|
||||||
// the "update" phase of the state transition.
|
|
||||||
dirtyCode bool // true if the code was updated
|
|
||||||
suicided bool
|
|
||||||
deleted bool
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func newStateObject(db *CommitStateDB, accProto authexported.Account, balance sdk.Int) *stateObject {
|
func newStateObject(db *CommitStateDB, accProto authexported.Account, balance sdk.Int) *stateObject {
|
||||||
ethermintAccount, ok := accProto.(*types.EthAccount)
|
ethermintAccount, ok := accProto.(*types.EthAccount)
|
||||||
@ -88,12 +94,14 @@ func newStateObject(db *CommitStateDB, accProto authexported.Account, balance sd
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &stateObject{
|
return &stateObject{
|
||||||
stateDB: db,
|
stateDB: db,
|
||||||
account: ethermintAccount,
|
account: ethermintAccount,
|
||||||
balance: balance,
|
balance: balance,
|
||||||
address: ethcmn.BytesToAddress(ethermintAccount.GetAddress().Bytes()),
|
address: ethcmn.BytesToAddress(ethermintAccount.GetAddress().Bytes()),
|
||||||
originStorage: make(types.Storage),
|
originStorage: Storage{},
|
||||||
dirtyStorage: make(types.Storage),
|
dirtyStorage: Storage{},
|
||||||
|
keyToOriginStorageIndex: make(map[ethcmn.Hash]int),
|
||||||
|
keyToDirtyStorageIndex: make(map[ethcmn.Hash]int),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,8 +130,18 @@ func (so *stateObject) SetState(db ethstate.Database, key, value ethcmn.Hash) {
|
|||||||
so.setState(prefixKey, value)
|
so.setState(prefixKey, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// setState sets a state with a prefixed key and value to the dirty storage.
|
||||||
func (so *stateObject) setState(key, value ethcmn.Hash) {
|
func (so *stateObject) setState(key, value ethcmn.Hash) {
|
||||||
so.dirtyStorage[key] = value
|
idx, ok := so.keyToDirtyStorageIndex[key]
|
||||||
|
if ok {
|
||||||
|
so.dirtyStorage[idx].Value = value
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// create new entry
|
||||||
|
so.dirtyStorage = append(so.dirtyStorage, NewState(key, value))
|
||||||
|
idx = len(so.dirtyStorage) - 1
|
||||||
|
so.keyToDirtyStorageIndex[key] = idx
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetCode sets the state object's code.
|
// SetCode sets the state object's code.
|
||||||
@ -224,23 +242,31 @@ func (so *stateObject) commitState() {
|
|||||||
ctx := so.stateDB.ctx
|
ctx := so.stateDB.ctx
|
||||||
store := prefix.NewStore(ctx.KVStore(so.stateDB.storeKey), AddressStoragePrefix(so.Address()))
|
store := prefix.NewStore(ctx.KVStore(so.stateDB.storeKey), AddressStoragePrefix(so.Address()))
|
||||||
|
|
||||||
for key, value := range so.dirtyStorage {
|
for i, state := range so.dirtyStorage {
|
||||||
delete(so.dirtyStorage, key)
|
delete(so.keyToDirtyStorageIndex, state.Key)
|
||||||
|
so.dirtyStorage = append(so.dirtyStorage[:i], so.dirtyStorage[i+1:]...)
|
||||||
|
|
||||||
// skip no-op changes, persist actual changes
|
// skip no-op changes, persist actual changes
|
||||||
if value == so.originStorage[key] {
|
idx, ok := so.keyToOriginStorageIndex[state.Key]
|
||||||
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
so.originStorage[key] = value
|
if state.Value == so.originStorage[idx].Value {
|
||||||
|
|
||||||
// delete empty values
|
|
||||||
if (value == ethcmn.Hash{}) {
|
|
||||||
store.Delete(key.Bytes())
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
store.Set(key.Bytes(), value.Bytes())
|
so.originStorage[idx].Value = state.Value
|
||||||
|
|
||||||
|
// NOTE: key is already prefixed from GetStorageByAddressKey
|
||||||
|
|
||||||
|
// delete empty values from the store
|
||||||
|
if (state.Value == ethcmn.Hash{}) {
|
||||||
|
store.Delete(state.Key.Bytes())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
store.Set(state.Key.Bytes(), state.Value.Bytes())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -312,9 +338,9 @@ func (so *stateObject) GetState(db ethstate.Database, key ethcmn.Hash) ethcmn.Ha
|
|||||||
prefixKey := so.GetStorageByAddressKey(key.Bytes())
|
prefixKey := so.GetStorageByAddressKey(key.Bytes())
|
||||||
|
|
||||||
// if we have a dirty value for this state entry, return it
|
// if we have a dirty value for this state entry, return it
|
||||||
value, dirty := so.dirtyStorage[prefixKey]
|
idx, dirty := so.keyToDirtyStorageIndex[prefixKey]
|
||||||
if dirty {
|
if dirty {
|
||||||
return value
|
return so.dirtyStorage[idx].Value
|
||||||
}
|
}
|
||||||
|
|
||||||
// otherwise return the entry's original value
|
// otherwise return the entry's original value
|
||||||
@ -322,27 +348,35 @@ func (so *stateObject) GetState(db ethstate.Database, key ethcmn.Hash) ethcmn.Ha
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetCommittedState retrieves a value from the committed account storage trie.
|
// GetCommittedState retrieves a value from the committed account storage trie.
|
||||||
// Note, the key will be prefixed with the address of the state object.
|
//
|
||||||
|
// NOTE: the key will be prefixed with the address of the state object.
|
||||||
func (so *stateObject) GetCommittedState(_ ethstate.Database, key ethcmn.Hash) ethcmn.Hash {
|
func (so *stateObject) GetCommittedState(_ ethstate.Database, key ethcmn.Hash) ethcmn.Hash {
|
||||||
prefixKey := so.GetStorageByAddressKey(key.Bytes())
|
prefixKey := so.GetStorageByAddressKey(key.Bytes())
|
||||||
|
|
||||||
// if we have the original value cached, return that
|
// if we have the original value cached, return that
|
||||||
value, cached := so.originStorage[prefixKey]
|
idx, cached := so.keyToOriginStorageIndex[prefixKey]
|
||||||
if cached {
|
if cached {
|
||||||
return value
|
return so.originStorage[idx].Value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(so.originStorage) == 0 {
|
||||||
|
so.originStorage = append(so.originStorage, NewState(prefixKey, ethcmn.Hash{}))
|
||||||
|
so.keyToOriginStorageIndex[prefixKey] = len(so.originStorage) - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
state := so.originStorage[idx]
|
||||||
|
|
||||||
// otherwise load the value from the KVStore
|
// otherwise load the value from the KVStore
|
||||||
ctx := so.stateDB.ctx
|
ctx := so.stateDB.ctx
|
||||||
store := prefix.NewStore(ctx.KVStore(so.stateDB.storeKey), AddressStoragePrefix(so.Address()))
|
store := prefix.NewStore(ctx.KVStore(so.stateDB.storeKey), AddressStoragePrefix(so.Address()))
|
||||||
rawValue := store.Get(prefixKey.Bytes())
|
rawValue := store.Get(prefixKey.Bytes())
|
||||||
|
|
||||||
if len(rawValue) > 0 {
|
if len(rawValue) > 0 {
|
||||||
value.SetBytes(rawValue)
|
state.Value.SetBytes(rawValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
so.originStorage[prefixKey] = value
|
so.originStorage[idx] = state
|
||||||
return value
|
return state.Value
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
@ -403,3 +437,11 @@ func (so stateObject) GetStorageByAddressKey(key []byte) ethcmn.Hash {
|
|||||||
|
|
||||||
return ethcrypto.Keccak256Hash(compositeKey)
|
return ethcrypto.Keccak256Hash(compositeKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// stateEntry represents a single key value pair from the StateDB's stateObject mappindg.
|
||||||
|
// This is to prevent non determinism at genesis initialization or export.
|
||||||
|
type stateEntry struct {
|
||||||
|
// address key of the state object
|
||||||
|
address ethcmn.Address
|
||||||
|
stateObject *stateObject
|
||||||
|
}
|
||||||
|
@ -35,6 +35,14 @@ func (suite *StateDBTestSuite) TestStateObject_State() {
|
|||||||
suite.stateObject.SetState(nil, ethcmn.BytesToHash([]byte("key1")), ethcmn.BytesToHash([]byte("value1")))
|
suite.stateObject.SetState(nil, ethcmn.BytesToHash([]byte("key1")), ethcmn.BytesToHash([]byte("value1")))
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"update value",
|
||||||
|
ethcmn.BytesToHash([]byte("key1")),
|
||||||
|
ethcmn.BytesToHash([]byte("value2")),
|
||||||
|
func() {
|
||||||
|
suite.stateObject.SetState(nil, ethcmn.BytesToHash([]byte("key1")), ethcmn.BytesToHash([]byte("value2")))
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCase {
|
for _, tc := range testCase {
|
||||||
|
@ -16,6 +16,7 @@ import (
|
|||||||
ethtypes "github.com/ethereum/go-ethereum/core/types"
|
ethtypes "github.com/ethereum/go-ethereum/core/types"
|
||||||
ethvm "github.com/ethereum/go-ethereum/core/vm"
|
ethvm "github.com/ethereum/go-ethereum/core/vm"
|
||||||
ethcrypto "github.com/ethereum/go-ethereum/crypto"
|
ethcrypto "github.com/ethereum/go-ethereum/crypto"
|
||||||
|
"github.com/ethereum/go-ethereum/rlp"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -45,10 +46,11 @@ type CommitStateDB struct {
|
|||||||
accountKeeper AccountKeeper
|
accountKeeper AccountKeeper
|
||||||
bankKeeper BankKeeper
|
bankKeeper BankKeeper
|
||||||
|
|
||||||
// maps that hold 'live' objects, which will get modified while processing a
|
// array that hold 'live' objects, which will get modified while processing a
|
||||||
// state transition
|
// state transition
|
||||||
stateObjects map[ethcmn.Address]*stateObject
|
stateObjects []stateEntry
|
||||||
stateObjectsDirty map[ethcmn.Address]struct{}
|
addressToObjectIndex map[ethcmn.Address]int // map from address to the index of the state objects slice
|
||||||
|
stateObjectsDirty map[ethcmn.Address]struct{}
|
||||||
|
|
||||||
// The refund counter, also used by state transitioning.
|
// The refund counter, also used by state transitioning.
|
||||||
refund uint64
|
refund uint64
|
||||||
@ -59,6 +61,8 @@ type CommitStateDB struct {
|
|||||||
|
|
||||||
// TODO: Determine if we actually need this as we do not need preimages in
|
// TODO: Determine if we actually need this as we do not need preimages in
|
||||||
// the SDK, but it seems to be used elsewhere in Geth.
|
// the SDK, but it seems to be used elsewhere in Geth.
|
||||||
|
//
|
||||||
|
// NOTE: it is safe to use map here because it's only used for Copy
|
||||||
preimages map[ethcmn.Hash][]byte
|
preimages map[ethcmn.Hash][]byte
|
||||||
|
|
||||||
// DB error.
|
// DB error.
|
||||||
@ -87,14 +91,15 @@ func NewCommitStateDB(
|
|||||||
ctx sdk.Context, storeKey sdk.StoreKey, ak AccountKeeper, bk BankKeeper,
|
ctx sdk.Context, storeKey sdk.StoreKey, ak AccountKeeper, bk BankKeeper,
|
||||||
) *CommitStateDB {
|
) *CommitStateDB {
|
||||||
return &CommitStateDB{
|
return &CommitStateDB{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
storeKey: storeKey,
|
storeKey: storeKey,
|
||||||
accountKeeper: ak,
|
accountKeeper: ak,
|
||||||
bankKeeper: bk,
|
bankKeeper: bk,
|
||||||
stateObjects: make(map[ethcmn.Address]*stateObject),
|
stateObjects: []stateEntry{},
|
||||||
stateObjectsDirty: make(map[ethcmn.Address]struct{}),
|
addressToObjectIndex: make(map[ethcmn.Address]int),
|
||||||
preimages: make(map[ethcmn.Hash][]byte),
|
stateObjectsDirty: make(map[ethcmn.Address]struct{}),
|
||||||
journal: newJournal(),
|
preimages: make(map[ethcmn.Hash][]byte),
|
||||||
|
journal: newJournal(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -389,34 +394,34 @@ func (csdb *CommitStateDB) Commit(deleteEmptyObjects bool) (ethcmn.Hash, error)
|
|||||||
defer csdb.clearJournalAndRefund()
|
defer csdb.clearJournalAndRefund()
|
||||||
|
|
||||||
// remove dirty state object entries based on the journal
|
// remove dirty state object entries based on the journal
|
||||||
for addr := range csdb.journal.dirties {
|
for _, dirty := range csdb.journal.dirties {
|
||||||
csdb.stateObjectsDirty[addr] = struct{}{}
|
csdb.stateObjectsDirty[dirty.address] = struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// set the state objects
|
// set the state objects
|
||||||
for addr, so := range csdb.stateObjects {
|
for _, stateEntry := range csdb.stateObjects {
|
||||||
_, isDirty := csdb.stateObjectsDirty[addr]
|
_, isDirty := csdb.stateObjectsDirty[stateEntry.address]
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case so.suicided || (isDirty && deleteEmptyObjects && so.empty()):
|
case stateEntry.stateObject.suicided || (isDirty && deleteEmptyObjects && stateEntry.stateObject.empty()):
|
||||||
// If the state object has been removed, don't bother syncing it and just
|
// If the state object has been removed, don't bother syncing it and just
|
||||||
// remove it from the store.
|
// remove it from the store.
|
||||||
csdb.deleteStateObject(so)
|
csdb.deleteStateObject(stateEntry.stateObject)
|
||||||
|
|
||||||
case isDirty:
|
case isDirty:
|
||||||
// write any contract code associated with the state object
|
// write any contract code associated with the state object
|
||||||
if so.code != nil && so.dirtyCode {
|
if stateEntry.stateObject.code != nil && stateEntry.stateObject.dirtyCode {
|
||||||
so.commitCode()
|
stateEntry.stateObject.commitCode()
|
||||||
so.dirtyCode = false
|
stateEntry.stateObject.dirtyCode = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// update the object in the KVStore
|
// update the object in the KVStore
|
||||||
if err := csdb.updateStateObject(so); err != nil {
|
if err := csdb.updateStateObject(stateEntry.stateObject); err != nil {
|
||||||
return ethcmn.Hash{}, err
|
return ethcmn.Hash{}, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(csdb.stateObjectsDirty, addr)
|
delete(csdb.stateObjectsDirty, stateEntry.address)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: Ethereum returns the trie merkle root here, but as commitment
|
// NOTE: Ethereum returns the trie merkle root here, but as commitment
|
||||||
@ -429,8 +434,8 @@ func (csdb *CommitStateDB) Commit(deleteEmptyObjects bool) (ethcmn.Hash, error)
|
|||||||
// removing the csdb destructed objects and clearing the journal as well as the
|
// removing the csdb destructed objects and clearing the journal as well as the
|
||||||
// refunds.
|
// refunds.
|
||||||
func (csdb *CommitStateDB) Finalise(deleteEmptyObjects bool) error {
|
func (csdb *CommitStateDB) Finalise(deleteEmptyObjects bool) error {
|
||||||
for addr := range csdb.journal.dirties {
|
for _, dirty := range csdb.journal.dirties {
|
||||||
so, exist := csdb.stateObjects[addr]
|
idx, exist := csdb.addressToObjectIndex[dirty.address]
|
||||||
if !exist {
|
if !exist {
|
||||||
// ripeMD is 'touched' at block 1714175, in tx:
|
// ripeMD is 'touched' at block 1714175, in tx:
|
||||||
// 0x1237f737031e40bcde4a8b7e717b2d15e3ecadfe49bb1bbc71ee9deb09c6fcf2
|
// 0x1237f737031e40bcde4a8b7e717b2d15e3ecadfe49bb1bbc71ee9deb09c6fcf2
|
||||||
@ -444,18 +449,19 @@ func (csdb *CommitStateDB) Finalise(deleteEmptyObjects bool) error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if so.suicided || (deleteEmptyObjects && so.empty()) {
|
stateEntry := csdb.stateObjects[idx]
|
||||||
csdb.deleteStateObject(so)
|
if stateEntry.stateObject.suicided || (deleteEmptyObjects && stateEntry.stateObject.empty()) {
|
||||||
|
csdb.deleteStateObject(stateEntry.stateObject)
|
||||||
} else {
|
} else {
|
||||||
// Set all the dirty state storage items for the state object in the
|
// Set all the dirty state storage items for the state object in the
|
||||||
// KVStore and finally set the account in the account mapper.
|
// KVStore and finally set the account in the account mapper.
|
||||||
so.commitState()
|
stateEntry.stateObject.commitState()
|
||||||
if err := csdb.updateStateObject(so); err != nil {
|
if err := csdb.updateStateObject(stateEntry.stateObject); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
csdb.stateObjectsDirty[addr] = struct{}{}
|
csdb.stateObjectsDirty[dirty.address] = struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// invalidate journal because reverting across transactions is not allowed
|
// invalidate journal because reverting across transactions is not allowed
|
||||||
@ -587,7 +593,8 @@ func (csdb *CommitStateDB) Suicide(addr ethcmn.Address) bool {
|
|||||||
// the underlying account mapper and store keys to avoid reloading data for the
|
// the underlying account mapper and store keys to avoid reloading data for the
|
||||||
// next operations.
|
// next operations.
|
||||||
func (csdb *CommitStateDB) Reset(_ ethcmn.Hash) error {
|
func (csdb *CommitStateDB) Reset(_ ethcmn.Hash) error {
|
||||||
csdb.stateObjects = make(map[ethcmn.Address]*stateObject)
|
csdb.stateObjects = []stateEntry{}
|
||||||
|
csdb.addressToObjectIndex = make(map[ethcmn.Address]int)
|
||||||
csdb.stateObjectsDirty = make(map[ethcmn.Address]struct{})
|
csdb.stateObjectsDirty = make(map[ethcmn.Address]struct{})
|
||||||
csdb.thash = ethcmn.Hash{}
|
csdb.thash = ethcmn.Hash{}
|
||||||
csdb.bhash = ethcmn.Hash{}
|
csdb.bhash = ethcmn.Hash{}
|
||||||
@ -601,27 +608,28 @@ func (csdb *CommitStateDB) Reset(_ ethcmn.Hash) error {
|
|||||||
|
|
||||||
// UpdateAccounts updates the nonce and coin balances of accounts
|
// UpdateAccounts updates the nonce and coin balances of accounts
|
||||||
func (csdb *CommitStateDB) UpdateAccounts() {
|
func (csdb *CommitStateDB) UpdateAccounts() {
|
||||||
for addr, so := range csdb.stateObjects {
|
for _, stateEntry := range csdb.stateObjects {
|
||||||
currAcc := csdb.accountKeeper.GetAccount(csdb.ctx, sdk.AccAddress(addr.Bytes()))
|
currAcc := csdb.accountKeeper.GetAccount(csdb.ctx, sdk.AccAddress(stateEntry.address.Bytes()))
|
||||||
emintAcc, ok := currAcc.(*emint.EthAccount)
|
emintAcc, ok := currAcc.(*emint.EthAccount)
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
balance := csdb.bankKeeper.GetBalance(csdb.ctx, emintAcc.GetAddress(), emint.DenomDefault)
|
balance := csdb.bankKeeper.GetBalance(csdb.ctx, emintAcc.GetAddress(), emint.DenomDefault)
|
||||||
if so.Balance() != balance.Amount.BigInt() && balance.IsValid() {
|
if stateEntry.stateObject.Balance() != balance.Amount.BigInt() && balance.IsValid() {
|
||||||
so.balance = balance.Amount
|
stateEntry.stateObject.balance = balance.Amount
|
||||||
}
|
}
|
||||||
|
|
||||||
if so.Nonce() != emintAcc.GetSequence() {
|
if stateEntry.stateObject.Nonce() != emintAcc.GetSequence() {
|
||||||
so.account = emintAcc
|
stateEntry.stateObject.account = emintAcc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClearStateObjects clears cache of state objects to handle account changes outside of the EVM
|
// ClearStateObjects clears cache of state objects to handle account changes outside of the EVM
|
||||||
func (csdb *CommitStateDB) ClearStateObjects() {
|
func (csdb *CommitStateDB) ClearStateObjects() {
|
||||||
csdb.stateObjects = make(map[ethcmn.Address]*stateObject)
|
csdb.stateObjects = []stateEntry{}
|
||||||
|
csdb.addressToObjectIndex = make(map[ethcmn.Address]int)
|
||||||
csdb.stateObjectsDirty = make(map[ethcmn.Address]struct{})
|
csdb.stateObjectsDirty = make(map[ethcmn.Address]struct{})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -665,28 +673,32 @@ func (csdb *CommitStateDB) Copy() *CommitStateDB {
|
|||||||
|
|
||||||
// copy all the basic fields, initialize the memory ones
|
// copy all the basic fields, initialize the memory ones
|
||||||
state := &CommitStateDB{
|
state := &CommitStateDB{
|
||||||
ctx: csdb.ctx,
|
ctx: csdb.ctx,
|
||||||
storeKey: csdb.storeKey,
|
storeKey: csdb.storeKey,
|
||||||
accountKeeper: csdb.accountKeeper,
|
accountKeeper: csdb.accountKeeper,
|
||||||
bankKeeper: csdb.bankKeeper,
|
bankKeeper: csdb.bankKeeper,
|
||||||
stateObjects: make(map[ethcmn.Address]*stateObject, len(csdb.journal.dirties)),
|
stateObjects: make([]stateEntry, len(csdb.journal.dirties)),
|
||||||
stateObjectsDirty: make(map[ethcmn.Address]struct{}, len(csdb.journal.dirties)),
|
addressToObjectIndex: make(map[ethcmn.Address]int, len(csdb.journal.dirties)),
|
||||||
refund: csdb.refund,
|
stateObjectsDirty: make(map[ethcmn.Address]struct{}, len(csdb.journal.dirties)),
|
||||||
logSize: csdb.logSize,
|
refund: csdb.refund,
|
||||||
preimages: make(map[ethcmn.Hash][]byte),
|
logSize: csdb.logSize,
|
||||||
journal: newJournal(),
|
preimages: make(map[ethcmn.Hash][]byte),
|
||||||
|
journal: newJournal(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// copy the dirty states, logs, and preimages
|
// copy the dirty states, logs, and preimages
|
||||||
for addr := range csdb.journal.dirties {
|
for _, dirty := range csdb.journal.dirties {
|
||||||
// There is a case where an object is in the journal but not in the
|
// There is a case where an object is in the journal but not in the
|
||||||
// stateObjects: OOG after touch on ripeMD prior to Byzantium. Thus, we
|
// stateObjects: OOG after touch on ripeMD prior to Byzantium. Thus, we
|
||||||
// need to check for nil.
|
// need to check for nil.
|
||||||
//
|
//
|
||||||
// Ref: https://github.com/ethereum/go-ethereum/pull/16485#issuecomment-380438527
|
// Ref: https://github.com/ethereum/go-ethereum/pull/16485#issuecomment-380438527
|
||||||
if object, exist := csdb.stateObjects[addr]; exist {
|
if idx, exist := csdb.addressToObjectIndex[dirty.address]; exist {
|
||||||
state.stateObjects[addr] = object.deepCopy(state)
|
state.stateObjects[idx] = stateEntry{
|
||||||
state.stateObjectsDirty[addr] = struct{}{}
|
address: dirty.address,
|
||||||
|
stateObject: csdb.stateObjects[idx].stateObject.deepCopy(state),
|
||||||
|
}
|
||||||
|
state.stateObjectsDirty[dirty.address] = struct{}{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -694,8 +706,8 @@ func (csdb *CommitStateDB) Copy() *CommitStateDB {
|
|||||||
// copied, the loop above will be a no-op, since the copy's journal is empty.
|
// copied, the loop above will be a no-op, since the copy's journal is empty.
|
||||||
// Thus, here we iterate over stateObjects, to enable copies of copies.
|
// Thus, here we iterate over stateObjects, to enable copies of copies.
|
||||||
for addr := range csdb.stateObjectsDirty {
|
for addr := range csdb.stateObjectsDirty {
|
||||||
if _, exist := state.stateObjects[addr]; !exist {
|
if idx, exist := state.addressToObjectIndex[addr]; !exist {
|
||||||
state.stateObjects[addr] = csdb.stateObjects[addr].deepCopy(state)
|
state.setStateObject(csdb.stateObjects[idx].stateObject.deepCopy(state))
|
||||||
state.stateObjectsDirty[addr] = struct{}{}
|
state.stateObjectsDirty[addr] = struct{}{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -708,9 +720,9 @@ func (csdb *CommitStateDB) Copy() *CommitStateDB {
|
|||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
// ForEachStorage iterates over each storage items, all invokes the provided
|
// ForEachStorage iterates over each storage items, all invoke the provided
|
||||||
// callback on each key, value pair .
|
// callback on each key, value pair.
|
||||||
func (csdb *CommitStateDB) ForEachStorage(addr ethcmn.Address, cb func(key, value ethcmn.Hash) bool) error {
|
func (csdb *CommitStateDB) ForEachStorage(addr ethcmn.Address, cb func(key, value ethcmn.Hash) (stop bool)) error {
|
||||||
so := csdb.getStateObject(addr)
|
so := csdb.getStateObject(addr)
|
||||||
if so == nil {
|
if so == nil {
|
||||||
return nil
|
return nil
|
||||||
@ -725,18 +737,23 @@ func (csdb *CommitStateDB) ForEachStorage(addr ethcmn.Address, cb func(key, valu
|
|||||||
key := ethcmn.BytesToHash(iterator.Key())
|
key := ethcmn.BytesToHash(iterator.Key())
|
||||||
value := ethcmn.BytesToHash(iterator.Value())
|
value := ethcmn.BytesToHash(iterator.Value())
|
||||||
|
|
||||||
if value, dirty := so.dirtyStorage[key]; dirty {
|
if idx, dirty := so.keyToDirtyStorageIndex[key]; dirty {
|
||||||
// check if iteration stops
|
// check if iteration stops
|
||||||
if cb(key, value) {
|
if cb(key, so.dirtyStorage[idx].Value) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_, content, _, err := rlp.Split(value.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// check if iteration stops
|
// check if iteration stops
|
||||||
if cb(key, value) {
|
if cb(key, ethcmn.BytesToHash(content)) {
|
||||||
break
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -784,13 +801,16 @@ func (csdb *CommitStateDB) setError(err error) {
|
|||||||
// getStateObject attempts to retrieve a state object given by the address.
|
// getStateObject attempts to retrieve a state object given by the address.
|
||||||
// Returns nil and sets an error if not found.
|
// Returns nil and sets an error if not found.
|
||||||
func (csdb *CommitStateDB) getStateObject(addr ethcmn.Address) (stateObject *stateObject) {
|
func (csdb *CommitStateDB) getStateObject(addr ethcmn.Address) (stateObject *stateObject) {
|
||||||
// prefer 'live' (cached) objects
|
idx, found := csdb.addressToObjectIndex[addr]
|
||||||
if so := csdb.stateObjects[addr]; so != nil {
|
if found {
|
||||||
if so.deleted {
|
// prefer 'live' (cached) objects
|
||||||
return nil
|
if so := csdb.stateObjects[idx].stateObject; so != nil {
|
||||||
}
|
if so.deleted {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
return so
|
return so
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// otherwise, attempt to fetch the account from the account mapper
|
// otherwise, attempt to fetch the account from the account mapper
|
||||||
@ -810,7 +830,21 @@ func (csdb *CommitStateDB) getStateObject(addr ethcmn.Address) (stateObject *sta
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (csdb *CommitStateDB) setStateObject(so *stateObject) {
|
func (csdb *CommitStateDB) setStateObject(so *stateObject) {
|
||||||
csdb.stateObjects[so.Address()] = so
|
idx, found := csdb.addressToObjectIndex[so.Address()]
|
||||||
|
if found {
|
||||||
|
// update the existing object
|
||||||
|
csdb.stateObjects[idx].stateObject = so
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// append the new state object to the stateObjects slice
|
||||||
|
se := stateEntry{
|
||||||
|
address: so.Address(),
|
||||||
|
stateObject: so,
|
||||||
|
}
|
||||||
|
|
||||||
|
csdb.stateObjects = append(csdb.stateObjects, se)
|
||||||
|
csdb.addressToObjectIndex[se.address] = len(csdb.stateObjects) - 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// RawDump returns a raw state dump.
|
// RawDump returns a raw state dump.
|
||||||
|
@ -404,6 +404,13 @@ func (suite *StateDBTestSuite) TestCommitStateDB_Finalize() {
|
|||||||
},
|
},
|
||||||
false, true,
|
false, true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"finalize, dirty storage",
|
||||||
|
func() {
|
||||||
|
suite.stateDB.SetState(suite.address, ethcmn.BytesToHash([]byte("key")), ethcmn.BytesToHash([]byte("value")))
|
||||||
|
},
|
||||||
|
false, true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"faled to update state object",
|
"faled to update state object",
|
||||||
func() {
|
func() {
|
||||||
|
72
x/evm/types/storage.go
Normal file
72
x/evm/types/storage.go
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||||
|
|
||||||
|
ethcmn "github.com/ethereum/go-ethereum/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Storage represents the account Storage map as a slice of single key value
|
||||||
|
// State pairs. This is to prevent non determinism at genesis initialization or export.
|
||||||
|
type Storage []State
|
||||||
|
|
||||||
|
// Validate performs a basic validation of the Storage fields.
|
||||||
|
func (s Storage) Validate() error {
|
||||||
|
seenStorage := make(map[string]bool)
|
||||||
|
for i, state := range s {
|
||||||
|
if seenStorage[state.Key.String()] {
|
||||||
|
return sdkerrors.Wrapf(ErrInvalidState, "duplicate state key %d", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := state.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
seenStorage[state.Key.String()] = true
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// String implements the stringer interface
|
||||||
|
func (s Storage) String() string {
|
||||||
|
var str string
|
||||||
|
for _, state := range s {
|
||||||
|
str += fmt.Sprintf("%s: %s\n", state.Key.String(), state.Value.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy returns a copy of storage.
|
||||||
|
func (s Storage) Copy() Storage {
|
||||||
|
cpy := make(Storage, len(s))
|
||||||
|
copy(cpy, s)
|
||||||
|
|
||||||
|
return cpy
|
||||||
|
}
|
||||||
|
|
||||||
|
// State represents a single Storage key value pair item.
|
||||||
|
type State struct {
|
||||||
|
Key ethcmn.Hash `json:"key"`
|
||||||
|
Value ethcmn.Hash `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate performs a basic validation of the State fields.
|
||||||
|
func (s State) Validate() error {
|
||||||
|
if bytes.Equal(s.Key.Bytes(), ethcmn.Hash{}.Bytes()) {
|
||||||
|
return sdkerrors.Wrap(ErrInvalidState, "state key hash cannot be empty")
|
||||||
|
}
|
||||||
|
// NOTE: state value can be empty
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewState creates a new State instance
|
||||||
|
func NewState(key, value ethcmn.Hash) State {
|
||||||
|
return State{
|
||||||
|
Key: key,
|
||||||
|
Value: value,
|
||||||
|
}
|
||||||
|
}
|
84
x/evm/types/storage_test.go
Normal file
84
x/evm/types/storage_test.go
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
ethcmn "github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStorageValidate(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
storage Storage
|
||||||
|
expPass bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"valid storage",
|
||||||
|
Storage{
|
||||||
|
NewState(ethcmn.BytesToHash([]byte{1, 2, 3}), ethcmn.BytesToHash([]byte{1, 2, 3})),
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"empty storage key bytes",
|
||||||
|
Storage{
|
||||||
|
{Key: ethcmn.Hash{}},
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duplicated storage key",
|
||||||
|
Storage{
|
||||||
|
{Key: ethcmn.BytesToHash([]byte{1, 2, 3})},
|
||||||
|
{Key: ethcmn.BytesToHash([]byte{1, 2, 3})},
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
tc := tc
|
||||||
|
err := tc.storage.Validate()
|
||||||
|
if tc.expPass {
|
||||||
|
require.NoError(t, err, tc.name)
|
||||||
|
} else {
|
||||||
|
require.Error(t, err, tc.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStorageCopy(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
storage Storage
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"single storage",
|
||||||
|
Storage{
|
||||||
|
NewState(ethcmn.BytesToHash([]byte{1, 2, 3}), ethcmn.BytesToHash([]byte{1, 2, 3})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"empty storage key value bytes",
|
||||||
|
Storage{
|
||||||
|
{Key: ethcmn.Hash{}, Value: ethcmn.Hash{}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"empty storage",
|
||||||
|
Storage{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
tc := tc
|
||||||
|
require.Equal(t, tc.storage, tc.storage.Copy(), tc.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStorageString(t *testing.T) {
|
||||||
|
storage := Storage{NewState(ethcmn.BytesToHash([]byte("key")), ethcmn.BytesToHash([]byte("value")))}
|
||||||
|
str := "0x00000000000000000000000000000000000000000000000000000000006b6579: 0x00000000000000000000000000000000000000000000000000000076616c7565\n"
|
||||||
|
require.Equal(t, str, storage.String())
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user