Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add StateDB IAVL consistency tests #39

Closed
wants to merge 22 commits into from
Closed
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
0b1d547
test: Update apphash test for suspected issue
drklee3 Mar 4, 2024
b9acaf8
temp disable account clearning
drklee3 Mar 5, 2024
dd184c8
add journal based statedb for comparison testing
drklee3 Mar 6, 2024
827a16f
test: Add test for comparing statedb output iavl hashes
drklee3 Mar 6, 2024
5694ac8
Remove tracing
drklee3 Mar 6, 2024
760aa3a
feat: Add test for noop SetState unmodified iavl hash
drklee3 Mar 8, 2024
f00bfc5
test: Skip noop state change
drklee3 Mar 8, 2024
78aefce
fix: Re-add SetAccount on SetState
drklee3 Mar 9, 2024
4f5da07
test: Add additional test cases for noop SetState changes
drklee3 Mar 11, 2024
ed834a0
test: Add account number tests, extend hash test
drklee3 Mar 12, 2024
e99425f
feat: Skip no-op contract state changes
drklee3 Mar 12, 2024
94856c1
fix: Avoid skip on 0 value SubBalance
drklee3 Mar 12, 2024
6cf2664
test: Add account number legacy statedb tests for test correctness
drklee3 Mar 12, 2024
1279ba9
test: Enable reverse/random addr tests
drklee3 Mar 12, 2024
7bc0762
test: Skip account number cachectx tests
drklee3 Mar 12, 2024
7f73625
deps: Update cosmos-sdk version with additional store methods
drklee3 Mar 12, 2024
cd77f54
refactor: Rename legacy statedb package
drklee3 Mar 13, 2024
00f53e7
remove key logging
drklee3 Mar 13, 2024
3613d63
style: Fix lint issues
drklee3 Feb 28, 2024
9b2d90a
test: Run unmodified IAVL tree test on legacy statedb
drklee3 Mar 13, 2024
6958d5a
feat: Prototype to align account numbers behavior with legacy statedb
drklee3 Mar 13, 2024
08cd821
feat: Update ReassignAccountNumbers docs
drklee3 Mar 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -756,6 +756,13 @@ func (app *EthermintApp) InterfaceRegistry() types.InterfaceRegistry {
return app.interfaceRegistry
}

// GetKeys returns all KVStoreKeys
//
// NOTE: This is solely to be used for testing purposes.
func (app *EthermintApp) GetKeys() map[string]*storetypes.KVStoreKey {
return app.keys
}

// GetKey returns the KVStoreKey for the provided store key.
//
// NOTE: This is solely to be used for testing purposes.
Expand Down
299 changes: 269 additions & 30 deletions x/evm/keeper/state_transition_test.go
Original file line number Diff line number Diff line change
@@ -1,27 +1,32 @@
package keeper_test

import (
"bytes"
"fmt"
"math"
"math/big"
"os"
"time"
"strings"

codectypes "github.com/cosmos/cosmos-sdk/codec/types"
"github.com/cosmos/cosmos-sdk/store/iavl"
storetypes "github.com/cosmos/cosmos-sdk/store/types"
sdk "github.com/cosmos/cosmos-sdk/types"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
ethtypes "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/params"
"github.com/evmos/ethermint/tests"
"github.com/evmos/ethermint/x/evm/keeper"
"github.com/evmos/ethermint/x/evm/statedb"
"github.com/evmos/ethermint/x/evm/statedb_legacy"
"github.com/evmos/ethermint/x/evm/types"
"github.com/tendermint/tendermint/crypto/tmhash"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
tmtypes "github.com/tendermint/tendermint/types"

cosmosiavl "github.com/cosmos/iavl"
)

func (suite *KeeperTestSuite) TestGetHashFn() {
Expand Down Expand Up @@ -727,43 +732,277 @@ func (suite *KeeperTestSuite) TestGetProposerAddress() {
}
}

