Skip to content

Commit 6e6a34e

Browse files
authored
Merge branch 'main' into ma/evm-rechecking-alignment
2 parents 7a48aad + 65e0977 commit 6e6a34e

11 files changed

Lines changed: 1190 additions & 178 deletions

File tree

Makefile

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -374,17 +374,17 @@ test-rpc-compat-stop:
374374

375375
.PHONY: localnet-start localnet-stop localnet-build-env localnet-build-nodes test-rpc-compat test-rpc-compat-stop mocks
376376

377-
test-system: build-v05 build
377+
test-system: build-v06 build
378378
mkdir -p ./tests/systemtests/binaries/
379379
cp $(BUILDDIR)/evmd ./tests/systemtests/binaries/
380380
cd tests/systemtests/Counter && forge build
381381
$(MAKE) -C tests/systemtests test
382382

383-
build-v05:
384-
mkdir -p ./tests/systemtests/binaries/v0.5
385-
git checkout v0.5.1
383+
build-v06:
384+
mkdir -p ./tests/systemtests/binaries/v0.6
385+
git checkout v0.6.0
386386
make build
387-
cp $(BUILDDIR)/evmd ./tests/systemtests/binaries/v0.5
387+
cp $(BUILDDIR)/evmd ./tests/systemtests/binaries/v0.6
388388
git checkout -
389389

390390
mocks:

docs/migrations/v0.5.0_to_v0.6.0.md

Lines changed: 0 additions & 55 deletions
This file was deleted.

docs/migrations/v0.6.x_to_v0.7.0.md

Lines changed: 728 additions & 0 deletions
Large diffs are not rendered by default.

evmd/upgrades.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@ import (
1010
)
1111

1212
// UpgradeName defines the on-chain upgrade name for the sample EVMD upgrade
13-
// from v0.5.0 to v0.6.0.
13+
// from v0.6.0 to v0.7.0.
1414
//
1515
// NOTE: This upgrade defines a reference implementation of what an upgrade
1616
// could look like when an application is migrating from EVMD version
17-
// v0.4.0 to v0.5.x
18-
const UpgradeName = "v0.5.0-to-v0.6.0"
17+
// v0.6.x to v0.7.0.
18+
const UpgradeName = "v0.6.0-to-v0.7.0"
1919

