diff --git a/app/app.go b/app/app.go index df5b6f3a..eeeca13b 100644 --- a/app/app.go +++ b/app/app.go @@ -1,6 +1,7 @@ package app import ( + "github.com/cosmos/cosmos-sdk/types/mempool" "io" "os" "path/filepath" @@ -335,6 +336,14 @@ func New( app.App = appBuilder.Build(db, traceStore, baseAppOptions...) + // Set the priority proposal handler + // We use a no-op mempool which means we rely on the CometBFT default transaction ordering (FIFO) + noOpMempool := mempool.NoOpMempool{} + app.App.BaseApp.SetMempool(noOpMempool) + defaultProposalHandler := baseapp.NewDefaultProposalHandler(noOpMempool, app.App.BaseApp) + proposalHandler := NewPriorityProposalHandler(defaultProposalHandler.PrepareProposalHandler(), app.txConfig.TxDecoder()) + app.App.BaseApp.SetPrepareProposal(proposalHandler.PrepareProposal()) + // Register legacy modules app.registerIBCModules() diff --git a/app/proposal_handler.go b/app/proposal_handler.go new file mode 100644 index 00000000..f76ab5a0 --- /dev/null +++ b/app/proposal_handler.go @@ -0,0 +1,72 @@ +package app + +import ( + bundlestypes "github.com/KYVENetwork/chain/x/bundles/types" + abci "github.com/cometbft/cometbft/abci/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "reflect" + "slices" +) + +type PriorityProposalHandler struct { + defaultHandler sdk.PrepareProposalHandler + txDecoder sdk.TxDecoder +} + +func NewPriorityProposalHandler(defaultHandler sdk.PrepareProposalHandler, decoder sdk.TxDecoder) *PriorityProposalHandler { + return &PriorityProposalHandler{ + defaultHandler: defaultHandler, + txDecoder: decoder, + } +} + +var priorityTypes = []string{ + reflect.TypeOf(bundlestypes.MsgSubmitBundleProposal{}).Name(), + reflect.TypeOf(bundlestypes.MsgVoteBundleProposal{}).Name(), + reflect.TypeOf(bundlestypes.MsgClaimUploaderRole{}).Name(), + reflect.TypeOf(bundlestypes.MsgSkipUploaderRole{}).Name(), +} + +// PrepareProposal returns a PrepareProposalHandler that separates transactions into different queues +// This function is only called by the block proposer and therefore does NOT need to be deterministic +func (h *PriorityProposalHandler) PrepareProposal() sdk.PrepareProposalHandler { + return func(ctx sdk.Context, req *abci.RequestPrepareProposal) (*abci.ResponsePrepareProposal, error) { + // Separate the transactions into different queues + // priorityQueue: transactions that should be executed before the default transactions + // defaultQueue: transactions that should be executed last + + var priorityQueue [][]byte + var defaultQueue [][]byte + + // Iterate through the transactions and separate them into different queues + for _, rawTx := range req.Txs { + tx, err := h.txDecoder(rawTx) + if err != nil { + return nil, err + } + msgs, err := tx.GetMsgsV2() + if err != nil { + return nil, err + } + + // We only care about transactions with a single message + if len(msgs) == 1 { + msg := msgs[0] + msgType := string(msg.ProtoReflect().Type().Descriptor().Name()) + + if slices.Contains(priorityTypes, msgType) { + priorityQueue = append(priorityQueue, rawTx) + continue + } + } + + // Otherwise, add the tx to the default queue + defaultQueue = append(defaultQueue, rawTx) + } + + // Append the transactions in the correct order + req.Txs = append(priorityQueue, defaultQueue...) + + return h.defaultHandler(ctx, req) + } +} diff --git a/interchaintest/proposal_handler/proposal_handler_test.go b/interchaintest/proposal_handler/proposal_handler_test.go new file mode 100644 index 00000000..16f3cacb --- /dev/null +++ b/interchaintest/proposal_handler/proposal_handler_test.go @@ -0,0 +1,204 @@ +package proposal_handler_test + +import ( + "context" + "cosmossdk.io/math" + bundlestypes "github.com/KYVENetwork/chain/x/bundles/types" + stakerstypes "github.com/KYVENetwork/chain/x/stakers/types" + sdkclient "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/client/tx" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/strangelove-ventures/interchaintest/v8" + "github.com/strangelove-ventures/interchaintest/v8/testutil" + "reflect" + "testing" + + "github.com/strangelove-ventures/interchaintest/v8/chain/cosmos" + "go.uber.org/zap/zaptest" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +/* + +TEST CASES - proposal_handler.go + +* Execute multiple transactions and check their order +* Execute transactions that exceed max tx bytes + +*/ + +func TestProposalHandler(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "interchaintest/ProposalHandler Test Suite") +} + +var _ = Describe("proposal_handler.go", Ordered, func() { + var chain *cosmos.CosmosChain + + var ctx context.Context + var interchain *interchaintest.Interchain + + var broadcaster *cosmos.Broadcaster + + BeforeAll(func() { + numFullNodes := 0 + numValidators := 2 + factory := interchaintest.NewBuiltinChainFactory( + zaptest.NewLogger(GinkgoT()), + []*interchaintest.ChainSpec{mainnetChainSpec(numValidators, numFullNodes)}, + ) + + chains, err := factory.Chains(GinkgoT().Name()) + Expect(err).To(BeNil()) + chain = chains[0].(*cosmos.CosmosChain) + + interchain = interchaintest.NewInterchain(). + AddChain(chain) + + broadcaster = cosmos.NewBroadcaster(GinkgoT(), chain) + broadcaster.ConfigureClientContextOptions(func(clientContext sdkclient.Context) sdkclient.Context { + return clientContext. + WithBroadcastMode(flags.BroadcastAsync) + }) + broadcaster.ConfigureFactoryOptions(func(factory tx.Factory) tx.Factory { + return factory. + WithGas(flags.DefaultGasLimit * 10) + }) + + ctx = context.Background() + client, network := interchaintest.DockerSetup(GinkgoT()) + + err = interchain.Build(ctx, nil, interchaintest.InterchainBuildOptions{ + TestName: GinkgoT().Name(), + Client: client, + NetworkID: network, + SkipPathCreation: true, + }) + Expect(err).To(BeNil()) + }) + + AfterAll(func() { + _ = chain.StopAllNodes(ctx) + _ = interchain.Close() + }) + + It("Execute multiple transactions and check their order", func() { + // ARRANGE + var wallets []*cosmos.CosmosWallet + for i := 0; i < 8; i++ { + wallets = append(wallets, interchaintest.GetAndFundTestUsers( + GinkgoT(), ctx, GinkgoT().Name(), math.NewInt(10_000_000_000), chain, + )[0].(*cosmos.CosmosWallet)) + } + + err := testutil.WaitForBlocks(ctx, 1, chain) + Expect(err).To(BeNil()) + + height, err := chain.Height(ctx) + Expect(err).To(BeNil()) + + // ACT + + // Execute different transactions + // We don't care about the results, they only have to be included in a block + broadcastMsgs(ctx, broadcaster, wallets[0], &banktypes.MsgSend{FromAddress: wallets[0].FormattedAddress()}) + broadcastMsgs(ctx, broadcaster, wallets[1], &stakerstypes.MsgCreateStaker{Creator: wallets[1].FormattedAddress()}) + broadcastMsgs(ctx, broadcaster, wallets[2], &bundlestypes.MsgClaimUploaderRole{Creator: wallets[2].FormattedAddress()}) // priority msg + broadcastMsgs(ctx, broadcaster, wallets[3], &stakerstypes.MsgJoinPool{Creator: wallets[3].FormattedAddress(), Valaddress: wallets[0].FormattedAddress(), PoolId: 0}) + broadcastMsgs(ctx, broadcaster, wallets[4], &banktypes.MsgSend{FromAddress: wallets[4].FormattedAddress()}) + broadcastMsgs(ctx, broadcaster, wallets[5], &bundlestypes.MsgVoteBundleProposal{Creator: wallets[5].FormattedAddress()}) // priority msg + broadcastMsgs(ctx, broadcaster, wallets[6], &bundlestypes.MsgSkipUploaderRole{Creator: wallets[6].FormattedAddress()}) // priority msg + broadcastMsgs(ctx, broadcaster, wallets[7], &bundlestypes.MsgSubmitBundleProposal{Creator: wallets[7].FormattedAddress()}) // priority msg + + expectedOrder := []string{ + // priority msgs + reflect.TypeOf(bundlestypes.MsgClaimUploaderRole{}).Name(), + reflect.TypeOf(bundlestypes.MsgVoteBundleProposal{}).Name(), + reflect.TypeOf(bundlestypes.MsgSkipUploaderRole{}).Name(), + reflect.TypeOf(bundlestypes.MsgSubmitBundleProposal{}).Name(), + // default msgs + reflect.TypeOf(banktypes.MsgSend{}).Name(), + reflect.TypeOf(stakerstypes.MsgCreateStaker{}).Name(), + reflect.TypeOf(stakerstypes.MsgJoinPool{}).Name(), + reflect.TypeOf(banktypes.MsgSend{}).Name(), + } + + afterHeight, err := chain.Height(ctx) + Expect(err).To(BeNil()) + Expect(afterHeight).To(Equal(height)) + + // Wait for the transactions to be included in a block + err = testutil.WaitForBlocks(ctx, 2, chain) + Expect(err).To(BeNil()) + + // ASSERT + + // Check the order of the transactions + checkTxsOrder(ctx, chain, height+1, expectedOrder) + }) + + It("Execute transactions that exceed max tx bytes", func() { + // ARRANGE + var wallets []*cosmos.CosmosWallet + for i := 0; i < 6; i++ { + wallets = append(wallets, interchaintest.GetAndFundTestUsers( + GinkgoT(), ctx, GinkgoT().Name(), math.NewInt(10_000_000_000), chain, + )[0].(*cosmos.CosmosWallet)) + } + err := testutil.WaitForBlocks(ctx, 1, chain) + Expect(err).To(BeNil()) + + height, err := chain.Height(ctx) + Expect(err).To(BeNil()) + + // ACT + const duplications = 40 + broadcastMsgs(ctx, broadcaster, wallets[0], &stakerstypes.MsgCreateStaker{Creator: wallets[0].FormattedAddress()}) + broadcastMsgs(ctx, broadcaster, wallets[1], duplicateMsg(&banktypes.MsgSend{FromAddress: wallets[1].FormattedAddress()}, duplications)...) + broadcastMsgs(ctx, broadcaster, wallets[2], &bundlestypes.MsgSkipUploaderRole{Creator: wallets[2].FormattedAddress()}) // priority msg + + // this will not make it into the actual block, so it goes into the next one with all following msgs + broadcastMsgs(ctx, broadcaster, wallets[3], duplicateMsg(&banktypes.MsgSend{FromAddress: wallets[3].FormattedAddress()}, duplications)...) + broadcastMsgs(ctx, broadcaster, wallets[4], &stakerstypes.MsgJoinPool{Creator: wallets[4].FormattedAddress(), Valaddress: wallets[0].FormattedAddress(), PoolId: 0}) + broadcastMsgs(ctx, broadcaster, wallets[5], &bundlestypes.MsgVoteBundleProposal{Creator: wallets[5].FormattedAddress()}) // priority msg + + afterHeight, err := chain.Height(ctx) + Expect(err).To(BeNil()) + Expect(afterHeight).To(Equal(height)) + + // Wait for the transactions to be included in a block + err = testutil.WaitForBlocks(ctx, 2, chain) + Expect(err).To(BeNil()) + + // ASSERT + var msgTypes []string + for i := 0; i < duplications; i++ { + msgTypes = append(msgTypes, reflect.TypeOf(banktypes.MsgSend{}).Name()) + } + + expectedOrder1 := append( + []string{ + reflect.TypeOf(bundlestypes.MsgSkipUploaderRole{}).Name(), // priority msg + reflect.TypeOf(stakerstypes.MsgCreateStaker{}).Name(), + }, + msgTypes..., + ) + expectedOrder2 := append( + []string{ + reflect.TypeOf(bundlestypes.MsgVoteBundleProposal{}).Name(), // priority msg + }, + msgTypes..., + ) + expectedOrder2 = append(expectedOrder2, + reflect.TypeOf(stakerstypes.MsgJoinPool{}).Name(), + ) + + // Check that only the first block contains the first transactions + checkTxsOrder(ctx, chain, height+1, expectedOrder1) + // The second block should contain the rest of the transactions + checkTxsOrder(ctx, chain, height+2, expectedOrder2) + }) +}) diff --git a/interchaintest/proposal_handler/proposal_handler_utils_test.go b/interchaintest/proposal_handler/proposal_handler_utils_test.go new file mode 100644 index 00000000..38e4e7e2 --- /dev/null +++ b/interchaintest/proposal_handler/proposal_handler_utils_test.go @@ -0,0 +1,178 @@ +package proposal_handler_test + +import ( + "context" + "cosmossdk.io/math" + "encoding/json" + "github.com/KYVENetwork/chain/app" + clienttx "github.com/cosmos/cosmos-sdk/client/tx" + sdk "github.com/cosmos/cosmos-sdk/types" + sdktestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + authtx "github.com/cosmos/cosmos-sdk/x/auth/tx" + bankTypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/icza/dyno" + . "github.com/onsi/gomega" + "github.com/strangelove-ventures/interchaintest/v8" + "github.com/strangelove-ventures/interchaintest/v8/chain/cosmos" + "github.com/strangelove-ventures/interchaintest/v8/ibc" + "github.com/strangelove-ventures/interchaintest/v8/testutil" + "strconv" + "strings" + "time" +) + +const ( + uidGid = "1025:1025" + consensusSpeed = 2 * time.Second + maxTxBytes = 5_000 +) + +func encodingConfig() *sdktestutil.TestEncodingConfig { + cfg := sdktestutil.TestEncodingConfig{} + a := app.Setup() + + cfg.Codec = a.AppCodec() + cfg.TxConfig = authtx.NewTxConfig(a.AppCodec(), authtx.DefaultSignModes) + cfg.InterfaceRegistry = a.InterfaceRegistry() + cfg.Amino = a.LegacyAmino() + + return &cfg +} + +func mainnetChainSpec(numValidators int, numFullNodes int) *interchaintest.ChainSpec { + return &interchaintest.ChainSpec{ + NumValidators: &numValidators, + NumFullNodes: &numFullNodes, + ChainConfig: ibc.ChainConfig{ + Type: "cosmos", + Name: "kyve", + ChainID: "kyve-1", + Bin: "kyved", + Bech32Prefix: "kyve", + Denom: "ukyve", + GasPrices: "0.02ukyve", + GasAdjustment: 5, + TrustingPeriod: "112h", + NoHostMount: false, + EncodingConfig: encodingConfig(), + ModifyGenesis: ModifyGenesis, + ConfigFileOverrides: configFileOverrides(), + Images: []ibc.DockerImage{{ + Repository: "kyve", + Version: "local", + UidGid: uidGid, + }}, + }, + } +} + +func configFileOverrides() testutil.Toml { + override := make(testutil.Toml) + override["config/config.toml"] = testutil.Toml{ + "consensus": testutil.Toml{ + "timeout_propose": consensusSpeed.String(), + "timeout_prevote": consensusSpeed.String(), + "timeout_precommit": consensusSpeed.String(), + "timeout_commit": consensusSpeed.String(), + }, + } + return override +} + +func ModifyGenesis(config ibc.ChainConfig, genbz []byte) ([]byte, error) { + genesis := make(map[string]interface{}) + _ = json.Unmarshal(genbz, &genesis) + + teamSupply := math.NewInt(165_000_000_000_000) + balances, _ := dyno.GetSlice(genesis, "app_state", "bank", "balances") + balances = append(balances, bankTypes.Balance{ + Address: "kyve1e29j95xmsw3zmvtrk4st8e89z5n72v7nf70ma4", + Coins: sdk.NewCoins(sdk.NewCoin(config.Denom, teamSupply)), + }) + _ = dyno.Set(genesis, balances, "app_state", "bank", "balances") + totalSupply, _ := dyno.GetSlice(genesis, "app_state", "bank", "supply") + + // update total supply + coin := totalSupply[0].(map[string]interface{}) + amountStr := coin["amount"].(string) + amount, _ := strconv.Atoi(amountStr) + totalSupply[0] = sdk.NewCoin(config.Denom, math.NewInt(int64(amount)+teamSupply.Int64())) + _ = dyno.Set(genesis, totalSupply, "app_state", "bank", "supply") + + _ = dyno.Set(genesis, math.LegacyMustNewDecFromStr("0.5"), + "app_state", "global", "params", "min_initial_deposit_ratio", + ) + + _ = dyno.Set(genesis, "20s", + "app_state", "gov", "params", "voting_period", + ) + _ = dyno.Set(genesis, "0", + "app_state", "gov", "params", "min_deposit", 0, "amount", + ) + _ = dyno.Set(genesis, config.Denom, + "app_state", "gov", "params", "min_deposit", 0, "denom", + ) + + _ = dyno.Set(genesis, "0.169600000000000000", + "app_state", "pool", "params", "protocol_inflation_share", + ) + _ = dyno.Set(genesis, "0.050000000000000000", + "app_state", "pool", "params", "pool_inflation_payout_rate", + ) + + // set the max tx bytes + _ = dyno.Set(genesis, strconv.Itoa(maxTxBytes), + "consensus", "params", "block", "max_bytes", + ) + _ = dyno.Set(genesis, strconv.Itoa(maxTxBytes), + "consensus", "params", "evidence", "max_bytes", + ) + + newGenesis, _ := json.Marshal(genesis) + return newGenesis, nil +} + +type TxData struct { + Body struct { + Messages []struct { + Type string `json:"@type"` + Creator string `json:"creator"` + } `json:"messages"` + } `json:"body"` +} + +func broadcastMsgs(ctx context.Context, broadcaster *cosmos.Broadcaster, broadcastingUser cosmos.User, msgs ...sdk.Msg) { + f, err := broadcaster.GetFactory(ctx, broadcastingUser) + ExpectWithOffset(1, err).To(BeNil()) + + cc, err := broadcaster.GetClientContext(ctx, broadcastingUser) + ExpectWithOffset(1, err).To(BeNil()) + + err = clienttx.BroadcastTx(cc, f, msgs...) + ExpectWithOffset(1, err).To(BeNil()) +} + +func duplicateMsg(msg sdk.Msg, size int) []sdk.Msg { + var msgs []sdk.Msg + for i := 0; i < size; i++ { + msgs = append(msgs, msg) + } + return msgs +} + +func checkTxsOrder(ctx context.Context, chain *cosmos.CosmosChain, height int64, expectedOrder []string) { + txs, err := chain.FindTxs(ctx, height) + ExpectWithOffset(1, err).To(BeNil()) + + var order []string + for _, tx := range txs { + var result TxData + err := json.Unmarshal(tx.Data, &result) + ExpectWithOffset(1, err).To(BeNil()) + + for _, msg := range result.Body.Messages { + order = append(order, msg.Type[strings.LastIndex(msg.Type, ".")+1:]) + } + } + ExpectWithOffset(1, order).To(Equal(expectedOrder)) +} diff --git a/interchaintest/upgrades/v1_5/upgrade_test.go b/interchaintest/upgrades/v1_5/upgrade_test.go index b0edcb01..d9457c5d 100644 --- a/interchaintest/upgrades/v1_5/upgrade_test.go +++ b/interchaintest/upgrades/v1_5/upgrade_test.go @@ -24,14 +24,23 @@ import ( . "github.com/onsi/gomega" ) +/* + +TEST CASES - upgrade.go + +* Upgrade Kaon from 1.4.0 to v1.5.0 +* Upgrade Kyve from 1.4.0 to v1.5.0 + +*/ + var UpgradeContainerVersion = "local" func TestV1P2Upgrade(t *testing.T) { RegisterFailHandler(Fail) - RunSpecs(t, fmt.Sprintf("%s Upgrade Test Suite", v1_5.UpgradeName)) + RunSpecs(t, fmt.Sprintf("interchaintest/%s Upgrade Test Suite", v1_5.UpgradeName)) } -var _ = Describe(fmt.Sprintf("%s Upgrade Tests", v1_5.UpgradeName), Ordered, func() { +var _ = Describe("upgrade.go", Ordered, func() { var kaon *cosmos.CosmosChain var kyve *cosmos.CosmosChain @@ -92,11 +101,11 @@ var _ = Describe(fmt.Sprintf("%s Upgrade Tests", v1_5.UpgradeName), Ordered, fun _ = interchain.Close() }) - It("Kaon upgrade test", func() { + It(fmt.Sprintf("Upgrade Kaon from %v to %v", previousVersion, v1_5.UpgradeName), func() { PerformUpgrade(ctx, client, kaon, kaonWallet, 10, "kaon") }) - It("Kyve upgrade test", func() { + It(fmt.Sprintf("Upgrade Kyve from %v to %v", previousVersion, v1_5.UpgradeName), func() { PerformUpgrade(ctx, client, kyve, kyveWallet, 10, "kyve") }) })