From ac53e337a9b53fc9b04443458f7ece2553be1231 Mon Sep 17 00:00:00 2001 From: Hieu Vu <72878483+hieuvubk@users.noreply.github.com> Date: Mon, 9 Sep 2024 17:34:29 +0700 Subject: [PATCH] test(server/v2/cometbft): Add abci unit tests (#21020) Co-authored-by: Julien Robert --- server/v2/cometbft/abci_test.go | 714 ++++++++++++++++++ server/v2/cometbft/go.mod | 6 +- .../v2/cometbft/internal/mock/mock_mempool.go | 19 + .../v2/cometbft/internal/mock/mock_reader.go | 65 ++ .../v2/cometbft/internal/mock/mock_store.go | 139 ++++ server/v2/stf/mock/tx.go | 26 +- simapp/v2/go.mod | 2 +- 7 files changed, 957 insertions(+), 14 deletions(-) create mode 100644 server/v2/cometbft/abci_test.go create mode 100644 server/v2/cometbft/internal/mock/mock_mempool.go create mode 100644 server/v2/cometbft/internal/mock/mock_reader.go create mode 100644 server/v2/cometbft/internal/mock/mock_store.go diff --git a/server/v2/cometbft/abci_test.go b/server/v2/cometbft/abci_test.go new file mode 100644 index 000000000000..2208894e3878 --- /dev/null +++ b/server/v2/cometbft/abci_test.go @@ -0,0 +1,714 @@ +package cometbft + +import ( + "context" + "crypto/sha256" + "io" + "strings" + "testing" + "time" + + appmodulev2 "cosmossdk.io/core/appmodule/v2" + "cosmossdk.io/core/store" + "cosmossdk.io/core/transaction" + "cosmossdk.io/log" + am "cosmossdk.io/server/v2/appmanager" + "cosmossdk.io/server/v2/cometbft/handlers" + cometmock "cosmossdk.io/server/v2/cometbft/internal/mock" + "cosmossdk.io/server/v2/cometbft/mempool" + "cosmossdk.io/server/v2/cometbft/types" + "cosmossdk.io/server/v2/stf" + "cosmossdk.io/server/v2/stf/branch" + "cosmossdk.io/server/v2/stf/mock" + abciproto "github.com/cometbft/cometbft/api/cometbft/abci/v1" + v1 "github.com/cometbft/cometbft/api/cometbft/types/v1" + + "github.com/cosmos/gogoproto/proto" + + "encoding/json" + + consensustypes "cosmossdk.io/x/consensus/types" + gogotypes "github.com/cosmos/gogoproto/types" + "github.com/stretchr/testify/require" +) + +var ( + sum = sha256.Sum256([]byte("test-hash")) + DefaulConsensusParams = &v1.ConsensusParams{ + Block: &v1.BlockParams{ + MaxGas: 5000000, + }, + } + mockTx = mock.Tx{ + Sender: []byte("sender"), + Msg: &gogotypes.BoolValue{Value: true}, + GasLimit: 100_000, + } + invalidMockTx = mock.Tx{ + Sender: []byte("sender"), + Msg: &gogotypes.BoolValue{Value: true}, + GasLimit: 0, + } + actorName = []byte("cookies") +) + +func getQueryRouterBuilder[T any, PT interface { + *T + proto.Message +}, + U any, UT interface { + *U + proto.Message + }]( + t *testing.T, + handler func(ctx context.Context, msg PT) (UT, error), +) *stf.MsgRouterBuilder { + t.Helper() + queryRouterBuilder := stf.NewMsgRouterBuilder() + err := queryRouterBuilder.RegisterHandler( + proto.MessageName(PT(new(T))), + func(ctx context.Context, msg transaction.Msg) (msgResp transaction.Msg, err error) { + typedReq := msg.(PT) + typedResp, err := handler(ctx, typedReq) + if err != nil { + return nil, err + } + + return typedResp, nil + }, + ) + require.NoError(t, err) + + return queryRouterBuilder +} + +func getMsgRouterBuilder[T any, PT interface { + *T + transaction.Msg +}, + U any, UT interface { + *U + transaction.Msg + }]( + t *testing.T, + handler func(ctx context.Context, msg PT) (UT, error), +) *stf.MsgRouterBuilder { + t.Helper() + msgRouterBuilder := stf.NewMsgRouterBuilder() + err := msgRouterBuilder.RegisterHandler( + proto.MessageName(PT(new(T))), + func(ctx context.Context, msg transaction.Msg) (msgResp transaction.Msg, err error) { + typedReq := msg.(PT) + typedResp, err := handler(ctx, typedReq) + if err != nil { + return nil, err + } + + return typedResp, nil + }, + ) + require.NoError(t, err) + + return msgRouterBuilder +} + +func TestConsensus_InitChain_Without_UpdateParam(t *testing.T) { + c := setUpConsensus(t, 100_000, mempool.NoOpMempool[mock.Tx]{}) + mockStore := c.store + _, err := c.InitChain(context.Background(), &abciproto.InitChainRequest{ + Time: time.Now(), + ChainId: "test", + InitialHeight: 1, + }) + require.NoError(t, err) + assertStoreLatestVersion(t, mockStore, 0) + + _, err = c.FinalizeBlock(context.Background(), &abciproto.FinalizeBlockRequest{ + Time: time.Now(), + Height: 1, + }) + require.NoError(t, err) + assertStoreLatestVersion(t, mockStore, 1) +} + +func TestConsensus_InitChain_With_UpdateParam(t *testing.T) { + c := setUpConsensus(t, 100_000, mempool.NoOpMempool[mock.Tx]{}) + mockStore := c.store + _, err := c.InitChain(context.Background(), &abciproto.InitChainRequest{ + Time: time.Now(), + ChainId: "test", + ConsensusParams: DefaulConsensusParams, + InitialHeight: 1, + }) + require.NoError(t, err) + assertStoreLatestVersion(t, mockStore, 0) + + _, err = c.FinalizeBlock(context.Background(), &abciproto.FinalizeBlockRequest{ + Time: time.Now(), + Height: 1, + }) + require.NoError(t, err) + + assertStoreLatestVersion(t, mockStore, 1) +} + +func TestConsensus_InitChain_Invalid_Height(t *testing.T) { + c := setUpConsensus(t, 100_000, mempool.NoOpMempool[mock.Tx]{}) + mockStore := c.store + _, err := c.InitChain(context.Background(), &abciproto.InitChainRequest{ + Time: time.Now(), + ChainId: "test", + InitialHeight: 2, + }) + require.NoError(t, err) + assertStoreLatestVersion(t, mockStore, 0) + + // Shouldn't be able to commit genesis block 2 + _, err = c.FinalizeBlock(context.Background(), &abciproto.FinalizeBlockRequest{ + Time: time.Now(), + Height: 2, + }) + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), "unable to commit the changeset")) +} + +func TestConsensus_FinalizeBlock_Invalid_Height(t *testing.T) { + c := setUpConsensus(t, 100_000, mempool.NoOpMempool[mock.Tx]{}) + _, err := c.InitChain(context.Background(), &abciproto.InitChainRequest{ + Time: time.Now(), + ChainId: "test", + InitialHeight: 1, + }) + require.NoError(t, err) + + _, err = c.FinalizeBlock(context.Background(), &abciproto.FinalizeBlockRequest{ + Time: time.Now(), + Height: 1, + }) + require.NoError(t, err) + + _, err = c.FinalizeBlock(context.Background(), &abciproto.FinalizeBlockRequest{ + Time: time.Now(), + Height: 3, + }) + require.Error(t, err) +} + +func TestConsensus_FinalizeBlock_NoTxs(t *testing.T) { + c := setUpConsensus(t, 100_000, mempool.NoOpMempool[mock.Tx]{}) + mockStore := c.store + + _, err := c.InitChain(context.Background(), &abciproto.InitChainRequest{ + Time: time.Now(), + ChainId: "test", + InitialHeight: 1, + }) + require.NoError(t, err) + + _, err = c.FinalizeBlock(context.Background(), &abciproto.FinalizeBlockRequest{ + Time: time.Now(), + Height: 1, + }) + require.NoError(t, err) + + endBlock := 10 + for i := 2; i <= endBlock; i++ { + _, err = c.FinalizeBlock(context.Background(), &abciproto.FinalizeBlockRequest{ + Time: time.Now(), + Height: int64(i), + Hash: sum[:], + }) + require.NoError(t, err) + + assertStoreLatestVersion(t, mockStore, uint64(i)) + } + require.Equal(t, int64(endBlock), c.lastCommittedHeight.Load()) +} + +func TestConsensus_FinalizeBlock_MultiTxs_OutOfGas(t *testing.T) { + c := setUpConsensus(t, 100_000, mempool.NoOpMempool[mock.Tx]{}) + + _, err := c.InitChain(context.Background(), &abciproto.InitChainRequest{ + Time: time.Now(), + ChainId: "test", + InitialHeight: 1, + }) + require.NoError(t, err) + + _, err = c.FinalizeBlock(context.Background(), &abciproto.FinalizeBlockRequest{ + Time: time.Now(), + Height: 1, + }) + require.NoError(t, err) + + endBlock := 10 + for i := 2; i <= endBlock; i++ { + res, err := c.FinalizeBlock(context.Background(), &abciproto.FinalizeBlockRequest{ + Time: time.Now(), + Height: int64(i), + Hash: sum[:], + Txs: [][]byte{invalidMockTx.Bytes(), mockTx.Bytes()}, + }) + require.NoError(t, err) + require.NotEqual(t, res.TxResults[0].Code, 0) + } + require.Equal(t, int64(endBlock), c.lastCommittedHeight.Load()) +} + +func TestConsensus_FinalizeBlock_MultiTxs(t *testing.T) { + c := setUpConsensus(t, 100_000, mempool.NoOpMempool[mock.Tx]{}) + mockStore := c.store + + _, err := c.InitChain(context.Background(), &abciproto.InitChainRequest{ + Time: time.Now(), + ChainId: "test", + InitialHeight: 1, + }) + require.NoError(t, err) + + _, err = c.FinalizeBlock(context.Background(), &abciproto.FinalizeBlockRequest{ + Time: time.Now(), + Height: 1, + }) + require.NoError(t, err) + + endBlock := 10 + for i := 2; i <= endBlock; i++ { + _, err = c.FinalizeBlock(context.Background(), &abciproto.FinalizeBlockRequest{ + Time: time.Now(), + Height: int64(i), + Hash: sum[:], + Txs: [][]byte{mockTx.Bytes(), mockTx.Bytes()}, + }) + require.NoError(t, err) + assertStoreLatestVersion(t, mockStore, uint64(i)) + } + require.Equal(t, int64(endBlock), c.lastCommittedHeight.Load()) +} + +func TestConsensus_CheckTx(t *testing.T) { + c := setUpConsensus(t, 0, mempool.NoOpMempool[mock.Tx]{}) + + _, err := c.InitChain(context.Background(), &abciproto.InitChainRequest{ + Time: time.Now(), + ChainId: "test", + InitialHeight: 1, + }) + require.NoError(t, err) + + // empty byte + _, err = c.CheckTx(context.Background(), &abciproto.CheckTxRequest{ + Tx: []byte{}, + }) + require.Error(t, err) + + // out of gas + res, err := c.CheckTx(context.Background(), &abciproto.CheckTxRequest{ + Tx: mock.Tx{ + Sender: []byte("sender"), + Msg: &gogotypes.BoolValue{Value: true}, + GasLimit: 100_000, + }.Bytes(), + }) + require.NoError(t, err) + require.NotEqual(t, res.Code, 0) + + c = setUpConsensus(t, 100_000, mempool.NoOpMempool[mock.Tx]{}) + res, err = c.CheckTx(context.Background(), &abciproto.CheckTxRequest{ + Tx: mock.Tx{ + Sender: []byte("sender"), + Msg: &gogotypes.BoolValue{Value: true}, + GasLimit: 100_000, + }.Bytes(), + }) + require.NoError(t, err) + require.NotEqual(t, res.GasUsed, 0) +} + +func TestConsensus_ExtendVote(t *testing.T) { + c := setUpConsensus(t, 100_000, mempool.NoOpMempool[mock.Tx]{}) + + _, err := c.InitChain(context.Background(), &abciproto.InitChainRequest{ + Time: time.Now(), + ChainId: "test", + InitialHeight: 1, + ConsensusParams: &v1.ConsensusParams{ + Block: &v1.BlockParams{ + MaxGas: 5000000, + }, + Abci: &v1.ABCIParams{ + VoteExtensionsEnableHeight: 2, + }, + Feature: &v1.FeatureParams{ + VoteExtensionsEnableHeight: &gogotypes.Int64Value{Value: 2}, + }, + }, + }) + require.NoError(t, err) + + // Votes not enabled yet + _, err = c.ExtendVote(context.Background(), &abciproto.ExtendVoteRequest{ + Height: 1, + }) + require.ErrorContains(t, err, "vote extensions are not enabled") + + // Empty extendVote handler + _, err = c.ExtendVote(context.Background(), &abciproto.ExtendVoteRequest{ + Height: 2, + }) + require.ErrorContains(t, err, "no extend function was set") + + // Use NoOp handler + c.extendVote = DefaultServerOptions[mock.Tx]().ExtendVoteHandler + res, err := c.ExtendVote(context.Background(), &abciproto.ExtendVoteRequest{ + Height: 2, + }) + require.NoError(t, err) + require.Equal(t, len(res.VoteExtension), 0) +} + +func TestConsensus_VerifyVoteExtension(t *testing.T) { + c := setUpConsensus(t, 100_000, mempool.NoOpMempool[mock.Tx]{}) + + _, err := c.InitChain(context.Background(), &abciproto.InitChainRequest{ + Time: time.Now(), + ChainId: "test", + InitialHeight: 1, + ConsensusParams: &v1.ConsensusParams{ + Block: &v1.BlockParams{ + MaxGas: 5000000, + }, + Abci: &v1.ABCIParams{ + VoteExtensionsEnableHeight: 2, + }, + Feature: &v1.FeatureParams{ + VoteExtensionsEnableHeight: &gogotypes.Int64Value{Value: 2}, + }, + }, + }) + require.NoError(t, err) + + // Votes not enabled yet + _, err = c.VerifyVoteExtension(context.Background(), &abciproto.VerifyVoteExtensionRequest{ + Height: 1, + }) + require.ErrorContains(t, err, "vote extensions are not enabled") + + // Empty verifyVote handler + _, err = c.VerifyVoteExtension(context.Background(), &abciproto.VerifyVoteExtensionRequest{ + Height: 2, + }) + require.ErrorContains(t, err, "no verify function was set") + + // Use NoOp handler + c.verifyVoteExt = DefaultServerOptions[mock.Tx]().VerifyVoteExtensionHandler + res, err := c.VerifyVoteExtension(context.Background(), &abciproto.VerifyVoteExtensionRequest{ + Height: 2, + Hash: []byte("test"), + }) + require.NoError(t, err) + require.Equal(t, res.Status, abciproto.VERIFY_VOTE_EXTENSION_STATUS_ACCEPT) +} + +func TestConsensus_PrepareProposal(t *testing.T) { + c := setUpConsensus(t, 100_000, mempool.NoOpMempool[mock.Tx]{}) + + // Invalid height + _, err := c.PrepareProposal(context.Background(), &abciproto.PrepareProposalRequest{ + Height: 0, + }) + require.Error(t, err) + + // empty handler + _, err = c.PrepareProposal(context.Background(), &abciproto.PrepareProposalRequest{ + Height: 1, + }) + require.Error(t, err) + + // NoOp handler + c.prepareProposalHandler = DefaultServerOptions[mock.Tx]().PrepareProposalHandler + _, err = c.PrepareProposal(context.Background(), &abciproto.PrepareProposalRequest{ + Height: 1, + Txs: [][]byte{mockTx.Bytes()}, + }) + require.NoError(t, err) +} + +func TestConsensus_PrepareProposal_With_Handler_NoOpMempool(t *testing.T) { + c := setUpConsensus(t, 100_000, mempool.NoOpMempool[mock.Tx]{}) + + c.prepareProposalHandler = handlers.NewDefaultProposalHandler(c.mempool).PrepareHandler() + + // zero MaxTxBytes + res, err := c.PrepareProposal(context.Background(), &abciproto.PrepareProposalRequest{ + Height: 1, + MaxTxBytes: 0, + Txs: [][]byte{mockTx.Bytes()}, + }) + require.NoError(t, err) + require.Equal(t, len(res.Txs), 0) + + // have tx exeed MaxTxBytes + // each mock tx has 128 bytes, should select 2 txs + res, err = c.PrepareProposal(context.Background(), &abciproto.PrepareProposalRequest{ + Height: 1, + MaxTxBytes: 300, + Txs: [][]byte{mockTx.Bytes(), mockTx.Bytes(), mockTx.Bytes()}, + }) + require.NoError(t, err) + require.Equal(t, len(res.Txs), 2) + + // reach MaxTxBytes + res, err = c.PrepareProposal(context.Background(), &abciproto.PrepareProposalRequest{ + Height: 1, + MaxTxBytes: 256, + Txs: [][]byte{mockTx.Bytes(), mockTx.Bytes()}, + }) + require.NoError(t, err) + require.Equal(t, len(res.Txs), 2) + + // Over gas, under MaxTxBytes + // 300_000 gas limit, should only take 3 txs + res, err = c.PrepareProposal(context.Background(), &abciproto.PrepareProposalRequest{ + Height: 1, + MaxTxBytes: 1000, + Txs: [][]byte{mockTx.Bytes(), mockTx.Bytes(), mockTx.Bytes(), mockTx.Bytes()}, + }) + require.NoError(t, err) + require.Equal(t, len(res.Txs), 3) + + // Reach max gas + res, err = c.PrepareProposal(context.Background(), &abciproto.PrepareProposalRequest{ + Height: 1, + MaxTxBytes: 1000, + Txs: [][]byte{mockTx.Bytes(), mockTx.Bytes(), mockTx.Bytes()}, + }) + require.NoError(t, err) + require.Equal(t, len(res.Txs), 3) + + // have a bad encoding tx + res, err = c.PrepareProposal(context.Background(), &abciproto.PrepareProposalRequest{ + Height: 1, + MaxTxBytes: 1000, + Txs: [][]byte{mockTx.Bytes(), append(mockTx.Bytes(), []byte("bad")...), mockTx.Bytes()}, + }) + require.NoError(t, err) + require.Equal(t, len(res.Txs), 2) +} + +func TestConsensus_ProcessProposal(t *testing.T) { + c := setUpConsensus(t, 100_000, mempool.NoOpMempool[mock.Tx]{}) + + // Invalid height + _, err := c.ProcessProposal(context.Background(), &abciproto.ProcessProposalRequest{ + Height: 0, + }) + require.Error(t, err) + + // empty handler + _, err = c.ProcessProposal(context.Background(), &abciproto.ProcessProposalRequest{ + Height: 1, + }) + require.Error(t, err) + + // NoOp handler + c.processProposalHandler = DefaultServerOptions[mock.Tx]().ProcessProposalHandler + _, err = c.ProcessProposal(context.Background(), &abciproto.ProcessProposalRequest{ + Height: 1, + Txs: [][]byte{mockTx.Bytes()}, + }) + require.NoError(t, err) +} + +func TestConsensus_ProcessProposal_With_Handler(t *testing.T) { + c := setUpConsensus(t, 100_000, cometmock.MockMempool[mock.Tx]{}) + + c.processProposalHandler = handlers.NewDefaultProposalHandler(c.mempool).ProcessHandler() + + // exeed max gas + res, err := c.ProcessProposal(context.Background(), &abciproto.ProcessProposalRequest{ + Height: 1, + Txs: [][]byte{mockTx.Bytes(), mockTx.Bytes(), mockTx.Bytes(), mockTx.Bytes()}, + }) + require.NoError(t, err) + require.Equal(t, res.Status, abciproto.PROCESS_PROPOSAL_STATUS_REJECT) + + // have bad encode tx + // should reject + res, err = c.ProcessProposal(context.Background(), &abciproto.ProcessProposalRequest{ + Height: 1, + Txs: [][]byte{mockTx.Bytes(), append(mockTx.Bytes(), []byte("bad")...), mockTx.Bytes(), mockTx.Bytes()}, + }) + require.Equal(t, res.Status, abciproto.PROCESS_PROPOSAL_STATUS_REJECT) +} + +func TestConsensus_Info(t *testing.T) { + c := setUpConsensus(t, 100_000, cometmock.MockMempool[mock.Tx]{}) + + // Version 0 + res, err := c.Info(context.Background(), &abciproto.InfoRequest{}) + require.NoError(t, err) + require.Equal(t, res.LastBlockHeight, int64(0)) + + // Commit store to version 1 + _, err = c.InitChain(context.Background(), &abciproto.InitChainRequest{ + Time: time.Now(), + ChainId: "test", + InitialHeight: 1, + }) + require.NoError(t, err) + + _, err = c.FinalizeBlock(context.Background(), &abciproto.FinalizeBlockRequest{ + Time: time.Now(), + Height: 1, + }) + require.NoError(t, err) + + res, err = c.Info(context.Background(), &abciproto.InfoRequest{}) + require.NoError(t, err) + require.Equal(t, res.LastBlockHeight, int64(1)) +} + +// TODO: +// - GRPC request +// - app request +// - p2p request +func TestConsensus_Query(t *testing.T) { + c := setUpConsensus(t, 100_000, cometmock.MockMempool[mock.Tx]{}) + + // Write data to state storage + c.store.GetStateStorage().ApplyChangeset(1, &store.Changeset{ + Changes: []store.StateChanges{ + { + Actor: actorName, + StateChanges: []store.KVPair{ + { + Key: []byte("key"), + Value: []byte("value"), + Remove: false, + }, + }, + }, + }, + }) + + _, err := c.InitChain(context.Background(), &abciproto.InitChainRequest{ + Time: time.Now(), + ChainId: "test", + InitialHeight: 1, + }) + require.NoError(t, err) + + _, err = c.FinalizeBlock(context.Background(), &abciproto.FinalizeBlockRequest{ + Time: time.Now(), + Height: 1, + Txs: [][]byte{mockTx.Bytes()}, + }) + require.NoError(t, err) + + // empty request + res, err := c.Query(context.Background(), &abciproto.QueryRequest{}) + require.NoError(t, err) + require.Equal(t, res.Code, uint32(1)) + require.Contains(t, res.Log, "no query path provided") + + // Query store + res, err = c.Query(context.Background(), &abciproto.QueryRequest{ + Path: "store/cookies/", + Data: []byte("key"), + Height: 1, + }) + require.NoError(t, err) + require.Equal(t, string(res.Value), "value") + + // Query store with no value + res, err = c.Query(context.Background(), &abciproto.QueryRequest{ + Path: "store/cookies/", + Data: []byte("exec"), + Height: 1, + }) + require.NoError(t, err) + require.Equal(t, res.Value, []byte(nil)) +} + +func setUpConsensus(t *testing.T, gasLimit uint64, mempool mempool.Mempool[mock.Tx]) *Consensus[mock.Tx] { + msgRouterBuilder := getMsgRouterBuilder(t, func(ctx context.Context, msg *gogotypes.BoolValue) (*gogotypes.BoolValue, error) { + return nil, nil + }) + + queryRouterBuilder := getQueryRouterBuilder(t, func(ctx context.Context, q *consensustypes.QueryParamsRequest) (*consensustypes.QueryParamsResponse, error) { + cParams := &v1.ConsensusParams{ + Block: &v1.BlockParams{ + MaxGas: 300000, + }, + Abci: &v1.ABCIParams{ + VoteExtensionsEnableHeight: 2, + }, + Feature: &v1.FeatureParams{ + VoteExtensionsEnableHeight: &gogotypes.Int64Value{Value: 2}, + }, + } + return &consensustypes.QueryParamsResponse{ + Params: cParams, + }, nil + }) + + s, err := stf.NewSTF( + log.NewNopLogger().With("module", "stf"), + msgRouterBuilder, + queryRouterBuilder, + func(ctx context.Context, txs []mock.Tx) error { return nil }, + func(ctx context.Context) error { + return nil + }, + func(ctx context.Context) error { + return nil + }, + func(ctx context.Context, tx mock.Tx) error { + return nil + }, + func(ctx context.Context) ([]appmodulev2.ValidatorUpdate, error) { return nil, nil }, + func(ctx context.Context, tx mock.Tx, success bool) error { + return nil + }, + branch.DefaultNewWriterMap, + ) + require.NoError(t, err) + + ss := cometmock.NewMockStorage(log.NewNopLogger(), t.TempDir()) + sc := cometmock.NewMockCommiter(log.NewNopLogger(), string(actorName), "stf") + mockStore := cometmock.NewMockStore(ss, sc) + + b := am.Builder[mock.Tx]{ + STF: s, + DB: mockStore, + ValidateTxGasLimit: gasLimit, + QueryGasLimit: gasLimit, + SimulationGasLimit: gasLimit, + InitGenesis: func(ctx context.Context, src io.Reader, txHandler func(json.RawMessage) error) error { + return nil + }, + } + + am, err := b.Build() + require.NoError(t, err) + + return NewConsensus[mock.Tx](log.NewNopLogger(), "testing-app", "authority", am, mempool, map[string]struct{}{}, nil, mockStore, Config{AppTomlConfig: DefaultAppTomlConfig()}, mock.TxCodec{}, "test") +} + +// Check target version same with store's latest version +// And should have commit info of target version +// If block 0, commitInfo returned should be nil +func assertStoreLatestVersion(t *testing.T, store types.Store, target uint64) { + t.Helper() + version, err := store.GetLatestVersion() + require.NoError(t, err) + require.Equal(t, version, target) + commitInfo, err := store.GetStateCommitment().GetCommitInfo(version) + require.NoError(t, err) + if target != 0 { + require.Equal(t, commitInfo.Version, target) + } else { + require.Nil(t, commitInfo) + } +} diff --git a/server/v2/cometbft/go.mod b/server/v2/cometbft/go.mod index 8c009c95244f..b15fedb5c4f7 100644 --- a/server/v2/cometbft/go.mod +++ b/server/v2/cometbft/go.mod @@ -8,6 +8,7 @@ replace ( cosmossdk.io/core/testing => ../../../core/testing cosmossdk.io/server/v2 => ../ cosmossdk.io/server/v2/appmanager => ../appmanager + cosmossdk.io/server/v2/stf => ../stf cosmossdk.io/store => ../../../store cosmossdk.io/store/v2 => ../../../store/v2 cosmossdk.io/x/bank => ../../../x/bank @@ -25,6 +26,7 @@ require ( cosmossdk.io/log v1.4.1 cosmossdk.io/server/v2 v2.0.0-00010101000000-000000000000 cosmossdk.io/server/v2/appmanager v0.0.0-20240802110823-cffeedff643d + cosmossdk.io/server/v2/stf v0.0.0-20240708142107-25e99c54bac1 cosmossdk.io/store/v2 v2.0.0-00010101000000-000000000000 cosmossdk.io/x/consensus v0.0.0-00010101000000-000000000000 github.com/cometbft/cometbft v1.0.0-rc1.0.20240908111210-ab0be101882f @@ -37,6 +39,7 @@ require ( github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.19.0 + github.com/stretchr/testify v1.9.0 google.golang.org/genproto/googleapis/api v0.0.0-20240604185151-ef581f913117 google.golang.org/grpc v1.66.0 google.golang.org/protobuf v1.34.2 @@ -46,6 +49,7 @@ require ( require ( buf.build/gen/go/cosmos/gogo-proto/protocolbuffers/go v1.34.2-20240130113600-88ef6483f90f.2 // indirect cosmossdk.io/collections v0.4.0 // indirect + cosmossdk.io/core/testing v0.0.0-00010101000000-000000000000 // indirect cosmossdk.io/depinject v1.0.0 // indirect cosmossdk.io/errors/v2 v2.0.0-20240731132947-df72853b3ca5 // indirect cosmossdk.io/math v1.3.0 // indirect @@ -121,6 +125,7 @@ require ( github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-sqlite3 v1.14.22 // indirect github.com/minio/highwayhash v1.0.3 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect @@ -146,7 +151,6 @@ require ( github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.7.0 // indirect - github.com/stretchr/testify v1.9.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/supranational/blst v0.3.13 // indirect github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect diff --git a/server/v2/cometbft/internal/mock/mock_mempool.go b/server/v2/cometbft/internal/mock/mock_mempool.go new file mode 100644 index 000000000000..89d51e955fae --- /dev/null +++ b/server/v2/cometbft/internal/mock/mock_mempool.go @@ -0,0 +1,19 @@ +package mock + +import ( + "context" + + "cosmossdk.io/core/transaction" + "cosmossdk.io/server/v2/cometbft/mempool" +) + +var _ mempool.Mempool[transaction.Tx] = (*MockMempool[transaction.Tx])(nil) + +// MockMempool implements Mempool +// Used for testing instead of NoOpMempool +type MockMempool[T transaction.Tx] struct{} + +func (MockMempool[T]) Insert(context.Context, T) error { return nil } +func (MockMempool[T]) Select(context.Context, []T) mempool.Iterator[T] { return nil } +func (MockMempool[T]) CountTx() int { return 0 } +func (MockMempool[T]) Remove([]T) error { return nil } diff --git a/server/v2/cometbft/internal/mock/mock_reader.go b/server/v2/cometbft/internal/mock/mock_reader.go new file mode 100644 index 000000000000..7638fc3f115b --- /dev/null +++ b/server/v2/cometbft/internal/mock/mock_reader.go @@ -0,0 +1,65 @@ +package mock + +import ( + corestore "cosmossdk.io/core/store" +) + +// ReaderMap defines an adapter around a RootStore that only exposes read-only +// operations. This is useful for exposing a read-only view of the RootStore at +// a specific version in history, which could also be the latest state. +type ReaderMap struct { + store *MockStore + version uint64 +} + +func NewMockReaderMap(v uint64, rs *MockStore) *ReaderMap { + return &ReaderMap{ + store: rs, + version: v, + } +} + +func (roa *ReaderMap) GetReader(actor []byte) (corestore.Reader, error) { + return NewMockReader(roa.version, roa.store, actor), nil +} + +// Reader represents a read-only adapter for accessing data from the root store. +type MockReader struct { + version uint64 // The version of the data. + store *MockStore // The root store to read data from. + actor []byte // The actor associated with the data. +} + +func NewMockReader(v uint64, rs *MockStore, actor []byte) *MockReader { + return &MockReader{ + version: v, + store: rs, + actor: actor, + } +} + +func (roa *MockReader) Has(key []byte) (bool, error) { + val, err := roa.store.GetStateStorage().Has(roa.actor, roa.version, key) + if err != nil { + return false, err + } + + return val, nil +} + +func (roa *MockReader) Get(key []byte) ([]byte, error) { + result, err := roa.store.GetStateStorage().Get(roa.actor, roa.version, key) + if err != nil { + return nil, err + } + + return result, nil +} + +func (roa *MockReader) Iterator(start, end []byte) (corestore.Iterator, error) { + return roa.store.GetStateStorage().Iterator(roa.actor, roa.version, start, end) +} + +func (roa *MockReader) ReverseIterator(start, end []byte) (corestore.Iterator, error) { + return roa.store.GetStateStorage().ReverseIterator(roa.actor, roa.version, start, end) +} diff --git a/server/v2/cometbft/internal/mock/mock_store.go b/server/v2/cometbft/internal/mock/mock_store.go new file mode 100644 index 000000000000..ee9089f8d49f --- /dev/null +++ b/server/v2/cometbft/internal/mock/mock_store.go @@ -0,0 +1,139 @@ +package mock + +import ( + "crypto/sha256" + "fmt" + + "cosmossdk.io/core/log" + corestore "cosmossdk.io/core/store" + + storev2 "cosmossdk.io/store/v2" + "cosmossdk.io/store/v2/commitment" + "cosmossdk.io/store/v2/commitment/iavl" + dbm "cosmossdk.io/store/v2/db" + "cosmossdk.io/store/v2/proof" + "cosmossdk.io/store/v2/storage" + "cosmossdk.io/store/v2/storage/sqlite" +) + +type MockStore struct { + Storage storev2.VersionedDatabase + Commiter storev2.Committer +} + +func NewMockStorage(logger log.Logger, dir string) storev2.VersionedDatabase { + storageDB, _ := sqlite.New(dir) + ss := storage.NewStorageStore(storageDB, logger) + return ss +} + +func NewMockCommiter(logger log.Logger, actors ...string) storev2.Committer { + treeMap := make(map[string]commitment.Tree) + for _, actor := range actors { + tree := iavl.NewIavlTree(dbm.NewMemDB(), logger, iavl.DefaultConfig()) + treeMap[actor] = tree + } + sc, _ := commitment.NewCommitStore(treeMap, treeMap, dbm.NewMemDB(), logger) + return sc +} + +func NewMockStore(ss storev2.VersionedDatabase, sc storev2.Committer) *MockStore { + return &MockStore{Storage: ss, Commiter: sc} +} + +func (s *MockStore) GetLatestVersion() (uint64, error) { + lastCommitID, err := s.LastCommitID() + if err != nil { + return 0, err + } + + return lastCommitID.Version, nil +} + +func (s *MockStore) StateLatest() (uint64, corestore.ReaderMap, error) { + v, err := s.GetLatestVersion() + if err != nil { + return 0, nil, err + } + + return v, NewMockReaderMap(v, s), nil +} + +func (s *MockStore) Commit(changeset *corestore.Changeset) (corestore.Hash, error) { + v, _, _ := s.StateLatest() + err := s.Storage.ApplyChangeset(v, changeset) + if err != nil { + return []byte{}, err + } + + err = s.Commiter.WriteChangeset(changeset) + if err != nil { + return []byte{}, err + } + + commitInfo, err := s.Commiter.Commit(v + 1) + fmt.Println("commitInfo", commitInfo, err) + return []byte{}, err +} + +func (s *MockStore) StateAt(version uint64) (corestore.ReaderMap, error) { + info, err := s.Commiter.GetCommitInfo(version) + if err != nil || info == nil { + return nil, fmt.Errorf("failed to get commit info for version %d: %w", version, err) + } + return NewMockReaderMap(version, s), nil +} + +func (s *MockStore) GetStateStorage() storev2.VersionedDatabase { + return s.Storage +} + +func (s *MockStore) GetStateCommitment() storev2.Committer { + return s.Commiter +} + +type Result struct { + key []byte + value []byte + version uint64 + proofOps []proof.CommitmentOp +} + +func (s *MockStore) Query(storeKey []byte, version uint64, key []byte, prove bool) (storev2.QueryResult, error) { + state, err := s.StateAt(version) + reader, err := state.GetReader(storeKey) + value, err := reader.Get(key) + res := storev2.QueryResult{ + Key: key, + Value: value, + Version: version, + } + return res, err +} + +func (s *MockStore) LastCommitID() (proof.CommitID, error) { + v, err := s.GetStateCommitment().GetLatestVersion() + bz := sha256.Sum256([]byte{}) + return proof.CommitID{ + Version: v, + Hash: bz[:], + }, err +} + +func (s *MockStore) SetInitialVersion(v uint64) error { + return s.Commiter.SetInitialVersion(v) +} + +func (s *MockStore) WorkingHash(changeset *corestore.Changeset) (corestore.Hash, error) { + v, _, _ := s.StateLatest() + err := s.Storage.ApplyChangeset(v, changeset) + if err != nil { + return []byte{}, err + } + + err = s.Commiter.WriteChangeset(changeset) + if err != nil { + return []byte{}, err + } + return []byte{}, nil +} diff --git a/server/v2/stf/mock/tx.go b/server/v2/stf/mock/tx.go index aa89104fac61..7df7bc0e3208 100644 --- a/server/v2/stf/mock/tx.go +++ b/server/v2/stf/mock/tx.go @@ -65,56 +65,58 @@ func (t Tx) Bytes() []byte { return tx } -func (t *Tx) Decode(b []byte) { +func (t *Tx) Decode(b []byte) error { rawTx := new(encodedTx) err := json.Unmarshal(b, rawTx) if err != nil { - panic(err) + return err } msgName, err := gogoproto.AnyMessageName(rawTx.Msg) msgType := proto.MessageType(msgName).Elem() if err != nil { - panic(err) + return err } msg := reflect.New(msgType).Interface().(proto.Message) if err := gogoproto.UnmarshalAny(rawTx.Msg, msg); err != nil { - panic(err) + return err } t.Msg = msg t.Sender = rawTx.Sender t.GasLimit = rawTx.GasLimit + return nil } -func (t *Tx) DecodeJSON(b []byte) { +func (t *Tx) DecodeJSON(b []byte) error { rawTx := new(encodedTx) err := json.Unmarshal(b, rawTx) if err != nil { - panic(err) + return err } msgName, err := gogoproto.AnyMessageName(rawTx.Msg) msgType := proto.MessageType(msgName).Elem() if err != nil { - panic(err) + return err } msg := reflect.New(msgType).Interface().(transaction.Msg) if err := gogoproto.UnmarshalAny(rawTx.Msg, msg); err != nil { - panic(err) + return err } t.Msg = msg t.Sender = rawTx.Sender t.GasLimit = rawTx.GasLimit + return nil } type TxCodec struct{} func (TxCodec) Decode(bytes []byte) (Tx, error) { t := new(Tx) - t.Decode(bytes) - return *t, nil + err := t.Decode(bytes) + return *t, err } func (TxCodec) DecodeJSON(bytes []byte) (Tx, error) { t := new(Tx) - t.DecodeJSON(bytes) - return *t, nil + err := t.DecodeJSON(bytes) + return *t, err } diff --git a/simapp/v2/go.mod b/simapp/v2/go.mod index 34534a1e6197..2ebea140ab95 100644 --- a/simapp/v2/go.mod +++ b/simapp/v2/go.mod @@ -57,7 +57,7 @@ require ( cosmossdk.io/errors/v2 v2.0.0-20240731132947-df72853b3ca5 // indirect cosmossdk.io/schema v0.2.0 // indirect cosmossdk.io/server/v2/appmanager v0.0.0-20240802110823-cffeedff643d // indirect - cosmossdk.io/server/v2/stf v0.0.0-00010101000000-000000000000 // indirect + cosmossdk.io/server/v2/stf v0.0.0-20240708142107-25e99c54bac1 // indirect cosmossdk.io/store v1.1.1-0.20240418092142-896cdf1971bc // indirect cosmossdk.io/x/accounts/defaults/lockup v0.0.0-20240417181816-5e7aae0db1f5 // indirect cosmossdk.io/x/accounts/defaults/multisig v0.0.0-00010101000000-000000000000 // indirect