func (suite *KeeperTestSuite) TestConsistency() {
var tracer bytes.Buffer
suite.App.SetCommitMultiStoreTracer(&tracer)
// Commit so the ctx is updated with the tracer
var (
blockHash common.Hash = common.BigToHash(big.NewInt(9999))
emptyTxConfig statedb.TxConfig = statedb.NewEmptyTxConfig(blockHash)
)

func (suite *KeeperTestSuite) TestNoopStateChange_UnmodifiedIAVLTree() {
suite.T().Skip("CacheCtx StateDB does not currently skip noop state changes")

// On StateDB.Commit(), if there is a dirty state change that matches the
// committed state, it should be skipped. Only state changes that are
// different from committed state should be applied to the underlying store.
// Corresponding journal based StateDB code:
// https://github.com/ethereum/go-ethereum/blob/e31709db6570e302557a9bccd681034ea0dcc246/core/state/state_object.go#L302-L305
// https://github.com/Kava-Labs/ethermint/blob/877e8fd1bd140c37ad05ed613f31e28f0130c0c4/x/evm/statedb/statedb.go#L469-L472

// Even with store.Set() on the same pre-existing key and value, it will
// update the underlying iavl.Node version and thus the node hash, parent
// hashes, and the commitID

// E.g. SetState(A, B) -> xxx -> SetState(A, B) should not be applied to
// the underlying store.
// xxx could be 0 or more state changes to the same key. It can be different
// values since the only value that actually matters is the last one when
// Commit() is called.

addr := common.BigToAddress(big.NewInt(1))
key := common.BigToHash(big.NewInt(10))
value := common.BigToHash(big.NewInt(20))

db := statedb.New(suite.Ctx, suite.App.EvmKeeper, emptyTxConfig)
db.SetState(addr, key, value)
suite.Require().NoError(db.Commit())

suite.Commit()

suite.Require().True(
suite.Ctx.MultiStore().TracingEnabled(),
"tracer should be enabled",
store := suite.App.CommitMultiStore().GetStore(suite.App.GetKey(types.StoreKey))
iavlStore := store.(*iavl.Store)
commitID1 := iavlStore.LastCommitID()

// Set the same state again
db = statedb.New(suite.Ctx, suite.App.EvmKeeper, emptyTxConfig)
db.SetState(addr, key, value)
suite.Require().NoError(db.Commit())

suite.Commit()

commitID2 := iavlStore.LastCommitID()

// We can compare the commitIDs since this is *only* the x/evm store which
// doesn't change between blocks without state changes. Any version change,
// e.g. no-op change that was written when it shouldn't, will modify the
// hash.
suite.Require().Equal(
common.Bytes2Hex(commitID1.Hash),
common.Bytes2Hex(commitID2.Hash),
"evm store should be unchanged",
)
}

func (suite *KeeperTestSuite) TestStateDB_IAVLConsistency() {
// evm store keys prefixes:
// Code = 1
// Storage = 2
addr1 := common.BigToAddress(big.NewInt(1))
addr2 := common.BigToAddress(big.NewInt(2))

tests := []struct {
name string
maleate func(vmdb vm.StateDB)
shouldSkip bool
}{
{
"noop",
func(vmdb vm.StateDB) {
},
false,
},
{
"SetState",
func(vmdb vm.StateDB) {
vmdb.SetState(addr2, common.BigToHash(big.NewInt(1)), common.BigToHash(big.NewInt(2)))
},
false,
},
{
"SetCode",
func(vmdb vm.StateDB) {
vmdb.SetCode(addr2, []byte{1, 2, 3})
},
false,
},
{
"SetState",
func(vmdb vm.StateDB) {
vmdb.SetState(addr2, common.BytesToHash([]byte{1, 2, 3}), common.BytesToHash([]byte{4, 5, 6}))
},
false,
},
{
"SetState + SetCode",
func(vmdb vm.StateDB) {
vmdb.SetCode(addr1, []byte{10})
vmdb.SetState(addr2, common.BytesToHash([]byte{1, 2, 3}), common.BytesToHash([]byte{4, 5, 6}))
},
false,
},
{
// Fails due to different account numbers due to different SetAccount ordering
// Journal -> SetAccount ordered by address @ Commit() -> addr1, addr2
// CacheCtx -> Ordered by first SetCode/SetState call -> addr2, addr1
"SetState + SetCode, reverse address",
func(vmdb vm.StateDB) {
vmdb.SetCode(addr2, []byte{10})
vmdb.SetState(addr1, common.BytesToHash([]byte{1, 2, 3}), common.BytesToHash([]byte{4, 5, 6}))
},
true,
},
}

contractAddr := suite.DeployTestContract(suite.T(), suite.Address, big.NewInt(100))
suite.Require().NotEmpty(tracer.Bytes(), "tracer should have recorded something")
for _, tt := range tests {
suite.Run(tt.name, func() {
if tt.shouldSkip {
suite.T().Skip("skipping test - state incompatible")
}

suite.SetupTest()
suite.Commit()

// Cache CTX statedb
ctxDB := statedb.New(suite.Ctx, suite.App.EvmKeeper, emptyTxConfig)
tt.maleate(ctxDB)
suite.Require().NoError(ctxDB.Commit())

newRes := suite.Commit()

cacheNodes := suite.exportIAVLStoreNodes(suite.App.GetKey(authtypes.StoreKey))
cacheHashes := suite.GetStoreHashes()

// --------------------------------------------
// Reset state for legacy journal based StateDB
suite.SetupTest()
suite.Commit()

legacyDB := statedb_legacy.New(suite.Ctx, suite.App.EvmKeeper, emptyTxConfig)
tt.maleate(legacyDB)
suite.Require().NoError(legacyDB.Commit())

legacyRes := suite.Commit()

suite.T().Logf("newRes: %x", newRes.Data)
suite.T().Logf("legacyRes: %x", legacyRes.Data)

suite.Equalf(
common.Bytes2Hex(newRes.Data),
common.Bytes2Hex(legacyRes.Data),
"commitID.Hash should match between statedb versions, old %x, new %x",
legacyRes.Data,
newRes.Data,
)

// Don't log any additional info if the test passed
if !suite.T().Failed() {
return
}

journalNodes := suite.exportIAVLStoreNodes(suite.App.GetKey(authtypes.StoreKey))
journalHashes := suite.GetStoreHashes()

suite.Equal(cacheHashes, journalHashes)

hashDiff := storeHashDiff(cacheHashes, journalHashes)
for k, v := range hashDiff {
suite.T().Logf("%v (cache -> journal): %v", k, v)
}

fmt.Printf("----------------------------------------\n")
fmt.Printf("CTX NODES:\n")
fmt.Printf("%v\n\n", cacheNodes)
fmt.Printf("----------------------------------------\n")
fmt.Printf("JOURNAL NODES:\n")
fmt.Printf("%v\n\n", journalNodes)
})
}
}

_, _, err := suite.TransferERC20Token(contractAddr, suite.Address, common.Address{1}, big.NewInt(1000))
suite.Require().Error(err)
func storeHashDiff(
aHashes map[string]string,
bHashes map[string]string,
) map[string]string {
diff := make(map[string]string)

for k, aHash := range aHashes {
bHash, ok := bHashes[k]
if !ok {
diff[k] = fmt.Sprintf("%v -> nil", aHash)
continue
}

if aHash != bHash {
diff[k] = fmt.Sprintf("%v -> %v", aHash, bHash)
}
}

res := suite.Commit()
for k, bHash := range bHashes {
if _, ok := aHashes[k]; !ok {
diff[k] = fmt.Sprintf("nil -> %v", bHash)
}
}

// Log the tracer contents
suite.T().Logf("Tracer (%v): %s", tracer.Len(), tracer.String())
return diff
}

// GetStoreHashes returns the IAVL hashes of all the stores in the multistore
func (suite *KeeperTestSuite) GetStoreHashes() map[string]string {
cms := suite.App.CommitMultiStore()
storeKeys := suite.App.GetKeys()
storeHashes := make(map[string]string)

for _, key := range storeKeys {
store := cms.GetStore(key)
iavlStore := store.(*iavl.Store)
storeHashes[key.Name()] = common.Bytes2Hex(iavlStore.LastCommitID().Hash)
}

return storeHashes
}

// Write tracer contents to file
err = os.WriteFile(fmt.Sprintf("tracer-ctx-%v.log", time.Now().Unix()), tracer.Bytes(), 0644)
func (suite *KeeperTestSuite) exportIAVLStoreNodes(
storeKey *storetypes.KVStoreKey,
) string {
cms := suite.App.CommitMultiStore()
store := cms.GetStore(storeKey)
authIavlStore := store.(*iavl.Store)

lastVersion := authIavlStore.LastCommitID().Version

exporter, err := authIavlStore.Export(lastVersion)
suite.Require().NoError(err)
defer exporter.Close()

suite.T().Logf("commitID.Hash: %x", res.Data)
nodes := []*cosmosiavl.ExportNode{}
for {
node, err := exporter.Next()
if err != nil || node == nil {
break
}

expectedHash := common.Hex2Bytes("10eaacd8ba1a2763c7ef1ac1090f7687baa299d2330ea1d593860a7aece3ecb5")
suite.Require().Equalf(
expectedHash,
res.Data,
"commitID.Hash should match, expected %x, got %x",
expectedHash,
res.Data,
)
nodes = append(nodes, node)
}

s := ""
s += fmt.Sprintf("%v store nodes @ version %v\n", len(nodes), lastVersion)

for _, node := range nodes {
indent := strings.Repeat(" ", int(node.Height))

valueStr := fmt.Sprintf("%x", node.Value)

if len(node.Value) == 0 {
valueStr = "nil"
}

s += fmt.Sprintf(
"%v[%v-%v] %x -> %s\n",
indent,
node.Height,
node.Version,
node.Key,
valueStr,
)
}

acc := suite.App.EvmKeeper.GetAccount(suite.Ctx, contractAddr)
suite.Require().True(acc.IsContract())
return s
}
5 changes: 5 additions & 0 deletions x/evm/statedb/statedb.go
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,11 @@ func (s *StateDB) SetCode(addr common.Address, code []byte) {

// SetState sets the contract state.
func (s *StateDB) SetState(addr common.Address, key, value common.Hash) {
acc := s.getOrNewAccount(addr)
if err := s.keeper.SetAccount(s.ctx.CurrentCtx(), addr, *acc); err != nil {
s.SetError(fmt.Errorf("failed to set account for state: %w", err))
}

// We cannot attempt to skip noop changes by just checking committed state
// Example:
// 1. With committed state to 0x0
Expand Down
Loading
Loading