Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Query txs by signature and by address+seq (backport #9750) #9782

Merged
merged 5 commits into from
Jul 27, 2021
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,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='<base64_sig>'`) or by address and sequence combo (`tx.acc_seq='<addr>/<seq>'`).

### Improvements

* (cli) [\#9717](https://github.com/cosmos/cosmos-sdk/pull/9717) Added CLI flag `--output json/text` to `tx` cli commands.
Expand Down
5 changes: 5 additions & 0 deletions types/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,11 @@ func toBytes(i interface{}) []byte {

// Common event types and attribute keys
var (
EventTypeTx = "tx"

AttributeKeyAccountSequence = "acc_seq"
AttributeKeySignature = "signature"

EventTypeMessage = "message"

AttributeKeyAction = "action"
Expand Down
68 changes: 68 additions & 0 deletions x/auth/ante/sigverify.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package ante

import (
"bytes"
"encoding/base64"
"encoding/hex"
"fmt"

Expand Down Expand Up @@ -94,6 +95,34 @@ func (spkd SetPubKeyDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate b
spkd.ak.SetAccount(ctx, acc)
}

// Also emit the following events, so that txs can be indexed by these
// indices:
// - signature (via `tx.signature='<sig_as_base64>'`),
// - 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)
}

Expand Down Expand Up @@ -447,3 +476,42 @@ func CountSubKeys(pub cryptotypes.PubKey) int {

return numKeys
}

// signatureDataToBz converts a SignatureData into raw bytes signature.
// For SingleSignatureData, it returns the signature raw bytes.
// For MultiSignatureData, it returns an array of all individual signatures,
// as well as the aggregated signature.
func signatureDataToBz(data signing.SignatureData) ([][]byte, error) {
if data == nil {
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

for _, d := range data.Signatures {
nestedSigs, err := signatureDataToBz(d)
if err != nil {
return nil, err
}
sigs = append(sigs, nestedSigs...)
}

multisig := cryptotypes.MultiSignature{
Signatures: sigs,
}
aggregatedSig, err := multisig.Marshal()
if err != nil {
return nil, err
}
sigs = append(sigs, aggregatedSig)

return sigs, nil
default:
return nil, sdkerrors.ErrInvalidType.Wrapf("unexpected signature data type %T", data)
}
}
111 changes: 100 additions & 11 deletions x/auth/client/cli/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"
sdk "github.com/cosmos/cosmos-sdk/types"
"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"
Expand All @@ -18,6 +20,11 @@ import (

const (
flagEvents = "events"
flagType = "type"

typeHash = "hash"
typeAccSeq = "acc_seq"
typeSig = "signature"

eventFormat = "{eventType}.{eventAttribute}={value}"
)
Expand Down Expand Up @@ -210,28 +217,110 @@ $ %s query txs --%s 'message.sender=cosmos1...&message.action=withdraw_delegator
// QueryTxCmd implements the default command for a tx query.
func QueryTxCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "tx [hash]",
Short: "Query for a transaction by hash in a committed block",
Args: cobra.ExactArgs(1),
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 <hash>
$ %s query tx --%s=%s <addr>:<sequence>
$ %s query tx --%s=%s <sig1_base64,sig2_base64...>
`,
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
}
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])
}
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
}

return clientCtx.PrintProto(output)
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 '<addr>/<seq>'")
}

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, fmt.Errorf("argument should be comma-separated signatures")
}

return strings.Split(args[0], ","), nil
}
32 changes: 32 additions & 0 deletions x/auth/client/cli/query_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package cli

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestParseSigs(t *testing.T) {
cases := []struct {
name string
args []string
expErr bool
expNumSigs int
}{
{"no args", []string{}, true, 0},
{"empty args", []string{""}, true, 0},
{"too many args", []string{"foo", "bar"}, true, 0},
{"1 sig", []string{"foo"}, false, 1},
{"3 sigs", []string{"foo,bar,baz"}, false, 3},
}

for _, tc := range cases {
sigs, err := parseSigArgs(tc.args)
if tc.expErr {
require.Error(t, err)
} else {
require.NoError(t, err)
require.Equal(t, tc.expNumSigs, len(sigs))
}
}
}
Loading