e6c9b7723b
Closes: #876 - coverage: 99.3% of statements
580 lines
17 KiB
Go
580 lines
17 KiB
Go
package statedb_test
|
|
|
|
import (
|
|
"math/big"
|
|
"testing"
|
|
|
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
|
"github.com/ethereum/go-ethereum/common"
|
|
ethtypes "github.com/ethereum/go-ethereum/core/types"
|
|
"github.com/ethereum/go-ethereum/core/vm"
|
|
"github.com/ethereum/go-ethereum/crypto"
|
|
"github.com/stretchr/testify/suite"
|
|
"github.com/tharsis/ethermint/x/evm/statedb"
|
|
)
|
|
|
|
var (
|
|
address common.Address = common.BigToAddress(big.NewInt(101))
|
|
address2 common.Address = common.BigToAddress(big.NewInt(102))
|
|
address3 common.Address = common.BigToAddress(big.NewInt(103))
|
|
blockHash common.Hash = common.BigToHash(big.NewInt(9999))
|
|
emptyTxConfig statedb.TxConfig = statedb.NewEmptyTxConfig(blockHash)
|
|
)
|
|
|
|
type StateDBTestSuite struct {
|
|
suite.Suite
|
|
}
|
|
|
|
func (suite *StateDBTestSuite) TestAccount() {
|
|
key1 := common.BigToHash(big.NewInt(1))
|
|
value1 := common.BigToHash(big.NewInt(2))
|
|
key2 := common.BigToHash(big.NewInt(3))
|
|
value2 := common.BigToHash(big.NewInt(4))
|
|
testCases := []struct {
|
|
name string
|
|
malleate func(*statedb.StateDB)
|
|
}{
|
|
{"non-exist account", func(db *statedb.StateDB) {
|
|
suite.Require().Equal(false, db.Exist(address))
|
|
suite.Require().Equal(true, db.Empty(address))
|
|
suite.Require().Equal(big.NewInt(0), db.GetBalance(address))
|
|
suite.Require().Equal([]byte(nil), db.GetCode(address))
|
|
suite.Require().Equal(common.Hash{}, db.GetCodeHash(address))
|
|
suite.Require().Equal(uint64(0), db.GetNonce(address))
|
|
}},
|
|
{"empty account", func(db *statedb.StateDB) {
|
|
db.CreateAccount(address)
|
|
suite.Require().NoError(db.Commit())
|
|
|
|
keeper := db.Keeper().(*MockKeeper)
|
|
acct := keeper.accounts[address]
|
|
suite.Require().Equal(statedb.NewEmptyAccount(), &acct.account)
|
|
suite.Require().Empty(acct.states)
|
|
suite.Require().False(acct.account.IsContract())
|
|
|
|
db = statedb.New(sdk.Context{}, keeper, emptyTxConfig)
|
|
suite.Require().Equal(true, db.Exist(address))
|
|
suite.Require().Equal(true, db.Empty(address))
|
|
suite.Require().Equal(big.NewInt(0), db.GetBalance(address))
|
|
suite.Require().Equal([]byte(nil), db.GetCode(address))
|
|
suite.Require().Equal(common.BytesToHash(emptyCodeHash), db.GetCodeHash(address))
|
|
suite.Require().Equal(uint64(0), db.GetNonce(address))
|
|
}},
|
|
{"suicide", func(db *statedb.StateDB) {
|
|
// non-exist account.
|
|
suite.Require().False(db.Suicide(address))
|
|
suite.Require().False(db.HasSuicided(address))
|
|
|
|
// create a contract account
|
|
db.CreateAccount(address)
|
|
db.SetCode(address, []byte("hello world"))
|
|
db.AddBalance(address, big.NewInt(100))
|
|
db.SetState(address, key1, value1)
|
|
db.SetState(address, key2, value2)
|
|
suite.Require().NoError(db.Commit())
|
|
|
|
// suicide
|
|
db = statedb.New(sdk.Context{}, db.Keeper(), emptyTxConfig)
|
|
suite.Require().False(db.HasSuicided(address))
|
|
suite.Require().True(db.Suicide(address))
|
|
|
|
// check dirty state
|
|
suite.Require().True(db.HasSuicided(address))
|
|
// balance is cleared
|
|
suite.Require().Equal(big.NewInt(0), db.GetBalance(address))
|
|
// but code and state are still accessible in dirty state
|
|
suite.Require().Equal(value1, db.GetState(address, key1))
|
|
suite.Require().Equal([]byte("hello world"), db.GetCode(address))
|
|
|
|
suite.Require().NoError(db.Commit())
|
|
|
|
// not accessible from StateDB anymore
|
|
db = statedb.New(sdk.Context{}, db.Keeper(), emptyTxConfig)
|
|
suite.Require().False(db.Exist(address))
|
|
|
|
// and cleared in keeper too
|
|
keeper := db.Keeper().(*MockKeeper)
|
|
suite.Require().Empty(keeper.accounts)
|
|
suite.Require().Empty(keeper.codes)
|
|
}},
|
|
}
|
|
for _, tc := range testCases {
|
|
suite.Run(tc.name, func() {
|
|
keeper := NewMockKeeper()
|
|
db := statedb.New(sdk.Context{}, keeper, emptyTxConfig)
|
|
tc.malleate(db)
|
|
})
|
|
}
|
|
}
|
|
|
|
func (suite *StateDBTestSuite) TestAccountOverride() {
|
|
keeper := NewMockKeeper()
|
|
db := statedb.New(sdk.Context{}, keeper, emptyTxConfig)
|
|
// test balance carry over when overwritten
|
|
amount := big.NewInt(1)
|
|
|
|
// init an EOA account, account overriden only happens on EOA account.
|
|
db.AddBalance(address, amount)
|
|
db.SetNonce(address, 1)
|
|
|
|
// override
|
|
db.CreateAccount(address)
|
|
|
|
// check balance is not lost
|
|
suite.Require().Equal(amount, db.GetBalance(address))
|
|
// but nonce is reset
|
|
suite.Require().Equal(uint64(0), db.GetNonce(address))
|
|
}
|
|
|
|
func (suite *StateDBTestSuite) TestDBError() {
|
|
testCases := []struct {
|
|
name string
|
|
malleate func(vm.StateDB)
|
|
}{
|
|
{"set account", func(db vm.StateDB) {
|
|
db.SetNonce(errAddress, 1)
|
|
}},
|
|
{"delete account", func(db vm.StateDB) {
|
|
db.SetNonce(errAddress, 1)
|
|
suite.Require().True(db.Suicide(errAddress))
|
|
}},
|
|
}
|
|
for _, tc := range testCases {
|
|
db := statedb.New(sdk.Context{}, NewMockKeeper(), emptyTxConfig)
|
|
tc.malleate(db)
|
|
suite.Require().Error(db.Commit())
|
|
}
|
|
}
|
|
|
|
func (suite *StateDBTestSuite) TestBalance() {
|
|
// NOTE: no need to test overflow/underflow, that is guaranteed by evm implementation.
|
|
testCases := []struct {
|
|
name string
|
|
malleate func(*statedb.StateDB)
|
|
expBalance *big.Int
|
|
}{
|
|
{"add balance", func(db *statedb.StateDB) {
|
|
db.AddBalance(address, big.NewInt(10))
|
|
}, big.NewInt(10)},
|
|
{"sub balance", func(db *statedb.StateDB) {
|
|
db.AddBalance(address, big.NewInt(10))
|
|
// get dirty balance
|
|
suite.Require().Equal(big.NewInt(10), db.GetBalance(address))
|
|
db.SubBalance(address, big.NewInt(2))
|
|
}, big.NewInt(8)},
|
|
{"add zero balance", func(db *statedb.StateDB) {
|
|
db.AddBalance(address, big.NewInt(0))
|
|
}, big.NewInt(0)},
|
|
{"sub zero balance", func(db *statedb.StateDB) {
|
|
db.SubBalance(address, big.NewInt(0))
|
|
}, big.NewInt(0)},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
suite.Run(tc.name, func() {
|
|
keeper := NewMockKeeper()
|
|
db := statedb.New(sdk.Context{}, keeper, emptyTxConfig)
|
|
tc.malleate(db)
|
|
|
|
// check dirty state
|
|
suite.Require().Equal(tc.expBalance, db.GetBalance(address))
|
|
suite.Require().NoError(db.Commit())
|
|
// check committed balance too
|
|
suite.Require().Equal(tc.expBalance, keeper.accounts[address].account.Balance)
|
|
})
|
|
}
|
|
}
|
|
|
|
func (suite *StateDBTestSuite) TestState() {
|
|
key1 := common.BigToHash(big.NewInt(1))
|
|
value1 := common.BigToHash(big.NewInt(1))
|
|
testCases := []struct {
|
|
name string
|
|
malleate func(*statedb.StateDB)
|
|
expStates statedb.Storage
|
|
}{
|
|
{"empty state", func(db *statedb.StateDB) {
|
|
}, nil},
|
|
{"set empty value", func(db *statedb.StateDB) {
|
|
db.SetState(address, key1, common.Hash{})
|
|
}, statedb.Storage{}},
|
|
{"noop state change", func(db *statedb.StateDB) {
|
|
db.SetState(address, key1, value1)
|
|
db.SetState(address, key1, common.Hash{})
|
|
}, statedb.Storage{}},
|
|
{"set state", func(db *statedb.StateDB) {
|
|
// check empty initial state
|
|
suite.Require().Equal(common.Hash{}, db.GetState(address, key1))
|
|
suite.Require().Equal(common.Hash{}, db.GetCommittedState(address, key1))
|
|
|
|
// set state
|
|
db.SetState(address, key1, value1)
|
|
// query dirty state
|
|
suite.Require().Equal(value1, db.GetState(address, key1))
|
|
// check committed state is still not exist
|
|
suite.Require().Equal(common.Hash{}, db.GetCommittedState(address, key1))
|
|
|
|
// set same value again, should be noop
|
|
db.SetState(address, key1, value1)
|
|
suite.Require().Equal(value1, db.GetState(address, key1))
|
|
}, statedb.Storage{
|
|
key1: value1,
|
|
}},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
suite.Run(tc.name, func() {
|
|
keeper := NewMockKeeper()
|
|
db := statedb.New(sdk.Context{}, keeper, emptyTxConfig)
|
|
tc.malleate(db)
|
|
suite.Require().NoError(db.Commit())
|
|
|
|
// check committed states in keeper
|
|
suite.Require().Equal(tc.expStates, keeper.accounts[address].states)
|
|
|
|
// check ForEachStorage
|
|
db = statedb.New(sdk.Context{}, keeper, emptyTxConfig)
|
|
collected := CollectContractStorage(db)
|
|
if len(tc.expStates) > 0 {
|
|
suite.Require().Equal(tc.expStates, collected)
|
|
} else {
|
|
suite.Require().Empty(collected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func (suite *StateDBTestSuite) TestCode() {
|
|
code := []byte("hello world")
|
|
codeHash := crypto.Keccak256Hash(code)
|
|
|
|
testCases := []struct {
|
|
name string
|
|
malleate func(vm.StateDB)
|
|
expCode []byte
|
|
expCodeHash common.Hash
|
|
}{
|
|
{"non-exist account", func(vm.StateDB) {}, nil, common.Hash{}},
|
|
{"empty account", func(db vm.StateDB) {
|
|
db.CreateAccount(address)
|
|
}, nil, common.BytesToHash(emptyCodeHash)},
|
|
{"set code", func(db vm.StateDB) {
|
|
db.SetCode(address, code)
|
|
}, code, codeHash},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
suite.Run(tc.name, func() {
|
|
keeper := NewMockKeeper()
|
|
db := statedb.New(sdk.Context{}, keeper, emptyTxConfig)
|
|
tc.malleate(db)
|
|
|
|
// check dirty state
|
|
suite.Require().Equal(tc.expCode, db.GetCode(address))
|
|
suite.Require().Equal(len(tc.expCode), db.GetCodeSize(address))
|
|
suite.Require().Equal(tc.expCodeHash, db.GetCodeHash(address))
|
|
|
|
suite.Require().NoError(db.Commit())
|
|
|
|
// check again
|
|
db = statedb.New(sdk.Context{}, keeper, emptyTxConfig)
|
|
suite.Require().Equal(tc.expCode, db.GetCode(address))
|
|
suite.Require().Equal(len(tc.expCode), db.GetCodeSize(address))
|
|
suite.Require().Equal(tc.expCodeHash, db.GetCodeHash(address))
|
|
})
|
|
}
|
|
}
|
|
|
|
func (suite *StateDBTestSuite) TestRevertSnapshot() {
|
|
v1 := common.BigToHash(big.NewInt(1))
|
|
v2 := common.BigToHash(big.NewInt(2))
|
|
v3 := common.BigToHash(big.NewInt(3))
|
|
testCases := []struct {
|
|
name string
|
|
malleate func(vm.StateDB)
|
|
}{
|
|
{"set state", func(db vm.StateDB) {
|
|
db.SetState(address, v1, v3)
|
|
}},
|
|
{"set nonce", func(db vm.StateDB) {
|
|
db.SetNonce(address, 10)
|
|
}},
|
|
{"change balance", func(db vm.StateDB) {
|
|
db.AddBalance(address, big.NewInt(10))
|
|
db.SubBalance(address, big.NewInt(5))
|
|
}},
|
|
{"override account", func(db vm.StateDB) {
|
|
db.CreateAccount(address)
|
|
}},
|
|
{"set code", func(db vm.StateDB) {
|
|
db.SetCode(address, []byte("hello world"))
|
|
}},
|
|
{"suicide", func(db vm.StateDB) {
|
|
db.SetState(address, v1, v2)
|
|
db.SetCode(address, []byte("hello world"))
|
|
suite.Require().True(db.Suicide(address))
|
|
}},
|
|
{"add log", func(db vm.StateDB) {
|
|
db.AddLog(ðtypes.Log{
|
|
Address: address,
|
|
})
|
|
}},
|
|
{"add refund", func(db vm.StateDB) {
|
|
db.AddRefund(10)
|
|
db.SubRefund(5)
|
|
}},
|
|
{"access list", func(db vm.StateDB) {
|
|
db.AddAddressToAccessList(address)
|
|
db.AddSlotToAccessList(address, v1)
|
|
}},
|
|
}
|
|
for _, tc := range testCases {
|
|
suite.Run(tc.name, func() {
|
|
ctx := sdk.Context{}
|
|
keeper := NewMockKeeper()
|
|
|
|
{
|
|
// do some arbitrary changes to the storage
|
|
db := statedb.New(ctx, keeper, emptyTxConfig)
|
|
db.SetNonce(address, 1)
|
|
db.AddBalance(address, big.NewInt(100))
|
|
db.SetCode(address, []byte("hello world"))
|
|
db.SetState(address, v1, v2)
|
|
db.SetNonce(address2, 1)
|
|
suite.Require().NoError(db.Commit())
|
|
}
|
|
|
|
originalKeeper := keeper.Clone()
|
|
|
|
// run test
|
|
db := statedb.New(ctx, keeper, emptyTxConfig)
|
|
rev := db.Snapshot()
|
|
tc.malleate(db)
|
|
db.RevertToSnapshot(rev)
|
|
|
|
// check empty states after revert
|
|
suite.Require().Zero(db.GetRefund())
|
|
suite.Require().Empty(db.Logs())
|
|
|
|
suite.Require().NoError(db.Commit())
|
|
|
|
// check keeper should stay the same
|
|
suite.Require().Equal(originalKeeper, keeper)
|
|
})
|
|
}
|
|
}
|
|
|
|
func (suite *StateDBTestSuite) TestNestedSnapshot() {
|
|
key := common.BigToHash(big.NewInt(1))
|
|
value1 := common.BigToHash(big.NewInt(1))
|
|
value2 := common.BigToHash(big.NewInt(2))
|
|
|
|
db := statedb.New(sdk.Context{}, NewMockKeeper(), emptyTxConfig)
|
|
|
|
rev1 := db.Snapshot()
|
|
db.SetState(address, key, value1)
|
|
|
|
rev2 := db.Snapshot()
|
|
db.SetState(address, key, value2)
|
|
suite.Require().Equal(value2, db.GetState(address, key))
|
|
|
|
db.RevertToSnapshot(rev2)
|
|
suite.Require().Equal(value1, db.GetState(address, key))
|
|
|
|
db.RevertToSnapshot(rev1)
|
|
suite.Require().Equal(common.Hash{}, db.GetState(address, key))
|
|
}
|
|
|
|
func (suite *StateDBTestSuite) TestInvalidSnapshotId() {
|
|
db := statedb.New(sdk.Context{}, NewMockKeeper(), emptyTxConfig)
|
|
suite.Require().Panics(func() {
|
|
db.RevertToSnapshot(1)
|
|
})
|
|
}
|
|
|
|
func (suite *StateDBTestSuite) TestAccessList() {
|
|
value1 := common.BigToHash(big.NewInt(1))
|
|
value2 := common.BigToHash(big.NewInt(2))
|
|
|
|
testCases := []struct {
|
|
name string
|
|
malleate func(vm.StateDB)
|
|
}{
|
|
{"add address", func(db vm.StateDB) {
|
|
suite.Require().False(db.AddressInAccessList(address))
|
|
db.AddAddressToAccessList(address)
|
|
suite.Require().True(db.AddressInAccessList(address))
|
|
|
|
addrPresent, slotPresent := db.SlotInAccessList(address, value1)
|
|
suite.Require().True(addrPresent)
|
|
suite.Require().False(slotPresent)
|
|
|
|
// add again, should be no-op
|
|
db.AddAddressToAccessList(address)
|
|
suite.Require().True(db.AddressInAccessList(address))
|
|
}},
|
|
{"add slot", func(db vm.StateDB) {
|
|
addrPresent, slotPresent := db.SlotInAccessList(address, value1)
|
|
suite.Require().False(addrPresent)
|
|
suite.Require().False(slotPresent)
|
|
db.AddSlotToAccessList(address, value1)
|
|
addrPresent, slotPresent = db.SlotInAccessList(address, value1)
|
|
suite.Require().True(addrPresent)
|
|
suite.Require().True(slotPresent)
|
|
|
|
// add another slot
|
|
db.AddSlotToAccessList(address, value2)
|
|
addrPresent, slotPresent = db.SlotInAccessList(address, value2)
|
|
suite.Require().True(addrPresent)
|
|
suite.Require().True(slotPresent)
|
|
|
|
// add again, should be noop
|
|
db.AddSlotToAccessList(address, value2)
|
|
addrPresent, slotPresent = db.SlotInAccessList(address, value2)
|
|
suite.Require().True(addrPresent)
|
|
suite.Require().True(slotPresent)
|
|
}},
|
|
{"prepare access list", func(db vm.StateDB) {
|
|
al := ethtypes.AccessList{{
|
|
Address: address3,
|
|
StorageKeys: []common.Hash{value1},
|
|
}}
|
|
db.PrepareAccessList(address, &address2, vm.PrecompiledAddressesBerlin, al)
|
|
|
|
// check sender and dst
|
|
suite.Require().True(db.AddressInAccessList(address))
|
|
suite.Require().True(db.AddressInAccessList(address2))
|
|
// check precompiles
|
|
suite.Require().True(db.AddressInAccessList(common.BytesToAddress([]byte{1})))
|
|
// check AccessList
|
|
suite.Require().True(db.AddressInAccessList(address3))
|
|
addrPresent, slotPresent := db.SlotInAccessList(address3, value1)
|
|
suite.Require().True(addrPresent)
|
|
suite.Require().True(slotPresent)
|
|
addrPresent, slotPresent = db.SlotInAccessList(address3, value2)
|
|
suite.Require().True(addrPresent)
|
|
suite.Require().False(slotPresent)
|
|
}},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
db := statedb.New(sdk.Context{}, NewMockKeeper(), emptyTxConfig)
|
|
tc.malleate(db)
|
|
}
|
|
}
|
|
|
|
func (suite *StateDBTestSuite) TestLog() {
|
|
txHash := common.BytesToHash([]byte("tx"))
|
|
// use a non-default tx config
|
|
txConfig := statedb.NewTxConfig(
|
|
blockHash,
|
|
txHash,
|
|
1, 1,
|
|
)
|
|
db := statedb.New(sdk.Context{}, NewMockKeeper(), txConfig)
|
|
data := []byte("hello world")
|
|
db.AddLog(ðtypes.Log{
|
|
Address: address,
|
|
Topics: []common.Hash{},
|
|
Data: data,
|
|
BlockNumber: 1,
|
|
})
|
|
suite.Require().Equal(1, len(db.Logs()))
|
|
expecedLog := ðtypes.Log{
|
|
Address: address,
|
|
Topics: []common.Hash{},
|
|
Data: data,
|
|
BlockNumber: 1,
|
|
BlockHash: blockHash,
|
|
TxHash: txHash,
|
|
TxIndex: 1,
|
|
Index: 1,
|
|
}
|
|
suite.Require().Equal(expecedLog, db.Logs()[0])
|
|
|
|
db.AddLog(ðtypes.Log{
|
|
Address: address,
|
|
Topics: []common.Hash{},
|
|
Data: data,
|
|
BlockNumber: 1,
|
|
})
|
|
suite.Require().Equal(2, len(db.Logs()))
|
|
expecedLog.Index++
|
|
suite.Require().Equal(expecedLog, db.Logs()[1])
|
|
}
|
|
|
|
func (suite *StateDBTestSuite) TestRefund() {
|
|
testCases := []struct {
|
|
name string
|
|
malleate func(vm.StateDB)
|
|
expRefund uint64
|
|
expPanic bool
|
|
}{
|
|
{"add refund", func(db vm.StateDB) {
|
|
db.AddRefund(uint64(10))
|
|
}, 10, false},
|
|
{"sub refund", func(db vm.StateDB) {
|
|
db.AddRefund(uint64(10))
|
|
db.SubRefund(uint64(5))
|
|
}, 5, false},
|
|
{"negative refund counter", func(db vm.StateDB) {
|
|
db.AddRefund(uint64(5))
|
|
db.SubRefund(uint64(10))
|
|
}, 0, true},
|
|
}
|
|
for _, tc := range testCases {
|
|
db := statedb.New(sdk.Context{}, NewMockKeeper(), emptyTxConfig)
|
|
if !tc.expPanic {
|
|
tc.malleate(db)
|
|
suite.Require().Equal(tc.expRefund, db.GetRefund())
|
|
} else {
|
|
suite.Require().Panics(func() {
|
|
tc.malleate(db)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func (suite *StateDBTestSuite) TestIterateStorage() {
|
|
key1 := common.BigToHash(big.NewInt(1))
|
|
value1 := common.BigToHash(big.NewInt(2))
|
|
key2 := common.BigToHash(big.NewInt(3))
|
|
value2 := common.BigToHash(big.NewInt(4))
|
|
|
|
keeper := NewMockKeeper()
|
|
db := statedb.New(sdk.Context{}, keeper, emptyTxConfig)
|
|
db.SetState(address, key1, value1)
|
|
db.SetState(address, key2, value2)
|
|
|
|
// ForEachStorage only iterate committed state
|
|
suite.Require().Empty(CollectContractStorage(db))
|
|
|
|
suite.Require().NoError(db.Commit())
|
|
|
|
storage := CollectContractStorage(db)
|
|
suite.Require().Equal(2, len(storage))
|
|
suite.Require().Equal(keeper.accounts[address].states, storage)
|
|
|
|
// break early iteration
|
|
storage = make(statedb.Storage)
|
|
db.ForEachStorage(address, func(k, v common.Hash) bool {
|
|
storage[k] = v
|
|
// return false to break early
|
|
return false
|
|
})
|
|
suite.Require().Equal(1, len(storage))
|
|
}
|
|
|
|
func CollectContractStorage(db vm.StateDB) statedb.Storage {
|
|
storage := make(statedb.Storage)
|
|
db.ForEachStorage(address, func(k, v common.Hash) bool {
|
|
storage[k] = v
|
|
return true
|
|
})
|
|
return storage
|
|
}
|
|
|
|
func TestStateDBTestSuite(t *testing.T) {
|
|
suite.Run(t, &StateDBTestSuite{})
|
|
}
|