2020
func (app EVMD) RegisterUpgradeHandlers() {
2121
app.UpgradeKeeper.SetUpgradeHandler(

mempool/mempool_test.go

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"crypto/ecdsa"
66
"errors"
7+
"fmt"
78
"math/big"
89
"strconv"
910
"sync"
@@ -629,6 +630,66 @@ type testMempoolDependencies struct {
629630
accounts []testAccount
630631
}
631632

633+
// AdvanceNonce updates the mock vmKeeper to return newNonce for GetNonce(addr)
634+
// and a matching Account from GetAccount. Used to simulate chain-state advance
635+
// between blocks so legacypool's reactive reset path can observe the change.
636+
//
637+
// Assumes prior GetNonce/GetAccount expectations were registered with a
638+
// concrete common.Address argument (as setupMempool does). Expectations
639+
// registered with mock.Anything or other non-Address matchers would not match
640+
// the type assertion below and would survive — leaving stale entries that race
641+
// with the new ones.
642+
func (s *testMempoolDependencies) AdvanceNonce(t *testing.T, addr common.Address, newNonce uint64) {
643+
t.Helper()
644+
var toUnset []*mock.Call
645+
for _, c := range s.vmKeeper.ExpectedCalls {
646+
switch c.Method {
647+
case "GetNonce":
648+
if len(c.Arguments) >= 1 {
649+
if a, ok := c.Arguments[0].(common.Address); ok && a == addr {
650+
toUnset = append(toUnset, c)
651+
}
652+
}
653+
case "GetAccount":
654+
if len(c.Arguments) >= 2 {
655+
if a, ok := c.Arguments[1].(common.Address); ok && a == addr {
656+
toUnset = append(toUnset, c)
657+
}
658+
}
659+
}
660+
}
661+
for _, c := range toUnset {
662+
c.Unset() // acquires the mock's internal mutex
663+
}
664+
s.vmKeeper.On("GetNonce", addr).Return(newNonce).Maybe()
665+
s.vmKeeper.On("GetAccount", mock.Anything, addr).Return(&statedb.Account{
666+
Nonce: newNonce,
667+
Balance: uint256.NewInt(1e18),
668+
}).Maybe()
669+
}
670+
671+
// InstallNonceCheckingRechecker wires the EVM rechecker to reject txs whose
672+
// nonce is below the mock's GetAccount(sender).Nonce. Simulates the ante
673+
// handler's sequence check during reset's demoteUnexecutables.
674+
func (s *testMempoolDependencies) InstallNonceCheckingRechecker(t *testing.T) {
675+
t.Helper()
676+
signer := types.LatestSignerForChainID(big.NewInt(int64(constants.EighteenDecimalsChainID)))
677+
s.evmRechecker.SetEVMRecheck(func(ctx sdk.Context, tx *types.Transaction) (sdk.Context, error) {
678+
from, err := types.Sender(signer, tx)
679+
if err != nil {
680+
return ctx, nil
681+
}
682+
acc := s.vmKeeper.GetAccount(sdk.Context{}, from)
683+
if acc == nil {
684+
return ctx, nil
685+
}
686+
if tx.Nonce() < acc.Nonce {
687+
return sdk.Context{}, fmt.Errorf("stale tx: nonce %d < chain %d", tx.Nonce(), acc.Nonce)
688+
}
689+
return ctx, nil
690+
})
691+
}
692+
632693
func setupMempoolWithAccounts(t *testing.T, numAccounts int) (*mempool.Mempool, testMempoolDependencies) {
633694
t.Helper()
634695

@@ -795,6 +856,173 @@ func createMsgEthereumTx(
795856
return cosmosTx
796857
}
797858

859+
// createMsgEthereum7702Tx builds a cosmos-wrapped MsgEthereumTx of type-04
860+
// (EIP-7702 SetCode) signed by senderKey with the supplied authorization
861+
// list. Each `auths` entry is signed by its `key` field with the embedded
862+
// nonce. Returns the cosmos-signed sdk.Tx and the underlying ethereum tx
863+
// for assertion convenience.
864+
func createMsgEthereum7702Tx(
865+
t *testing.T,
866+
txConfig client.TxConfig,
867+
senderKey *ecdsa.PrivateKey,
868+
txNonce uint64,
869+
auths []types.SetCodeAuthorization,
870+
) sdk.Tx {
871+
t.Helper()
872+
873+
chainID := vmtypes.GetEthChainConfig().ChainID
874+
to := common.Address{0x55}
875+
signedTx := types.MustSignNewTx(senderKey, types.LatestSignerForChainID(chainID), &types.SetCodeTx{
876+
ChainID: uint256.MustFromBig(chainID),
877+
Nonce: txNonce,
878+
GasTipCap: uint256.NewInt(1),
879+
GasFeeCap: uint256.NewInt(1e9),
880+
Gas: 250000,
881+
To: to,
882+
Value: uint256.NewInt(0),
883+
AuthList: auths,
884+
})
885+
886+
msg := &vmtypes.MsgEthereumTx{
887+
Raw: vmtypes.EthereumTx{Transaction: signedTx},
888+
From: crypto.PubkeyToAddress(senderKey.PublicKey).Bytes(),
889+
}
890+
891+
// priv=nil → PrepareEthTx skips re-signing (we already have a signed eth
892+
// tx) and just wraps the message into a cosmos tx with the EVM extension
893+
// option.
894+
cosmosTx, err := evmtestutiltx.PrepareEthTx(txConfig, nil, msg)
895+
require.NoError(t, err)
896+
return cosmosTx
897+
}
898+
899+
// signSetCodeAuth is a small wrapper around types.SignSetCode for tests.
900+
func signSetCodeAuth(t *testing.T, key *ecdsa.PrivateKey, nonce uint64) types.SetCodeAuthorization {
901+
t.Helper()
902+
chainID := vmtypes.GetEthChainConfig().ChainID
903+
auth, err := types.SignSetCode(key, types.SetCodeAuthorization{
904+
ChainID: *uint256.MustFromBig(chainID),
905+
Address: common.Address{0x42},
906+
Nonce: nonce,
907+
})
908+
require.NoError(t, err)
909+
return auth
910+
}
911+
912+
// TestEvictsStaleTx covers eviction of stale txs via demoteUnexecutables →
913+
// RecheckEVM during reset, across three scenarios where the eager mechanism
914+
// can't or doesn't catch the chain advance.
915+
func TestEvictsStaleTx(t *testing.T) {
916+
type scenario struct {
917+
name string
918+
numAccounts int
919+
targetIdx int // account whose pool should drain
920+
seedNonces []uint64 // nonces to pre-seed for accounts[targetIdx]
921+
finalize7702 func(t *testing.T, mp *mempool.Mempool, txConfig client.TxConfig, accs []testAccount)
922+
advanceTo uint64 // chain nonce to set for accounts[targetIdx]
923+
// Expected eager-cache state for accounts[targetIdx] AFTER the test:
924+
// expectsEager=false when no SetLatestNonce was triggered for the target;
925+
// expectsEager=true with eagerNonce set when the eager path fired.
926+
expectsEager bool
927+
eagerNonce uint64
928+
}
929+
930+
cases := []scenario{
931+
{
932+
name: "stale-sender-no-7702",
933+
numAccounts: 1,
934+
targetIdx: 0,
935+
seedNonces: []uint64{0},
936+
advanceTo: 1,
937+
// no RemoveWithReason call → eager cache untouched.
938+
expectsEager: false,
939+
},
940+
{
941+
name: "authority-after-cross-account-7702",
942+
numAccounts: 2,
943+
targetIdx: 1,
944+
seedNonces: []uint64{0},
945+
finalize7702: func(t *testing.T, mp *mempool.Mempool, txConfig client.TxConfig, accs []testAccount) {
946+
t.Helper()
947+
auths := []types.SetCodeAuthorization{signSetCodeAuth(t, accs[1].key, 1)}
948+
cosmosTx := createMsgEthereum7702Tx(t, txConfig, accs[0].key, 0, auths)
949+
require.NoError(t, mp.RemoveWithReason(context.Background(), cosmosTx, mempooltypes.RemoveReason{
950+
Caller: mempooltypes.CallerRunTxFinalize,
951+
}))
952+
},
953+
advanceTo: 1,
954+
// Eager fires for sender (idx 0); authority (idx 1, the target) is invisible.
955+
expectsEager: false,
956+
},
957+
{
958+
name: "sender-+1-gap-after-self-sponsored-7702",
959+
numAccounts: 1,
960+
targetIdx: 0,
961+
seedNonces: []uint64{0, 1},
962+
finalize7702: func(t *testing.T, mp *mempool.Mempool, txConfig client.TxConfig, accs []testAccount) {
963+
t.Helper()
964+
auths := []types.SetCodeAuthorization{signSetCodeAuth(t, accs[0].key, 1)}
965+
cosmosTx := createMsgEthereum7702Tx(t, txConfig, accs[0].key, 0, auths)
966+
require.NoError(t, mp.RemoveWithReason(context.Background(), cosmosTx, mempooltypes.RemoveReason{
967+
Caller: mempooltypes.CallerRunTxFinalize,
968+
}))
969+
},
970+
advanceTo: 2,
971+
// Eager fires for sender (== target) at tx.Nonce()=0; the +1 bump is reactive.
972+
expectsEager: true,
973+
eagerNonce: 0,
974+
},
975+
}
976+
977+
for _, tc := range cases {
978+
t.Run(tc.name, func(t *testing.T) {
979+
mp, s := setupMempoolWithAccounts(t, tc.numAccounts)
980+
txConfig, bus, accounts := s.txConfig, s.eventBus, s.accounts
981+
target := accounts[tc.targetIdx]
982+
983+
require.NoError(t, bus.PublishEventNewBlockHeader(cmttypes.EventDataNewBlockHeader{
984+
Header: cmttypes.Header{Height: 1, Time: time.Now(), ChainID: strconv.Itoa(constants.EighteenDecimalsChainID)},
985+
}))
986+
require.NoError(t, mp.GetTxPool().Sync())
987+
988+
for _, n := range tc.seedNonces {
989+
tx := createMsgEthereumTx(t, txConfig, target.key, n, big.NewInt(1e8))
990+
require.NoError(t, mp.Insert(context.Background(), tx))
991+
}
992+
require.NoError(t, mp.GetTxPool().Sync())
993+
994+
legacyPool := mp.GetTxPool().Subpools[0].(*legacypool.LegacyPool)
995+
pending, _ := legacyPool.ContentFrom(target.address)
996+
require.Len(t, pending, len(tc.seedNonces))
997+
998+
if tc.finalize7702 != nil {
999+
tc.finalize7702(t, mp, txConfig, accounts)
1000+
}
1001+
1002+
s.InstallNonceCheckingRechecker(t)
1003+
s.AdvanceNonce(t, target.address, tc.advanceTo)
1004+
1005+
require.NoError(t, bus.PublishEventNewBlockHeader(cmttypes.EventDataNewBlockHeader{
1006+
Header: cmttypes.Header{Height: 2, Time: time.Now(), ChainID: strconv.Itoa(constants.EighteenDecimalsChainID)},
1007+
}))
1008+
require.NoError(t, mp.GetTxPool().Sync())
1009+
1010+
require.Eventually(t, func() bool {
1011+
p, q := legacyPool.ContentFrom(target.address)
1012+
return len(p) == 0 && len(q) == 0
1013+
}, time.Second, 10*time.Millisecond, "reset must evict stale tx")
1014+
1015+
got, ok := legacyPool.LatestNonce(target.address)
1016+
if tc.expectsEager {
1017+
require.True(t, ok, "eager cache should have an entry for target")
1018+
require.Equal(t, tc.eagerNonce, got)
1019+
} else {
1020+
require.False(t, ok, "no eager signal expected; eviction proves reactive path")
1021+
}
1022+
})
1023+
}
1024+
}
1025+
7981026
// decodeTxBytes decodes transaction bytes returned from ReapNewValidTxs back into an Ethereum transaction
7991027
func decodeTxBytes(t *testing.T, txConfig client.TxConfig, txBytes []byte) *types.Transaction {
8001028
t.Helper()

mempool/recheck_pool.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,8 @@ func (m *RecheckMempool) markTxRechecked(txn sdk.Tx) {
515515
// a higher nonce is dropped and rebuilt by the next recheck.
516516
func (m *RecheckMempool) markTxInserted(txn sdk.Tx) {
517517
m.recheckedTxs.Do(func(store *CosmosTxStore) {
518+
// If we invalidate any txs we can't execute this txn's antehandler sequence until the next rechecker.Update.
519+
// This is because the invalidated txns have written their state to the Store's cache context already.
518520
if store.InvalidateFrom(txn) > 0 {
519521
return
520522
}

mempool/rechecker.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,11 @@ func (r *TxRechecker) Update(ctx sdk.Context, header *ethtypes.Header) {
8888
cached = cached.WithBlockGasMeter(storetypes.NewGasMeter(header.GasLimit))
8989
cached = cached.WithGasMeter(storetypes.NewInfiniteGasMeter())
9090
if cached.ConsensusParams().Block == nil {
91-
// set the latest blocks gas limit as the max gas in cp. this is
92-
// necessary to validate each tx's gas wanted
91+
// On freshly-initialized contexts Block can be nil e.g.
92+
// * the Start path before consensus params are populated
93+
// * the EVM-side path where the ctx didn't come from CometBFT
94+
// That would result in either dereference-panic or skipping the gas-wanted check during antehandler.
95+
// Set the latest blocks gas limit as the max gas in cp to avoid this.
9396
maxGas, err := utils.SafeInt64(header.GasLimit)
9497
if err != nil {
9598
panic(fmt.Errorf("converting evm block gas limit to int64: %w", err))

mempool/signer.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,13 @@ func NewEthSignerExtractionAdapter(fallback mempool.SignerExtractionAdapter) Eth
2121
return EthSignerExtractionAdapter{fallback}
2222
}
2323

24-
// GetSigners implements the Adapter interface
25-
// NOTE: only the first item is used by the mempool
24+
// GetSigners returns the EVM tx sender. EIP-7702 authorities are NOT enumerated:
25+
// whether each authorization succeeds on chain (and therefore actually bumps
26+
// the authority's nonce) is unverifiable without chain state — a failed
27+
// authorization does not increment the nonce. Eagerly reporting auth.Nonce
28+
// would falsely evict legitimate pending txs from authorities whose auths
29+
// failed. Async reset catches authority nonce advances instead.
30+
// Non-EVM cosmos txs fall through to the SDK default.
2631
func (s EthSignerExtractionAdapter) GetSigners(tx sdk.Tx) ([]mempool.SignerData, error) {
2732
if txWithExtensions, ok := tx.(authante.HasExtensionOptionsTx); ok {
2833
opts := txWithExtensions.GetExtensionOptions()

mempool/txpool/legacypool/legacypool.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -555,6 +555,13 @@ func (pool *LegacyPool) SetLatestNonce(sender common.Address, nonce uint64) {
555555
}
556556
}
557557

558+
// LatestNonce returns the most recently recorded latest-included nonce for
559+
// addr and whether an entry exists in the cache. Primarily useful for tests
560+
// and debugging.
561+
func (pool *LegacyPool) LatestNonce(addr common.Address) (uint64, bool) {
562+
return pool.latestIncludedNonce.Get(addr)
563+
}
564+
558565
// removeOlds removes txs that have been scheduled for removals from
559566
// list l for sender addr. Returns the txs successfully removed.
560567
func (pool *LegacyPool) removeOlds(addr common.Address, l *list, poolType PoolType) types.Transactions {

0 commit comments

Comments
 (0)