diff --git a/x/qgb/client/query.go b/x/qgb/client/query.go new file mode 100644 index 0000000000..e1069155f5 --- /dev/null +++ b/x/qgb/client/query.go @@ -0,0 +1,102 @@ +package client + +import ( + "encoding/json" + "fmt" + "strconv" + + "github.com/celestiaorg/celestia-app/x/qgb/types" + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/flags" + cdctypes "github.com/cosmos/cosmos-sdk/codec/types" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/std" + "github.com/spf13/cobra" +) + +// GetQueryCmd returns the CLI query commands for this module +func GetQueryCmd() *cobra.Command { + // Group qgb queries under a subcommand + cmd := &cobra.Command{ + Use: types.ModuleName, + Short: fmt.Sprintf("Querying commands for the %s module", types.ModuleName), + DisableFlagParsing: true, + SuggestionsMinimumDistance: 2, + RunE: client.ValidateCmd, + } + + cmd.AddCommand(CmdQueryAttestationByNonce()) + + return cmd +} + +func CmdQueryAttestationByNonce() *cobra.Command { + cmd := &cobra.Command{ + Use: "attestation ", + Aliases: []string{"att"}, + Short: "query an attestation by nonce", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx := client.GetClientContextFromCmd(cmd) + queryClient := types.NewQueryClient(clientCtx) + + nonce, err := strconv.ParseUint(args[0], 10, 0) + if err != nil { + return err + } + res, err := queryClient.AttestationRequestByNonce( + cmd.Context(), + &types.QueryAttestationRequestByNonceRequest{Nonce: nonce}, + ) + if err != nil { + return err + } + if res.Attestation == nil { + return types.ErrNilAttestation + } + att, err := unmarshallAttestation(res.Attestation) + if err != nil { + return err + } + + switch att.(type) { + case *types.Valset, *types.DataCommitment: + jsonDC, err := json.Marshal(att) + if err != nil { + return err + } + return clientCtx.PrintString(string(jsonDC)) + default: + return types.ErrUnknownAttestationType + } + }, + } + + flags.AddQueryFlagsToCmd(cmd) + + return cmd +} + +// unmarshallAttestation unmarshal a wrapper protobuf `Any` type to an `AttestationRequestI`. +func unmarshallAttestation(attestation *cdctypes.Any) (types.AttestationRequestI, error) { + var unmarshalledAttestation types.AttestationRequestI + err := makeInterfaceRegistry().UnpackAny(attestation, &unmarshalledAttestation) + if err != nil { + return nil, err + } + return unmarshalledAttestation, nil +} + +// makeInterfaceRegistry creates the interface registry containing the QGB interfaces +func makeInterfaceRegistry() codectypes.InterfaceRegistry { + // create the codec + interfaceRegistry := codectypes.NewInterfaceRegistry() + + // register the standard types from the sdk + std.RegisterInterfaces(interfaceRegistry) + + // register the qgb module interfaces + types.RegisterInterfaces(interfaceRegistry) + + return interfaceRegistry +} diff --git a/x/qgb/client/query_test.go b/x/qgb/client/query_test.go new file mode 100644 index 0000000000..65789ba989 --- /dev/null +++ b/x/qgb/client/query_test.go @@ -0,0 +1,59 @@ +package client_test + +import ( + "testing" + + "github.com/celestiaorg/celestia-app/x/qgb/client" + clitestutil "github.com/cosmos/cosmos-sdk/testutil/cli" +) + +func (s *CLITestSuite) TestQueryAttestationByNonce() { + _, err := s.network.WaitForHeight(402) + s.Require().NoError(err) + val := s.network.Validators[0] + testCases := []struct { + name string + nonce string + expectErr bool + }{ + { + name: "query the first valset that's created on chain startup", + nonce: "1", + expectErr: false, + }, + { + name: "query the first data commitment", + nonce: "2", + expectErr: false, + }, + { + name: "negative attestation nonce", + nonce: "-1", + expectErr: true, + }, + { + name: "zero attestation nonce", + nonce: "0", + expectErr: true, + }, + { + name: "higher attestation nonce than latest attestation nonce", + nonce: "100", + expectErr: true, + }, + } + + for _, tc := range testCases { + s.T().Run(tc.name, func(t *testing.T) { + cmd := client.CmdQueryAttestationByNonce() + clientCtx := val.ClientCtx + + _, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, []string{tc.nonce}) + if tc.expectErr { + s.Assert().Error(err) + } else { + s.Assert().NoError(err) + } + }) + } +} diff --git a/x/qgb/client/suite_test.go b/x/qgb/client/suite_test.go new file mode 100644 index 0000000000..30e7b0f4c8 --- /dev/null +++ b/x/qgb/client/suite_test.go @@ -0,0 +1,55 @@ +package client_test + +import ( + "testing" + "time" + + "github.com/celestiaorg/celestia-app/test/util/network" + "github.com/cosmos/cosmos-sdk/crypto/keyring" + cosmosnet "github.com/cosmos/cosmos-sdk/testutil/network" + "github.com/stretchr/testify/suite" + tmrand "github.com/tendermint/tendermint/libs/rand" +) + +type CLITestSuite struct { + suite.Suite + cfg cosmosnet.Config + network *cosmosnet.Network + kr keyring.Keyring +} + +func (s *CLITestSuite) SetupSuite() { + if testing.Short() { + s.T().Skip("skipping QGB CLI tests in short mode.") + } + s.T().Log("setting up QGB CLI test suite") + + cfg := network.DefaultConfig() + cfg.EnableTMLogging = false + cfg.MinGasPrices = "0utia" + cfg.NumValidators = 1 + cfg.TargetHeightDuration = time.Millisecond + s.cfg = cfg + + numAccounts := 120 + accounts := make([]string, numAccounts) + for i := 0; i < numAccounts; i++ { + accounts[i] = tmrand.Str(20) + } + + net := network.New(s.T(), cfg, accounts...) + + s.network = net + s.kr = net.Validators[0].ClientCtx.Keyring + _, err := s.network.WaitForHeight(2) + s.Require().NoError(err) +} + +func (s *CLITestSuite) TearDownSuite() { + s.T().Log("tearing down QGB CLI test suite") + s.network.Cleanup() +} + +func TestQGBCLI(t *testing.T) { + suite.Run(t, new(CLITestSuite)) +} diff --git a/x/qgb/keeper/query_attestation.go b/x/qgb/keeper/query_attestation.go index d082a3317c..8d7939fac3 100644 --- a/x/qgb/keeper/query_attestation.go +++ b/x/qgb/keeper/query_attestation.go @@ -13,8 +13,13 @@ func (k Keeper) AttestationRequestByNonce( ctx context.Context, request *types.QueryAttestationRequestByNonceRequest, ) (*types.QueryAttestationRequestByNonceResponse, error) { + unwrappedCtx := sdk.UnwrapSDKContext(ctx) + if latestAttestationNonce := k.GetLatestAttestationNonce(unwrappedCtx); latestAttestationNonce < request.Nonce { + return nil, types.ErrNonceHigherThanLatestAttestationNonce + } + attestation, found, err := k.GetAttestationByNonce( - sdk.UnwrapSDKContext(ctx), + unwrappedCtx, request.Nonce, ) if err != nil { diff --git a/x/qgb/module.go b/x/qgb/module.go index 3db0f768e2..092a7ffa3d 100644 --- a/x/qgb/module.go +++ b/x/qgb/module.go @@ -4,6 +4,8 @@ import ( "encoding/json" "fmt" + qgbcmd "github.com/celestiaorg/celestia-app/x/qgb/client" + "github.com/gorilla/mux" "github.com/grpc-ecosystem/grpc-gateway/runtime" "github.com/spf13/cobra" @@ -84,7 +86,7 @@ func (a AppModuleBasic) GetTxCmd() *cobra.Command { // GetQueryCmd returns the capability module's root query command. func (AppModuleBasic) GetQueryCmd() *cobra.Command { - return nil + return qgbcmd.GetQueryCmd() } // ---------------------------------------------------------------------------- diff --git a/x/qgb/types/errors.go b/x/qgb/types/errors.go index 2c91281d58..17a8513509 100644 --- a/x/qgb/types/errors.go +++ b/x/qgb/types/errors.go @@ -23,4 +23,5 @@ var ( ErrInvalidDataCommitmentWindow = errors.Register(ModuleName, 32, "invalid data commitment window") ErrEarliestAvailableNonceStillNotInitialized = errors.Register(ModuleName, 33, "the earliest available nonce after pruning has still not been defined in store") ErrRequestedNonceWasPruned = errors.Register(ModuleName, 34, "the requested nonce has been pruned") + ErrUnknownAttestationType = errors.Register(ModuleName, 35, "unknown attestation type") )