diff --git a/CHANGELOG.md b/CHANGELOG.md index b63098470c4f..d95bae220490 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,10 @@ Ref: https://keepachangelog.com/en/1.0.0/ ## [Unreleased] +### Features + +* [\#9750](https://github.com/cosmos/cosmos-sdk/pull/9750) Emit events for tx signature and sequence, so clients can now query txs by signature (`tx.signature=''`) or by address and sequence combo (`tx.acc_seq='/'`). + ### Improvements * (cli) [\#9717](https://github.com/cosmos/cosmos-sdk/pull/9717) Added CLI flag `--output json/text` to `tx` cli commands. diff --git a/types/events.go b/types/events.go index 17b0983bea3d..e435d1056cb5 100644 --- a/types/events.go +++ b/types/events.go @@ -282,13 +282,11 @@ func (e Events) GetAttributes(key string) ([]Attribute, bool) { } // Common event types and attribute keys -const ( +var ( EventTypeTx = "tx" AttributeKeyAccountSequence = "acc_seq" AttributeKeySignature = "signature" - AttributeKeyFee = "fee" - AttributeKeyFeePayer = "fee_payer" EventTypeMessage = "message" diff --git a/x/auth/ante/sigverify.go b/x/auth/ante/sigverify.go index f5e5a8626738..228b7a8fbee0 100644 --- a/x/auth/ante/sigverify.go +++ b/x/auth/ante/sigverify.go @@ -1,7 +1,7 @@ package ante import ( - "context" + "bytes" "encoding/base64" "encoding/hex" "errors" @@ -143,10 +143,37 @@ func verifyIsOnCurve(pubKey cryptotypes.PubKey) (err error) { return errorsmod.Wrap(sdkerrors.ErrInvalidPubKey, "some keys are not on curve") } - default: - return errorsmod.Wrapf(sdkerrors.ErrInvalidPubKey, "unsupported key type: %T", typedPubKey) + // Also emit the following events, so that txs can be indexed by these + // indices: + // - signature (via `tx.signature=''`), + // - concat(address,"/",sequence) (via `tx.acc_seq='cosmos1abc...def/42'`). + sigs, err := sigTx.GetSignaturesV2() + if err != nil { + return ctx, err } + var events sdk.Events + for i, sig := range sigs { + events = append(events, sdk.NewEvent(sdk.EventTypeTx, + sdk.NewAttribute(sdk.AttributeKeyAccountSequence, fmt.Sprintf("%s/%d", signers[i], sig.Sequence)), + )) + + sigBzs, err := signatureDataToBz(sig.Data) + if err != nil { + return ctx, err + } + for _, sigBz := range sigBzs { + events = append(events, sdk.NewEvent(sdk.EventTypeTx, + sdk.NewAttribute(sdk.AttributeKeySignature, base64.StdEncoding.EncodeToString(sigBz)), + )) + } + } + + ctx.EventManager().EmitEvents(events) + + return next(ctx, tx, simulate) +} + return nil } @@ -632,13 +659,12 @@ func CountSubKeys(pub cryptotypes.PubKey) int { // as well as the aggregated signature. func signatureDataToBz(data signing.SignatureData) ([][]byte, error) { if data == nil { - return nil, errors.New("got empty SignatureData") + return nil, fmt.Errorf("got empty SignatureData") } switch data := data.(type) { case *signing.SingleSignatureData: return [][]byte{data.Signature}, nil - case *signing.MultiSignatureData: sigs := [][]byte{} var err error @@ -648,39 +674,20 @@ func signatureDataToBz(data signing.SignatureData) ([][]byte, error) { if err != nil { return nil, err } - sigs = append(sigs, nestedSigs...) } - multiSignature := cryptotypes.MultiSignature{ + multisig := cryptotypes.MultiSignature{ Signatures: sigs, } - - aggregatedSig, err := multiSignature.Marshal() + aggregatedSig, err := multisig.Marshal() if err != nil { return nil, err } - sigs = append(sigs, aggregatedSig) - return sigs, nil + return sigs, nil default: return nil, sdkerrors.ErrInvalidType.Wrapf("unexpected signature data type %T", data) } } - -// isSigverifyTx will always return true, unless the context is a sdk.Context, in which case we will return the -// value of IsSigverifyTx. -func isSigverifyTx(ctx context.Context) bool { - if sdkCtx, ok := sdk.TryUnwrapSDKContext(ctx); ok { - return sdkCtx.IsSigverifyTx() - } - return true -} - -func isRecheckTx(ctx context.Context, txSvc transaction.Service) bool { - if sdkCtx, ok := sdk.TryUnwrapSDKContext(ctx); ok { - return sdkCtx.IsReCheckTx() - } - return txSvc.ExecMode(ctx) == transaction.ExecModeReCheck -} diff --git a/x/auth/client/cli/query.go b/x/auth/client/cli/query.go index 491c1ba7ad43..2d49a1433f65 100644 --- a/x/auth/client/cli/query.go +++ b/x/auth/client/cli/query.go @@ -10,16 +10,20 @@ import ( "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" sdk "github.com/cosmos/cosmos-sdk/types" - sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" - querytypes "github.com/cosmos/cosmos-sdk/types/query" + "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/types/query" + "github.com/cosmos/cosmos-sdk/types/rest" "github.com/cosmos/cosmos-sdk/version" authtx "github.com/cosmos/cosmos-sdk/x/auth/tx" ) const ( - FlagQuery = "query" - FlagType = "type" - FlagOrderBy = "order_by" + flagEvents = "events" + flagType = "type" + + typeHash = "hash" + typeAccSeq = "acc_seq" + typeSig = "signature" TypeHash = "hash" TypeAccSeq = "acc_seq" @@ -180,10 +184,112 @@ $ %s query tx --%s=%s , return cmd } -// ParseSigArgs parses comma-separated signatures from the CLI arguments. -func ParseSigArgs(args []string) ([]string, error) { +// QueryTxCmd implements the default command for a tx query. +func QueryTxCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "tx --type=[hash|acc_seq|signature] [hash|acc_seq|signature]", + Short: "Query for a transaction by hash, addr++seq combination or signature in a committed block", + Long: strings.TrimSpace(fmt.Sprintf(` +Example: +$ %s query tx +$ %s query tx --%s=%s : +$ %s query tx --%s=%s +`, + version.AppName, + version.AppName, flagType, typeAccSeq, + version.AppName, flagType, typeSig)), + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + + typ, _ := cmd.Flags().GetString(flagType) + + switch typ { + case typeHash: + { + if args[0] == "" { + return fmt.Errorf("argument should be a tx hash") + } + + // If hash is given, then query the tx by hash. + output, err := authtx.QueryTx(clientCtx, args[0]) + if err != nil { + return err + } + + if output.Empty() { + return fmt.Errorf("no transaction found with hash %s", args[0]) + } + + return clientCtx.PrintProto(output) + } + case typeSig: + { + sigParts, err := parseSigArgs(args) + if err != nil { + return err + } + tmEvents := make([]string, len(sigParts)) + for i, sig := range sigParts { + tmEvents[i] = fmt.Sprintf("%s.%s='%s'", sdk.EventTypeTx, sdk.AttributeKeySignature, sig) + } + + txs, err := authtx.QueryTxsByEvents(clientCtx, tmEvents, rest.DefaultPage, query.DefaultLimit, "") + if err != nil { + return err + } + if len(txs.Txs) == 0 { + return fmt.Errorf("found no txs matching given signatures") + } + if len(txs.Txs) > 1 { + // This case means there's a bug somewhere else in the code. Should not happen. + return errors.ErrLogic.Wrapf("found %d txs matching given signatures", len(txs.Txs)) + } + + return clientCtx.PrintProto(txs.Txs[0]) + } + case typeAccSeq: + { + if args[0] == "" { + return fmt.Errorf("`acc_seq` type takes an argument '/'") + } + + tmEvents := []string{ + fmt.Sprintf("%s.%s='%s'", sdk.EventTypeTx, sdk.AttributeKeyAccountSequence, args[0]), + } + txs, err := authtx.QueryTxsByEvents(clientCtx, tmEvents, rest.DefaultPage, query.DefaultLimit, "") + if err != nil { + return err + } + if len(txs.Txs) == 0 { + return fmt.Errorf("found no txs matching given address and sequence combination") + } + if len(txs.Txs) > 1 { + // This case means there's a bug somewhere else in the code. Should not happen. + return fmt.Errorf("found %d txs matching given address and sequence combination", len(txs.Txs)) + } + + return clientCtx.PrintProto(txs.Txs[0]) + } + default: + return fmt.Errorf("unknown --%s value %s", flagType, typ) + } + }, + } + + flags.AddQueryFlagsToCmd(cmd) + cmd.Flags().String(flagType, typeHash, fmt.Sprintf("The type to be used when querying tx, can be one of \"%s\", \"%s\", \"%s\"", typeHash, typeAccSeq, typeSig)) + + return cmd +} + +// parseSigArgs parses comma-separated signatures from the CLI arguments. +func parseSigArgs(args []string) ([]string, error) { if len(args) != 1 || args[0] == "" { - return nil, errors.New("argument should be comma-separated signatures") + return nil, fmt.Errorf("argument should be comma-separated signatures") } return strings.Split(args[0], ","), nil diff --git a/x/auth/client/cli/query_test.go b/x/auth/client/cli/query_test.go index 58ee64fba217..0168d3008179 100644 --- a/x/auth/client/cli/query_test.go +++ b/x/auth/client/cli/query_test.go @@ -1,11 +1,9 @@ -package cli_test +package cli import ( "testing" "github.com/stretchr/testify/require" - - "github.com/cosmos/cosmos-sdk/x/auth/client/cli" ) func TestParseSigs(t *testing.T) { @@ -23,7 +21,7 @@ func TestParseSigs(t *testing.T) { } for _, tc := range cases { - sigs, err := cli.ParseSigArgs(tc.args) + sigs, err := parseSigArgs(tc.args) if tc.expErr { require.Error(t, err) } else {