package integration_test import ( "context" "fmt" "time" "cosmossdk.io/math" cmthttp "github.com/cometbft/cometbft/rpc/client/http" cmttypes "github.com/cometbft/cometbft/types" "github.com/cosmos/cosmos-sdk/codec" codectypes "github.com/cosmos/cosmos-sdk/codec/types" cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" sdk "github.com/cosmos/cosmos-sdk/types" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" "google.golang.org/grpc" "github.com/skip-mev/chaintestutil/account" "github.com/skip-mev/chaintestutil/network" blockservicetypes "github.com/skip-mev/block-sdk/v2/block/service/types" auctiontypes "github.com/skip-mev/block-sdk/v2/x/auction/types" ) const ( free = "free" base = "base" mev = "mev" ) var cdc *codec.ProtoCodec func init() { ir := codectypes.NewInterfaceRegistry() authtypes.RegisterInterfaces(ir) cryptocodec.RegisterInterfaces(ir) cdc = codec.NewProtoCodec(ir) } // TestLanedMempool tests that the block-sdk mempool is properly synced w/ comet's mempool func (s *NetworkTestSuite) TestLanedMempoolSyncWithComet() { cc, closefn, err := s.NetworkSuite.GetGRPC() s.Require().NoError(err) defer closefn() tmClient, err := s.NetworkSuite.GetCometClient() s.Require().NoError(err) blockClient := blockservicetypes.NewServiceClient(cc) ctx, closeCtx := context.WithTimeout(context.Background(), 1*time.Minute) defer closeCtx() val := s.NetworkSuite.Network.Validators[0].ValAddress s.Require().NoError(err) acc := *s.Accounts[0] s.Run("test free-lane sync", func() { s.Run("all valid txs", func() { // create a bunch of delegation txs and check the app-mempool v. comet-mempool msg := createFreeTx(acc.Address(), val, sdk.NewCoin(sdk.DefaultBondDenom, math.NewInt(10))) s.Require().NoError(s.checkParity(ctx, tmClient, blockClient, cc, acc, free, msg)) }) s.Run("bid Verify invalidates later tx Verify", func() { // create a new account zeroAccount := account.NewAccount() status, err := tmClient.Status(ctx) s.Require().NoError(err) // initialize the account w/ enough for a single tx send := banktypes.NewMsgSend(acc.Address(), zeroAccount.Address(), sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, math.NewInt(3000100)))) seq, _, err := getAccount(ctx, authtypes.NewQueryClient(cc), acc) s.Require().NoError(err) // send the tx (pay for fees) tx, err := s.NetworkSuite.CreateTxBytes(ctx, network.TxGenInfo{ Account: acc, GasLimit: 1000000, TimeoutHeight: uint64(status.SyncInfo.LatestBlockHeight) + 1, Fee: sdk.NewCoins(sdk.NewInt64Coin(sdk.DefaultBondDenom, 1000000)), Sequence: seq, OverrideSequence: true, }, send) s.Require().NoError(err) // commit tx res, err := tmClient.BroadcastTxCommit(ctx, tx) s.Require().NoError(err) s.Require().Equal(uint32(0), res.TxResult.Code) // create a delegation tx -> this should spend fees in zeroAccount msg := banktypes.NewMsgSend(zeroAccount.Address(), acc.Address(), sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, math.NewInt(1)))) // update the balance of zeroAccount to pay for the next tx updateMsg := banktypes.NewMsgSend(acc.Address(), zeroAccount.Address(), sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, math.NewInt(1000000)))) status, err = tmClient.Status(ctx) s.Require().NoError(err) nextHeight := uint64(status.SyncInfo.LatestBlockHeight) + 1 // pay for fees of next tx for zeroAccount (account for bid sequence) tx2, err := s.NetworkSuite.CreateTxBytes(ctx, network.TxGenInfo{ Account: acc, GasLimit: 1000000, TimeoutHeight: nextHeight, Fee: sdk.NewCoins(sdk.NewInt64Coin(sdk.DefaultBondDenom, 1000000)), Sequence: seq + 2, OverrideSequence: true, }, updateMsg) s.Require().NoError(err) seq2, _, err := getAccount(ctx, authtypes.NewQueryClient(cc), *zeroAccount) s.Require().NoError(err) // spends all funds in account on fee deduction -> fees are refilled after bid txToWrap, err := s.NetworkSuite.CreateTxBytes(ctx, network.TxGenInfo{ Account: *zeroAccount, GasLimit: 1000000, TimeoutHeight: 100000000, Fee: sdk.NewCoins(sdk.NewInt64Coin(sdk.DefaultBondDenom, 3000000)), Sequence: seq2, OverrideSequence: true, }, msg) s.Require().NoError(err) // first delegate tx (just used to increment sequence) firstDelegateTx, err := s.NetworkSuite.CreateTxBytes(ctx, network.TxGenInfo{ Account: *zeroAccount, GasLimit: 1000000, TimeoutHeight: 100000000, Fee: sdk.NewCoins(sdk.NewInt64Coin(sdk.DefaultBondDenom, 1000000)), Sequence: seq2, OverrideSequence: true, }, msg) s.Require().NoError(err) // ordered after bid, and shld fail in PrepareProposal as there will be no funds to pay (will be removed from lane) secondDelegateTx, err := s.NetworkSuite.CreateTxBytes(ctx, network.TxGenInfo{ Account: *zeroAccount, GasLimit: 1000000, TimeoutHeight: 100000000, Fee: sdk.NewCoins(sdk.NewInt64Coin(sdk.DefaultBondDenom, 1000000)), Sequence: seq2 + 1, OverrideSequence: true, }, msg) s.Require().NoError(err) // create a bid wrapping firstDelegateTx, tx2 -> i.e spend funds in zeroAccount and refill bid := auctiontypes.NewMsgAuctionBid(acc.Address(), sdk.NewCoin(sdk.DefaultBondDenom, math.NewInt(1000000)), [][]byte{txToWrap, tx2}) bidTx, err := s.NetworkSuite.CreateTxBytes(ctx, network.TxGenInfo{ Account: acc, GasLimit: 1000000, TimeoutHeight: nextHeight, Fee: sdk.NewCoins(sdk.NewInt64Coin(sdk.DefaultBondDenom, 1000000)), Sequence: seq + 1, OverrideSequence: true, }, bid) s.Require().NoError(err) // broadcast txs resp, err := tmClient.BroadcastTxSync(ctx, bidTx) s.Require().NoError(err) s.Require().Equal(uint32(0), resp.Code) resp, err = tmClient.BroadcastTxSync(ctx, firstDelegateTx) s.Require().NoError(err) s.Require().Equal(uint32(0), resp.Code) resp, err = tmClient.BroadcastTxSync(ctx, secondDelegateTx) s.Require().NoError(err) s.Require().Equal(uint32(0), resp.Code) // wait for commit of bid s.Require().NoError(waitForTxCommit(ctx, tmClient, cmttypes.Tx(bidTx).Hash())) // check mempool size txs, err := tmClient.NumUnconfirmedTxs(ctx) s.Require().NoError(err) cmtTxs := uint64(txs.Total) // check app mempool size appTxDist, err := blockClient.GetTxDistribution(ctx, &blockservicetypes.GetTxDistributionRequest{}) s.Require().NoError(err) // check parity appTxs := 0 for _, tx := range appTxDist.Distribution { appTxs += int(tx) } s.Require().Equal(appTxs, int(cmtTxs)) }) }) } func createFreeTx(delegator sdk.AccAddress, validator sdk.ValAddress, amount sdk.Coin) sdk.Msg { return stakingtypes.NewMsgDelegate(delegator.String(), validator.String(), amount) } func getAccount(ctx context.Context, cc authtypes.QueryClient, acc account.Account) (uint64, uint64, error) { resp, err := cc.Account(ctx, &authtypes.QueryAccountRequest{Address: acc.Address().String()}) if err != nil { return 0, 0, err } var accI sdk.AccountI if err := cdc.UnpackAny(resp.Account, &accI); err != nil { return 0, 0, err } return accI.GetSequence(), accI.GetAccountNumber(), nil } func (s *NetworkTestSuite) checkParity( ctx context.Context, tmClient *cmthttp.HTTP, blockClient blockservicetypes.ServiceClient, cc *grpc.ClientConn, acc account.Account, lane string, msg sdk.Msg, ) error { seq, _, err := getAccount(ctx, authtypes.NewQueryClient(cc), acc) if err != nil { return err } res, err := tmClient.Status(ctx) if err != nil { return err } height := res.SyncInfo.LatestBlockHeight // send 100 txs to the app and check mempool sizes numTxs := 100 txsCh := make(chan []byte, numTxs) done := make(chan struct{}) // spin GR to wait on tx inclusions go func() { for tx := range txsCh { // check for tx inclusion waitForTxCommit(ctx, tmClient, tx) } close(done) }() for i := 0; i < numTxs; i++ { tx, err := s.NetworkSuite.CreateTxBytes(ctx, network.TxGenInfo{ Account: acc, GasLimit: 1000000, TimeoutHeight: uint64(height + 10), Fee: sdk.NewCoins(sdk.NewInt64Coin(sdk.DefaultBondDenom, 1000000)), Sequence: seq + uint64(i), OverrideSequence: true, }, msg) if err != nil { return err } res, err := tmClient.BroadcastTxSync(ctx, tx) if err != nil { return err } txsCh <- res.Hash } // all txs are sent close(txsCh) // wait for all txs to be included before checking size <-done // check comet mempool size txs, err := tmClient.NumUnconfirmedTxs(ctx) if err != nil { return err } cmtTxs := uint64(txs.Total) // check app mempool size appTxs, err := blockClient.GetTxDistribution(ctx, &blockservicetypes.GetTxDistributionRequest{}) if err != nil { return err } if cmtTxs != appTxs.Distribution[lane] { return fmt.Errorf("mempool size mismatch: %d != %d", cmtTxs, appTxs.Distribution[free]) } return nil } func waitForTxCommit(ctx context.Context, client *cmthttp.HTTP, hash []byte) error { _, err := client.Tx(ctx, hash, false) for ; err != nil; _, err = client.Tx(ctx, hash, false) { time.Sleep(time.Millisecond) } return nil }