|
4 | 4 | "context" |
5 | 5 | "crypto/ecdsa" |
6 | 6 | "errors" |
| 7 | + "fmt" |
7 | 8 | "math/big" |
8 | 9 | "strconv" |
9 | 10 | "sync" |
@@ -629,6 +630,66 @@ type testMempoolDependencies struct { |
629 | 630 | accounts []testAccount |
630 | 631 | } |
631 | 632 |
|
| 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 | + |
632 | 693 | func setupMempoolWithAccounts(t *testing.T, numAccounts int) (*mempool.Mempool, testMempoolDependencies) { |
633 | 694 | t.Helper() |
634 | 695 |
|
@@ -795,6 +856,173 @@ func createMsgEthereumTx( |
795 | 856 | return cosmosTx |
796 | 857 | } |
797 | 858 |
|
| 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 | + |
798 | 1026 | // decodeTxBytes decodes transaction bytes returned from ReapNewValidTxs back into an Ethereum transaction |
799 | 1027 | func decodeTxBytes(t *testing.T, txConfig client.TxConfig, txBytes []byte) *types.Transaction { |
800 | 1028 | t.Helper() |
|
0 commit comments