diff --git a/CHANGELOG.md b/CHANGELOG.md index c9d3952b5..7a4030946 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ - [\#658](https://github.com/cosmos/evm/pull/658) Fix race condition between legacypool's RemoveTx and runReorg. - [\#687](https://github.com/cosmos/evm/pull/687) Avoid blocking node shutdown when evm indexer is enabled, log startup failures instead of using errgroup. - [\#689](https://github.com/cosmos/evm/pull/689) Align debug addr for hex address. +- [\#668](https://github.com/cosmos/evm/pull/668) Fix panic in legacy mempool when Reset() was called with a skipped header between old and new block. ### IMPROVEMENTS diff --git a/mempool/txpool/legacypool/mocks/BlockChain.go b/mempool/txpool/legacypool/mocks/BlockChain.go new file mode 100644 index 000000000..4b3411098 --- /dev/null +++ b/mempool/txpool/legacypool/mocks/BlockChain.go @@ -0,0 +1,124 @@ +// Code generated by mockery v2.53.5. DO NOT EDIT. + +package mocks + +import ( + common "github.com/ethereum/go-ethereum/common" + + mock "github.com/stretchr/testify/mock" + + params "github.com/ethereum/go-ethereum/params" + + types "github.com/ethereum/go-ethereum/core/types" + + vm "github.com/ethereum/go-ethereum/core/vm" +) + +// BlockChain is an autogenerated mock type for the BlockChain type +type BlockChain struct { + mock.Mock +} + +// Config provides a mock function with no fields +func (_m *BlockChain) Config() *params.ChainConfig { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Config") + } + + var r0 *params.ChainConfig + if rf, ok := ret.Get(0).(func() *params.ChainConfig); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*params.ChainConfig) + } + } + + return r0 +} + +// CurrentBlock provides a mock function with no fields +func (_m *BlockChain) CurrentBlock() *types.Header { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for CurrentBlock") + } + + var r0 *types.Header + if rf, ok := ret.Get(0).(func() *types.Header); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.Header) + } + } + + return r0 +} + +// GetBlock provides a mock function with given fields: hash, number +func (_m *BlockChain) GetBlock(hash common.Hash, number uint64) *types.Block { + ret := _m.Called(hash, number) + + if len(ret) == 0 { + panic("no return value specified for GetBlock") + } + + var r0 *types.Block + if rf, ok := ret.Get(0).(func(common.Hash, uint64) *types.Block); ok { + r0 = rf(hash, number) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.Block) + } + } + + return r0 +} + +// StateAt provides a mock function with given fields: root +func (_m *BlockChain) StateAt(root common.Hash) (vm.StateDB, error) { + ret := _m.Called(root) + + if len(ret) == 0 { + panic("no return value specified for StateAt") + } + + var r0 vm.StateDB + var r1 error + if rf, ok := ret.Get(0).(func(common.Hash) (vm.StateDB, error)); ok { + return rf(root) + } + if rf, ok := ret.Get(0).(func(common.Hash) vm.StateDB); ok { + r0 = rf(root) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(vm.StateDB) + } + } + + if rf, ok := ret.Get(1).(func(common.Hash) error); ok { + r1 = rf(root) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewBlockChain creates a new instance of BlockChain. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewBlockChain(t interface { + mock.TestingT + Cleanup(func()) +}) *BlockChain { + mock := &BlockChain{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mempool/txpool/legacypool/reset_production.go b/mempool/txpool/legacypool/reset_production.go index b566d4549..48645dbc2 100644 --- a/mempool/txpool/legacypool/reset_production.go +++ b/mempool/txpool/legacypool/reset_production.go @@ -3,8 +3,6 @@ package legacypool import ( - "math" - "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/log" ) @@ -16,76 +14,15 @@ func (pool *LegacyPool) reset(oldHead, newHead *types.Header) { var reinject types.Transactions if oldHead != nil && oldHead.Hash() != newHead.ParentHash { - // If the reorg is too deep, avoid doing it (will happen during fast sync) - oldNum := oldHead.Number.Uint64() - newNum := newHead.Number.Uint64() - - if depth := uint64(math.Abs(float64(oldNum) - float64(newNum))); depth > 64 { - log.Debug("Skipping deep transaction reorg", "depth", depth) - } else { - // Reorg seems shallow enough to pull in all transactions into memory - var ( - rem = pool.chain.GetBlock(oldHead.Hash(), oldHead.Number.Uint64()) - add = pool.chain.GetBlock(newHead.Hash(), newHead.Number.Uint64()) - ) - if rem == nil { - // This can happen if a setHead is performed, where we simply discard the old - // head from the chain. - if newNum >= oldNum { - // If we reorged to a same or higher number, then it's not a case of setHead - log.Warn("Transaction pool reset with missing old head", - "old", oldHead.Hash(), "oldnum", oldNum, "new", newHead.Hash(), "newnum", newNum) - return - } - // If the reorg ended up on a lower number, it's indicative of setHead being the cause - log.Debug("Skipping transaction reset caused by setHead", - "old", oldHead.Hash(), "oldnum", oldNum, "new", newHead.Hash(), "newnum", newNum) - // We still need to update the current state s.th. the lost transactions can be readded by the user - } else { - if add == nil { - // if the new head is nil, it means that something happened between - // the firing of newhead-event and _now_: most likely a - // reorg caused by sync-reversion or explicit sethead back to an - // earlier block. - log.Warn("Transaction pool reset with missing new head", "number", newHead.Number, "hash", newHead.Hash()) - return - } - var discarded, included types.Transactions - for rem.NumberU64() > add.NumberU64() { - discarded = append(discarded, rem.Transactions()...) - if rem = pool.chain.GetBlock(rem.ParentHash(), rem.NumberU64()-1); rem == nil { - log.Error("Unrooted old chain seen by tx pool", "block", oldHead.Number, "hash", oldHead.Hash()) - return - } - } - for add.NumberU64() > rem.NumberU64() { - included = append(included, add.Transactions()...) - if add = pool.chain.GetBlock(add.ParentHash(), add.NumberU64()-1); add == nil { - log.Error("Unrooted new chain seen by tx pool", "block", newHead.Number, "hash", newHead.Hash()) - return - } - } - for rem.Hash() != add.Hash() { - discarded = append(discarded, rem.Transactions()...) - if rem = pool.chain.GetBlock(rem.ParentHash(), rem.NumberU64()-1); rem == nil { - log.Error("Unrooted old chain seen by tx pool", "block", oldHead.Number, "hash", oldHead.Hash()) - return - } - included = append(included, add.Transactions()...) - if add = pool.chain.GetBlock(add.ParentHash(), add.NumberU64()-1); add == nil { - log.Error("Unrooted new chain seen by tx pool", "block", newHead.Number, "hash", newHead.Hash()) - return - } - } - lost := make([]*types.Transaction, 0, len(discarded)) - for _, tx := range types.TxDifference(discarded, included) { - if pool.Filter(tx) { - lost = append(lost, tx) - } - } - reinject = lost - } - } + // this is a strange reorg check from geth, it is possible for cosmos + // chains to call this function with newHead=oldHead+2, so + // newHead.ParentHash != oldHead.Hash. This would incorrectly be seen + // as a reorg on a cosmos chain and would therefore panic. Since this + // logic would only panic for cosmos chains in a valid state, we have + // removed it and replaced with a debug log. + // + // see https://github.com/cosmos/evm/pull/668 for more context. + log.Debug("leacypool saw skipped block (reorg) on cosmos chain, doing nothing...", "oldHead", oldHead.Hash(), "newHead", newHead.Hash(), "newParent", newHead.ParentHash) } pool.resetInternalState(newHead, reinject) } diff --git a/mempool/txpool/mocks/BlockChain.go b/mempool/txpool/mocks/BlockChain.go new file mode 100644 index 000000000..539de1453 --- /dev/null +++ b/mempool/txpool/mocks/BlockChain.go @@ -0,0 +1,127 @@ +// Code generated by mockery v2.53.5. DO NOT EDIT. + +package mocks + +import ( + common "github.com/ethereum/go-ethereum/common" + core "github.com/ethereum/go-ethereum/core" + + event "github.com/ethereum/go-ethereum/event" + + mock "github.com/stretchr/testify/mock" + + params "github.com/ethereum/go-ethereum/params" + + types "github.com/ethereum/go-ethereum/core/types" + + vm "github.com/ethereum/go-ethereum/core/vm" +) + +// BlockChain is an autogenerated mock type for the BlockChain type +type BlockChain struct { + mock.Mock +} + +// Config provides a mock function with no fields +func (_m *BlockChain) Config() *params.ChainConfig { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Config") + } + + var r0 *params.ChainConfig + if rf, ok := ret.Get(0).(func() *params.ChainConfig); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*params.ChainConfig) + } + } + + return r0 +} + +// CurrentBlock provides a mock function with no fields +func (_m *BlockChain) CurrentBlock() *types.Header { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for CurrentBlock") + } + + var r0 *types.Header + if rf, ok := ret.Get(0).(func() *types.Header); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.Header) + } + } + + return r0 +} + +// StateAt provides a mock function with given fields: root +func (_m *BlockChain) StateAt(root common.Hash) (vm.StateDB, error) { + ret := _m.Called(root) + + if len(ret) == 0 { + panic("no return value specified for StateAt") + } + + var r0 vm.StateDB + var r1 error + if rf, ok := ret.Get(0).(func(common.Hash) (vm.StateDB, error)); ok { + return rf(root) + } + if rf, ok := ret.Get(0).(func(common.Hash) vm.StateDB); ok { + r0 = rf(root) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(vm.StateDB) + } + } + + if rf, ok := ret.Get(1).(func(common.Hash) error); ok { + r1 = rf(root) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SubscribeChainHeadEvent provides a mock function with given fields: ch +func (_m *BlockChain) SubscribeChainHeadEvent(ch chan<- core.ChainHeadEvent) event.Subscription { + ret := _m.Called(ch) + + if len(ret) == 0 { + panic("no return value specified for SubscribeChainHeadEvent") + } + + var r0 event.Subscription + if rf, ok := ret.Get(0).(func(chan<- core.ChainHeadEvent) event.Subscription); ok { + r0 = rf(ch) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(event.Subscription) + } + } + + return r0 +} + +// NewBlockChain creates a new instance of BlockChain. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewBlockChain(t interface { + mock.TestingT + Cleanup(func()) +}) *BlockChain { + mock := &BlockChain{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mempool/txpool/mocks/SubPool.go b/mempool/txpool/mocks/SubPool.go new file mode 100644 index 000000000..3bedf9f5d --- /dev/null +++ b/mempool/txpool/mocks/SubPool.go @@ -0,0 +1,442 @@ +// Code generated by mockery v2.53.5. DO NOT EDIT. + +package mocks + +import ( + big "math/big" + + common "github.com/ethereum/go-ethereum/common" + core "github.com/ethereum/go-ethereum/core" + + event "github.com/ethereum/go-ethereum/event" + + kzg4844 "github.com/ethereum/go-ethereum/crypto/kzg4844" + + mock "github.com/stretchr/testify/mock" + + txpool "github.com/cosmos/evm/mempool/txpool" + + types "github.com/ethereum/go-ethereum/core/types" +) + +// SubPool is an autogenerated mock type for the SubPool type +type SubPool struct { + mock.Mock +} + +// Add provides a mock function with given fields: txs, sync +func (_m *SubPool) Add(txs []*types.Transaction, sync bool) []error { + ret := _m.Called(txs, sync) + + if len(ret) == 0 { + panic("no return value specified for Add") + } + + var r0 []error + if rf, ok := ret.Get(0).(func([]*types.Transaction, bool) []error); ok { + r0 = rf(txs, sync) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]error) + } + } + + return r0 +} + +// Clear provides a mock function with no fields +func (_m *SubPool) Clear() { + _m.Called() +} + +// Close provides a mock function with no fields +func (_m *SubPool) Close() error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Close") + } + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Content provides a mock function with no fields +func (_m *SubPool) Content() (map[common.Address][]*types.Transaction, map[common.Address][]*types.Transaction) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Content") + } + + var r0 map[common.Address][]*types.Transaction + var r1 map[common.Address][]*types.Transaction + if rf, ok := ret.Get(0).(func() (map[common.Address][]*types.Transaction, map[common.Address][]*types.Transaction)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() map[common.Address][]*types.Transaction); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[common.Address][]*types.Transaction) + } + } + + if rf, ok := ret.Get(1).(func() map[common.Address][]*types.Transaction); ok { + r1 = rf() + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(map[common.Address][]*types.Transaction) + } + } + + return r0, r1 +} + +// ContentFrom provides a mock function with given fields: addr +func (_m *SubPool) ContentFrom(addr common.Address) ([]*types.Transaction, []*types.Transaction) { + ret := _m.Called(addr) + + if len(ret) == 0 { + panic("no return value specified for ContentFrom") + } + + var r0 []*types.Transaction + var r1 []*types.Transaction + if rf, ok := ret.Get(0).(func(common.Address) ([]*types.Transaction, []*types.Transaction)); ok { + return rf(addr) + } + if rf, ok := ret.Get(0).(func(common.Address) []*types.Transaction); ok { + r0 = rf(addr) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*types.Transaction) + } + } + + if rf, ok := ret.Get(1).(func(common.Address) []*types.Transaction); ok { + r1 = rf(addr) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).([]*types.Transaction) + } + } + + return r0, r1 +} + +// Filter provides a mock function with given fields: tx +func (_m *SubPool) Filter(tx *types.Transaction) bool { + ret := _m.Called(tx) + + if len(ret) == 0 { + panic("no return value specified for Filter") + } + + var r0 bool + if rf, ok := ret.Get(0).(func(*types.Transaction) bool); ok { + r0 = rf(tx) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// Get provides a mock function with given fields: hash +func (_m *SubPool) Get(hash common.Hash) *types.Transaction { + ret := _m.Called(hash) + + if len(ret) == 0 { + panic("no return value specified for Get") + } + + var r0 *types.Transaction + if rf, ok := ret.Get(0).(func(common.Hash) *types.Transaction); ok { + r0 = rf(hash) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.Transaction) + } + } + + return r0 +} + +// GetBlobs provides a mock function with given fields: vhashes +func (_m *SubPool) GetBlobs(vhashes []common.Hash) ([]*kzg4844.Blob, []*kzg4844.Proof) { + ret := _m.Called(vhashes) + + if len(ret) == 0 { + panic("no return value specified for GetBlobs") + } + + var r0 []*kzg4844.Blob + var r1 []*kzg4844.Proof + if rf, ok := ret.Get(0).(func([]common.Hash) ([]*kzg4844.Blob, []*kzg4844.Proof)); ok { + return rf(vhashes) + } + if rf, ok := ret.Get(0).(func([]common.Hash) []*kzg4844.Blob); ok { + r0 = rf(vhashes) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*kzg4844.Blob) + } + } + + if rf, ok := ret.Get(1).(func([]common.Hash) []*kzg4844.Proof); ok { + r1 = rf(vhashes) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).([]*kzg4844.Proof) + } + } + + return r0, r1 +} + +// GetMetadata provides a mock function with given fields: hash +func (_m *SubPool) GetMetadata(hash common.Hash) *txpool.TxMetadata { + ret := _m.Called(hash) + + if len(ret) == 0 { + panic("no return value specified for GetMetadata") + } + + var r0 *txpool.TxMetadata + if rf, ok := ret.Get(0).(func(common.Hash) *txpool.TxMetadata); ok { + r0 = rf(hash) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*txpool.TxMetadata) + } + } + + return r0 +} + +// GetRLP provides a mock function with given fields: hash +func (_m *SubPool) GetRLP(hash common.Hash) []byte { + ret := _m.Called(hash) + + if len(ret) == 0 { + panic("no return value specified for GetRLP") + } + + var r0 []byte + if rf, ok := ret.Get(0).(func(common.Hash) []byte); ok { + r0 = rf(hash) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]byte) + } + } + + return r0 +} + +// Has provides a mock function with given fields: hash +func (_m *SubPool) Has(hash common.Hash) bool { + ret := _m.Called(hash) + + if len(ret) == 0 { + panic("no return value specified for Has") + } + + var r0 bool + if rf, ok := ret.Get(0).(func(common.Hash) bool); ok { + r0 = rf(hash) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// Init provides a mock function with given fields: gasTip, head, reserver +func (_m *SubPool) Init(gasTip uint64, head *types.Header, reserver txpool.Reserver) error { + ret := _m.Called(gasTip, head, reserver) + + if len(ret) == 0 { + panic("no return value specified for Init") + } + + var r0 error + if rf, ok := ret.Get(0).(func(uint64, *types.Header, txpool.Reserver) error); ok { + r0 = rf(gasTip, head, reserver) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Nonce provides a mock function with given fields: addr +func (_m *SubPool) Nonce(addr common.Address) uint64 { + ret := _m.Called(addr) + + if len(ret) == 0 { + panic("no return value specified for Nonce") + } + + var r0 uint64 + if rf, ok := ret.Get(0).(func(common.Address) uint64); ok { + r0 = rf(addr) + } else { + r0 = ret.Get(0).(uint64) + } + + return r0 +} + +// Pending provides a mock function with given fields: filter +func (_m *SubPool) Pending(filter txpool.PendingFilter) map[common.Address][]*txpool.LazyTransaction { + ret := _m.Called(filter) + + if len(ret) == 0 { + panic("no return value specified for Pending") + } + + var r0 map[common.Address][]*txpool.LazyTransaction + if rf, ok := ret.Get(0).(func(txpool.PendingFilter) map[common.Address][]*txpool.LazyTransaction); ok { + r0 = rf(filter) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[common.Address][]*txpool.LazyTransaction) + } + } + + return r0 +} + +// RemoveTx provides a mock function with given fields: hash, outofbound, unreserve +func (_m *SubPool) RemoveTx(hash common.Hash, outofbound bool, unreserve bool) int { + ret := _m.Called(hash, outofbound, unreserve) + + if len(ret) == 0 { + panic("no return value specified for RemoveTx") + } + + var r0 int + if rf, ok := ret.Get(0).(func(common.Hash, bool, bool) int); ok { + r0 = rf(hash, outofbound, unreserve) + } else { + r0 = ret.Get(0).(int) + } + + return r0 +} + +// Reset provides a mock function with given fields: oldHead, newHead +func (_m *SubPool) Reset(oldHead *types.Header, newHead *types.Header) { + _m.Called(oldHead, newHead) +} + +// SetGasTip provides a mock function with given fields: tip +func (_m *SubPool) SetGasTip(tip *big.Int) { + _m.Called(tip) +} + +// Stats provides a mock function with no fields +func (_m *SubPool) Stats() (int, int) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Stats") + } + + var r0 int + var r1 int + if rf, ok := ret.Get(0).(func() (int, int)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() int); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(int) + } + + if rf, ok := ret.Get(1).(func() int); ok { + r1 = rf() + } else { + r1 = ret.Get(1).(int) + } + + return r0, r1 +} + +// Status provides a mock function with given fields: hash +func (_m *SubPool) Status(hash common.Hash) txpool.TxStatus { + ret := _m.Called(hash) + + if len(ret) == 0 { + panic("no return value specified for Status") + } + + var r0 txpool.TxStatus + if rf, ok := ret.Get(0).(func(common.Hash) txpool.TxStatus); ok { + r0 = rf(hash) + } else { + r0 = ret.Get(0).(txpool.TxStatus) + } + + return r0 +} + +// SubscribeTransactions provides a mock function with given fields: ch, reorgs +func (_m *SubPool) SubscribeTransactions(ch chan<- core.NewTxsEvent, reorgs bool) event.Subscription { + ret := _m.Called(ch, reorgs) + + if len(ret) == 0 { + panic("no return value specified for SubscribeTransactions") + } + + var r0 event.Subscription + if rf, ok := ret.Get(0).(func(chan<- core.NewTxsEvent, bool) event.Subscription); ok { + r0 = rf(ch, reorgs) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(event.Subscription) + } + } + + return r0 +} + +// ValidateTxBasics provides a mock function with given fields: tx +func (_m *SubPool) ValidateTxBasics(tx *types.Transaction) error { + ret := _m.Called(tx) + + if len(ret) == 0 { + panic("no return value specified for ValidateTxBasics") + } + + var r0 error + if rf, ok := ret.Get(0).(func(*types.Transaction) error); ok { + r0 = rf(tx) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewSubPool creates a new instance of SubPool. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewSubPool(t interface { + mock.TestingT + Cleanup(func()) +}) *SubPool { + mock := &SubPool{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mempool/txpool/txpool_test.go b/mempool/txpool/txpool_test.go new file mode 100644 index 000000000..e80805ba6 --- /dev/null +++ b/mempool/txpool/txpool_test.go @@ -0,0 +1,140 @@ +package txpool_test + +import ( + "math" + "math/big" + "testing" + "time" + + "github.com/cosmos/evm/mempool/txpool" + "github.com/cosmos/evm/mempool/txpool/legacypool" + legacypool_mocks "github.com/cosmos/evm/mempool/txpool/legacypool/mocks" + statedb_mocks "github.com/cosmos/evm/x/vm/statedb/mocks" + + "github.com/cosmos/evm/mempool/txpool/mocks" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/event" + "github.com/ethereum/go-ethereum/params" + "github.com/holiman/uint256" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +// TestTxPoolCosmosReorg is a regression test for when slow processing of the +// legacypool run reorg function (as a subpool) would cause a panic if new +// headers are produced during this slow processing. +// +// Here we are using the legacypool as a subpool of the txpool. We then add tx +// to the mempool, and simulate a long broadcast to the comet mempool via +// overriding the BroadcastFn. We then advance the chain 3 blocks by sending +// three headers on the newHeadCh. This will then cause runReorg to be run with +// a newHead that is at oldHead + 3. Previously, this incorrectly was seen as a +// reorg by the legacypool, and would call GetBlock on the mempools BlockChain, +// which would cause a panic. + +// NOTE: we are using a mocked BlockChain impl here, but are simply manually +// making any calls to GetBlock panic). +func TestTxPoolCosmosReorg(t *testing.T) { + gasTip := uint64(100) + gasLimit := uint64(1_000_000) + + // mock tx signer and priv key + signer := types.HomesteadSigner{} + key, err := crypto.GenerateKey() + require.NoError(t, err) + + // the blockchain interface that the legacypool and txpool want are + // slightly different, so sadly we have to use two different mocks for this + chain := mocks.NewBlockChain(t) + legacyChain := legacypool_mocks.NewBlockChain(t) + genesisState := statedb_mocks.NewStateDB(t) + + // simulated headers on chain + genesisHeader := &types.Header{GasLimit: gasLimit, Difficulty: big.NewInt(1), Number: big.NewInt(0)} + height1Header := &types.Header{ParentHash: genesisHeader.Hash(), GasLimit: gasLimit, Difficulty: big.NewInt(1), Number: big.NewInt(1)} + height2Header := &types.Header{ParentHash: height1Header.Hash(), GasLimit: gasLimit, Difficulty: big.NewInt(1), Number: big.NewInt(2)} + height3Header := &types.Header{ParentHash: height2Header.Hash(), GasLimit: gasLimit, Difficulty: big.NewInt(1), Number: big.NewInt(3)} + + // called during legacypool initialization + cfg := ¶ms.ChainConfig{ChainID: nil} + legacyChain.On("Config").Return(cfg) + chain.On("Config").Return(cfg) + legacyChain.On("StateAt", genesisHeader.Root).Return(genesisState, nil) + chain.On("StateAt", genesisHeader.Root).Return(nil, nil) + + // starting the chain(s) at genesisHeader + chain.On("CurrentBlock").Return(genesisHeader) + + // we have to mock this, but this matches the behavior of the real impl if + // GetBlock is called + legacyChain.On("GetBlock", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { + panic("GetBlock called means reorg detected when there was not one!") + }).Maybe() + + // all accounts have max balance at genesis + genesisState.On("GetBalance", mock.Anything).Return(uint256.NewInt(math.MaxUint64)) + genesisState.On("GetNonce", mock.Anything).Return(uint64(1)) + genesisState.On("GetCodeHash", mock.Anything).Return(types.EmptyCodeHash) + + legacyPool := legacypool.New(legacypool.DefaultConfig, legacyChain) + + // handle txpool subscribing to new head events from the chain. grab the + // reference to the chan that it is going to wait on so we can push mock + // headers during the test + waitForSubscription := make(chan struct{}, 1) + var newHeadCh chan<- core.ChainHeadEvent + chain.On("SubscribeChainHeadEvent", mock.Anything).Run(func(args mock.Arguments) { + newHeadCh = args.Get(0).(chan<- core.ChainHeadEvent) + waitForSubscription <- struct{}{} + }).Return(event.NewSubscription(func(c <-chan struct{}) error { return nil })) + + pool, err := txpool.New(gasTip, chain, []txpool.SubPool{legacyPool}) + require.NoError(t, err) + defer pool.Close() + + // wait for newHeadCh to be initialized + <-waitForSubscription + + // override broadcast fn to wait until we advance the chain a few blocks + broadcastGuard := make(chan struct{}) + legacyPool.BroadcastTxFn = func(txs []*types.Transaction) error { + <-broadcastGuard + return nil + } + + // add tx1 to the pool so that the blocking broadcast fn will be called, + // simulating a slow runReorg + tx1, _ := types.SignTx(types.NewTransaction(1, common.Address{}, big.NewInt(100), 100_000, big.NewInt(int64(gasTip)+1), nil), signer, key) + errs := pool.Add([]*types.Transaction{tx1}, false) + for _, err := range errs { + require.NoError(t, err) + } + + // broadcast fn is now blocking, waiting for broadcastGuard + + // during this time, we will simulate advancing the chain multiple times by + // sending headers on the newHeadCh + newHeadCh <- core.ChainHeadEvent{Header: height1Header} + newHeadCh <- core.ChainHeadEvent{Header: height2Header} + newHeadCh <- core.ChainHeadEvent{Header: height3Header} + + // now that we have advanced the headers, unblock the broadcast fn + broadcastGuard <- struct{}{} + + // a runReorg call will now be scheduled with oldHead=genesis and + // newHead=height3 + + time.Sleep(500 * time.Millisecond) + + // push another tx to make sure that runReorg was processed with the above + // headers + legacyPool.BroadcastTxFn = func(txs []*types.Transaction) error { return nil } + tx2, _ := types.SignTx(types.NewTransaction(2, common.Address{}, big.NewInt(100), 100_000, big.NewInt(int64(gasTip)+1), nil), signer, key) + errs = pool.Add([]*types.Transaction{tx2}, false) + for _, err := range errs { + require.NoError(t, err) + } +} diff --git a/x/vm/statedb/mocks/statedb.go b/x/vm/statedb/mocks/statedb.go new file mode 100644 index 000000000..cc437ade0 --- /dev/null +++ b/x/vm/statedb/mocks/statedb.go @@ -0,0 +1,638 @@ +// Code generated by mockery v2.53.5. DO NOT EDIT. + +package mocks + +import ( + common "github.com/ethereum/go-ethereum/common" + mock "github.com/stretchr/testify/mock" + + params "github.com/ethereum/go-ethereum/params" + + state "github.com/ethereum/go-ethereum/core/state" + + stateless "github.com/ethereum/go-ethereum/core/stateless" + + tracing "github.com/ethereum/go-ethereum/core/tracing" + + types "github.com/ethereum/go-ethereum/core/types" + + uint256 "github.com/holiman/uint256" + + utils "github.com/ethereum/go-ethereum/trie/utils" +) + +// StateDB is an autogenerated mock type for the StateDB type +type StateDB struct { + mock.Mock +} + +// AccessEvents provides a mock function with no fields +func (_m *StateDB) AccessEvents() *state.AccessEvents { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for AccessEvents") + } + + var r0 *state.AccessEvents + if rf, ok := ret.Get(0).(func() *state.AccessEvents); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*state.AccessEvents) + } + } + + return r0 +} + +// AddAddressToAccessList provides a mock function with given fields: addr +func (_m *StateDB) AddAddressToAccessList(addr common.Address) { + _m.Called(addr) +} + +// AddBalance provides a mock function with given fields: _a0, _a1, _a2 +func (_m *StateDB) AddBalance(_a0 common.Address, _a1 *uint256.Int, _a2 tracing.BalanceChangeReason) uint256.Int { + ret := _m.Called(_a0, _a1, _a2) + + if len(ret) == 0 { + panic("no return value specified for AddBalance") + } + + var r0 uint256.Int + if rf, ok := ret.Get(0).(func(common.Address, *uint256.Int, tracing.BalanceChangeReason) uint256.Int); ok { + r0 = rf(_a0, _a1, _a2) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(uint256.Int) + } + } + + return r0 +} + +// AddLog provides a mock function with given fields: _a0 +func (_m *StateDB) AddLog(_a0 *types.Log) { + _m.Called(_a0) +} + +// AddPreimage provides a mock function with given fields: _a0, _a1 +func (_m *StateDB) AddPreimage(_a0 common.Hash, _a1 []byte) { + _m.Called(_a0, _a1) +} + +// AddRefund provides a mock function with given fields: _a0 +func (_m *StateDB) AddRefund(_a0 uint64) { + _m.Called(_a0) +} + +// AddSlotToAccessList provides a mock function with given fields: addr, slot +func (_m *StateDB) AddSlotToAccessList(addr common.Address, slot common.Hash) { + _m.Called(addr, slot) +} + +// AddressInAccessList provides a mock function with given fields: addr +func (_m *StateDB) AddressInAccessList(addr common.Address) bool { + ret := _m.Called(addr) + + if len(ret) == 0 { + panic("no return value specified for AddressInAccessList") + } + + var r0 bool + if rf, ok := ret.Get(0).(func(common.Address) bool); ok { + r0 = rf(addr) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// CreateAccount provides a mock function with given fields: _a0 +func (_m *StateDB) CreateAccount(_a0 common.Address) { + _m.Called(_a0) +} + +// CreateContract provides a mock function with given fields: _a0 +func (_m *StateDB) CreateContract(_a0 common.Address) { + _m.Called(_a0) +} + +// Empty provides a mock function with given fields: _a0 +func (_m *StateDB) Empty(_a0 common.Address) bool { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for Empty") + } + + var r0 bool + if rf, ok := ret.Get(0).(func(common.Address) bool); ok { + r0 = rf(_a0) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// Exist provides a mock function with given fields: _a0 +func (_m *StateDB) Exist(_a0 common.Address) bool { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for Exist") + } + + var r0 bool + if rf, ok := ret.Get(0).(func(common.Address) bool); ok { + r0 = rf(_a0) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// Finalise provides a mock function with given fields: _a0 +func (_m *StateDB) Finalise(_a0 bool) { + _m.Called(_a0) +} + +// GetBalance provides a mock function with given fields: _a0 +func (_m *StateDB) GetBalance(_a0 common.Address) *uint256.Int { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for GetBalance") + } + + var r0 *uint256.Int + if rf, ok := ret.Get(0).(func(common.Address) *uint256.Int); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*uint256.Int) + } + } + + return r0 +} + +// GetCode provides a mock function with given fields: _a0 +func (_m *StateDB) GetCode(_a0 common.Address) []byte { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for GetCode") + } + + var r0 []byte + if rf, ok := ret.Get(0).(func(common.Address) []byte); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]byte) + } + } + + return r0 +} + +// GetCodeHash provides a mock function with given fields: _a0 +func (_m *StateDB) GetCodeHash(_a0 common.Address) common.Hash { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for GetCodeHash") + } + + var r0 common.Hash + if rf, ok := ret.Get(0).(func(common.Address) common.Hash); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(common.Hash) + } + } + + return r0 +} + +// GetCodeSize provides a mock function with given fields: _a0 +func (_m *StateDB) GetCodeSize(_a0 common.Address) int { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for GetCodeSize") + } + + var r0 int + if rf, ok := ret.Get(0).(func(common.Address) int); ok { + r0 = rf(_a0) + } else { + r0 = ret.Get(0).(int) + } + + return r0 +} + +// GetNonce provides a mock function with given fields: _a0 +func (_m *StateDB) GetNonce(_a0 common.Address) uint64 { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for GetNonce") + } + + var r0 uint64 + if rf, ok := ret.Get(0).(func(common.Address) uint64); ok { + r0 = rf(_a0) + } else { + r0 = ret.Get(0).(uint64) + } + + return r0 +} + +// GetRefund provides a mock function with no fields +func (_m *StateDB) GetRefund() uint64 { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for GetRefund") + } + + var r0 uint64 + if rf, ok := ret.Get(0).(func() uint64); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(uint64) + } + + return r0 +} + +// GetState provides a mock function with given fields: _a0, _a1 +func (_m *StateDB) GetState(_a0 common.Address, _a1 common.Hash) common.Hash { + ret := _m.Called(_a0, _a1) + + if len(ret) == 0 { + panic("no return value specified for GetState") + } + + var r0 common.Hash + if rf, ok := ret.Get(0).(func(common.Address, common.Hash) common.Hash); ok { + r0 = rf(_a0, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(common.Hash) + } + } + + return r0 +} + +// GetStateAndCommittedState provides a mock function with given fields: _a0, _a1 +func (_m *StateDB) GetStateAndCommittedState(_a0 common.Address, _a1 common.Hash) (common.Hash, common.Hash) { + ret := _m.Called(_a0, _a1) + + if len(ret) == 0 { + panic("no return value specified for GetStateAndCommittedState") + } + + var r0 common.Hash + var r1 common.Hash + if rf, ok := ret.Get(0).(func(common.Address, common.Hash) (common.Hash, common.Hash)); ok { + return rf(_a0, _a1) + } + if rf, ok := ret.Get(0).(func(common.Address, common.Hash) common.Hash); ok { + r0 = rf(_a0, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(common.Hash) + } + } + + if rf, ok := ret.Get(1).(func(common.Address, common.Hash) common.Hash); ok { + r1 = rf(_a0, _a1) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(common.Hash) + } + } + + return r0, r1 +} + +// GetStorageRoot provides a mock function with given fields: addr +func (_m *StateDB) GetStorageRoot(addr common.Address) common.Hash { + ret := _m.Called(addr) + + if len(ret) == 0 { + panic("no return value specified for GetStorageRoot") + } + + var r0 common.Hash + if rf, ok := ret.Get(0).(func(common.Address) common.Hash); ok { + r0 = rf(addr) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(common.Hash) + } + } + + return r0 +} + +// GetTransientState provides a mock function with given fields: addr, key +func (_m *StateDB) GetTransientState(addr common.Address, key common.Hash) common.Hash { + ret := _m.Called(addr, key) + + if len(ret) == 0 { + panic("no return value specified for GetTransientState") + } + + var r0 common.Hash + if rf, ok := ret.Get(0).(func(common.Address, common.Hash) common.Hash); ok { + r0 = rf(addr, key) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(common.Hash) + } + } + + return r0 +} + +// HasSelfDestructed provides a mock function with given fields: _a0 +func (_m *StateDB) HasSelfDestructed(_a0 common.Address) bool { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for HasSelfDestructed") + } + + var r0 bool + if rf, ok := ret.Get(0).(func(common.Address) bool); ok { + r0 = rf(_a0) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// IsStorageEmpty provides a mock function with given fields: addr +func (_m *StateDB) IsStorageEmpty(addr common.Address) bool { + ret := _m.Called(addr) + + if len(ret) == 0 { + panic("no return value specified for IsStorageEmpty") + } + + var r0 bool + if rf, ok := ret.Get(0).(func(common.Address) bool); ok { + r0 = rf(addr) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// PointCache provides a mock function with no fields +func (_m *StateDB) PointCache() *utils.PointCache { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for PointCache") + } + + var r0 *utils.PointCache + if rf, ok := ret.Get(0).(func() *utils.PointCache); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*utils.PointCache) + } + } + + return r0 +} + +// Prepare provides a mock function with given fields: rules, sender, coinbase, dest, precompiles, txAccesses +func (_m *StateDB) Prepare(rules params.Rules, sender common.Address, coinbase common.Address, dest *common.Address, precompiles []common.Address, txAccesses types.AccessList) { + _m.Called(rules, sender, coinbase, dest, precompiles, txAccesses) +} + +// RevertToSnapshot provides a mock function with given fields: _a0 +func (_m *StateDB) RevertToSnapshot(_a0 int) { + _m.Called(_a0) +} + +// SelfDestruct provides a mock function with given fields: _a0 +func (_m *StateDB) SelfDestruct(_a0 common.Address) uint256.Int { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for SelfDestruct") + } + + var r0 uint256.Int + if rf, ok := ret.Get(0).(func(common.Address) uint256.Int); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(uint256.Int) + } + } + + return r0 +} + +// SelfDestruct6780 provides a mock function with given fields: _a0 +func (_m *StateDB) SelfDestruct6780(_a0 common.Address) (uint256.Int, bool) { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for SelfDestruct6780") + } + + var r0 uint256.Int + var r1 bool + if rf, ok := ret.Get(0).(func(common.Address) (uint256.Int, bool)); ok { + return rf(_a0) + } + if rf, ok := ret.Get(0).(func(common.Address) uint256.Int); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(uint256.Int) + } + } + + if rf, ok := ret.Get(1).(func(common.Address) bool); ok { + r1 = rf(_a0) + } else { + r1 = ret.Get(1).(bool) + } + + return r0, r1 +} + +// SetCode provides a mock function with given fields: _a0, _a1 +func (_m *StateDB) SetCode(_a0 common.Address, _a1 []byte) []byte { + ret := _m.Called(_a0, _a1) + + if len(ret) == 0 { + panic("no return value specified for SetCode") + } + + var r0 []byte + if rf, ok := ret.Get(0).(func(common.Address, []byte) []byte); ok { + r0 = rf(_a0, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]byte) + } + } + + return r0 +} + +// SetNonce provides a mock function with given fields: _a0, _a1, _a2 +func (_m *StateDB) SetNonce(_a0 common.Address, _a1 uint64, _a2 tracing.NonceChangeReason) { + _m.Called(_a0, _a1, _a2) +} + +// SetState provides a mock function with given fields: _a0, _a1, _a2 +func (_m *StateDB) SetState(_a0 common.Address, _a1 common.Hash, _a2 common.Hash) common.Hash { + ret := _m.Called(_a0, _a1, _a2) + + if len(ret) == 0 { + panic("no return value specified for SetState") + } + + var r0 common.Hash + if rf, ok := ret.Get(0).(func(common.Address, common.Hash, common.Hash) common.Hash); ok { + r0 = rf(_a0, _a1, _a2) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(common.Hash) + } + } + + return r0 +} + +// SetTransientState provides a mock function with given fields: addr, key, value +func (_m *StateDB) SetTransientState(addr common.Address, key common.Hash, value common.Hash) { + _m.Called(addr, key, value) +} + +// SlotInAccessList provides a mock function with given fields: addr, slot +func (_m *StateDB) SlotInAccessList(addr common.Address, slot common.Hash) (bool, bool) { + ret := _m.Called(addr, slot) + + if len(ret) == 0 { + panic("no return value specified for SlotInAccessList") + } + + var r0 bool + var r1 bool + if rf, ok := ret.Get(0).(func(common.Address, common.Hash) (bool, bool)); ok { + return rf(addr, slot) + } + if rf, ok := ret.Get(0).(func(common.Address, common.Hash) bool); ok { + r0 = rf(addr, slot) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(common.Address, common.Hash) bool); ok { + r1 = rf(addr, slot) + } else { + r1 = ret.Get(1).(bool) + } + + return r0, r1 +} + +// Snapshot provides a mock function with no fields +func (_m *StateDB) Snapshot() int { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Snapshot") + } + + var r0 int + if rf, ok := ret.Get(0).(func() int); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(int) + } + + return r0 +} + +// SubBalance provides a mock function with given fields: _a0, _a1, _a2 +func (_m *StateDB) SubBalance(_a0 common.Address, _a1 *uint256.Int, _a2 tracing.BalanceChangeReason) uint256.Int { + ret := _m.Called(_a0, _a1, _a2) + + if len(ret) == 0 { + panic("no return value specified for SubBalance") + } + + var r0 uint256.Int + if rf, ok := ret.Get(0).(func(common.Address, *uint256.Int, tracing.BalanceChangeReason) uint256.Int); ok { + r0 = rf(_a0, _a1, _a2) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(uint256.Int) + } + } + + return r0 +} + +// SubRefund provides a mock function with given fields: _a0 +func (_m *StateDB) SubRefund(_a0 uint64) { + _m.Called(_a0) +} + +// Witness provides a mock function with no fields +func (_m *StateDB) Witness() *stateless.Witness { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Witness") + } + + var r0 *stateless.Witness + if rf, ok := ret.Get(0).(func() *stateless.Witness); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*stateless.Witness) + } + } + + return r0 +} + +// NewStateDB creates a new instance of StateDB. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewStateDB(t interface { + mock.TestingT + Cleanup(func()) +}) *StateDB { + mock := &StateDB{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +}