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

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

<!--
The default pull request template is for types feat, fix, or refactor.
For other templates, add one of the following parameters to the url:
- template=docs.md
- template=other.md
-->

## Description

Closes: #9741

<!-- Add a description of the changes that this PR introduces and the files that
are the most critical to review. -->

---

### Author Checklist

*All items are required. Please add a note to the item if the item is not applicable and
please add links to any relevant follow up issues.*

I have...

- [ ] included the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title
- [ ] added `!` to the type prefix if API or client breaking change
- [ ] targeted the correct branch (see [PR Targeting](https://github.com/cosmos/cosmos-sdk/blob/master/CONTRIBUTING.md#pr-targeting))
- [ ] provided a link to the relevant issue or specification
- [ ] followed the guidelines for [building modules](https://github.com/cosmos/cosmos-sdk/blob/master/docs/building-modules)
- [ ] included the necessary unit and integration [tests](https://github.com/cosmos/cosmos-sdk/blob/master/CONTRIBUTING.md#testing)
- [ ] added a changelog entry to `CHANGELOG.md`
- [ ] included comments for [documenting Go code](https://blog.golang.org/godoc)
- [ ] updated the relevant documentation or specification
- [ ] reviewed "Files changed" and left comments if necessary
- [ ] confirmed all CI checks have passed

### Reviewers Checklist

*All items are required. Please add a note if the item is not applicable and please add
your handle next to the items reviewed if you only reviewed selected items.*

I have...

- [ ] confirmed the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title
- [ ] confirmed `!` in the type prefix if API or client breaking change
- [ ] confirmed all author checklist items have been addressed
- [ ] reviewed state machine logic
- [ ] reviewed API design and naming
- [ ] reviewed documentation is accurate
- [ ] reviewed tests and test coverage
- [ ] manually tested (if applicable)

(cherry picked from commit 7c19434000)

# Conflicts:
#	CHANGELOG.md
#	x/auth/client/cli/query.go

* Fix conflicts

* Fix cl

* Fix conflicts

Co-authored-by: Amaury <1293565+amaurym@users.noreply.github.com>
This commit is contained in:
mergify[bot] 2021-07-27 17:39:41 +02:00 committed by GitHub
parent b402d1ce37
commit e7c5f6b1fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 334 additions and 18 deletions

View File

@ -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.

View File

@ -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"

View File

@ -2,6 +2,7 @@ package ante
import (
"bytes"
"encoding/base64"
"encoding/hex"
"fmt"
@ -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)
}
@ -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)
}
}

View File

@ -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"
@ -18,6 +20,11 @@ import (
const (
flagEvents = "events"
flagType = "type"
typeHash = "hash"
typeAccSeq = "acc_seq"
typeSig = "signature"
eventFormat = "{eventType}.{eventAttribute}={value}"
)
@ -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)
return clientCtx.PrintProto(output)
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 '<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
}

View File

@ -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))
}
}
}

View File

@ -2,6 +2,7 @@ package testutil
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
@ -243,7 +244,7 @@ func checkSignatures(require *require.Assertions, txCfg client.TxConfig, output
}
}
func (s *IntegrationTestSuite) TestCLIQueryTxCmd() {
func (s *IntegrationTestSuite) TestCLIQueryTxCmdByHash() {
val := s.network.Validators[0]
account2, err := val.ClientCtx.Keyring.Key("newAccount2")
@ -267,23 +268,26 @@ func (s *IntegrationTestSuite) TestCLIQueryTxCmd() {
expectErr bool
rawLogContains string
}{
{
"not enough args",
[]string{},
true, "",
},
{
"with invalid hash",
[]string{"somethinginvalid", fmt.Sprintf("--%s=json", tmcli.OutputFlag)},
true,
"",
true, "",
},
{
"with valid and not existing hash",
[]string{"C7E7D3A86A17AB3A321172239F3B61357937AF0F25D9FA4D2F4DCCAD9B0D7747", fmt.Sprintf("--%s=json", tmcli.OutputFlag)},
true,
"",
true, "",
},
{
"happy case",
[]string{txRes.TxHash, fmt.Sprintf("--%s=json", tmcli.OutputFlag)},
false,
"/cosmos.bank.v1beta1.MsgSend",
sdk.MsgTypeURL(&banktypes.MsgSend{}),
},
}
@ -308,6 +312,120 @@ func (s *IntegrationTestSuite) TestCLIQueryTxCmd() {
}
}
func (s *IntegrationTestSuite) TestCLIQueryTxCmdByEvents() {
val := s.network.Validators[0]
account2, err := val.ClientCtx.Keyring.Key("newAccount2")
s.Require().NoError(err)
sendTokens := sdk.NewInt64Coin(s.cfg.BondDenom, 10)
// Send coins.
out, err := s.createBankMsg(
val, account2.GetAddress(),
sdk.NewCoins(sendTokens),
)
s.Require().NoError(err)
var txRes sdk.TxResponse
s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &txRes))
s.Require().NoError(s.network.WaitForNextBlock())
// Query the tx by hash to get the inner tx.
out, err = clitestutil.ExecTestCLICmd(val.ClientCtx, authcli.QueryTxCmd(), []string{txRes.TxHash, fmt.Sprintf("--%s=json", tmcli.OutputFlag)})
s.Require().NoError(err)
s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &txRes))
protoTx := txRes.GetTx().(*tx.Tx)
testCases := []struct {
name string
args []string
expectErr bool
expectErrStr string
}{
{
"invalid --type",
[]string{
fmt.Sprintf("--type=%s", "foo"),
"bar",
fmt.Sprintf("--%s=json", tmcli.OutputFlag),
},
true, "unknown --type value foo",
},
{
"--type=acc_seq with no addr+seq",
[]string{
"--type=acc_seq",
"",
fmt.Sprintf("--%s=json", tmcli.OutputFlag),
},
true, "`acc_seq` type takes an argument '<addr>/<seq>'",
},
{
"non-existing addr+seq combo",
[]string{
"--type=acc_seq",
"foobar",
fmt.Sprintf("--%s=json", tmcli.OutputFlag),
},
true, "found no txs matching given address and sequence combination",
},
{
"addr+seq happy case",
[]string{
"--type=acc_seq",
fmt.Sprintf("%s/%d", val.Address, protoTx.AuthInfo.SignerInfos[0].Sequence),
fmt.Sprintf("--%s=json", tmcli.OutputFlag),
},
false, "",
},
{
"--type=signature with no signature",
[]string{
"--type=signature",
"",
fmt.Sprintf("--%s=json", tmcli.OutputFlag),
},
true, "argument should be comma-separated signatures",
},
{
"non-existing signatures",
[]string{
"--type=signature",
"foo",
fmt.Sprintf("--%s=json", tmcli.OutputFlag),
},
true, "found no txs matching given signatures",
},
{
"with --signatures happy case",
[]string{
"--type=signature",
base64.StdEncoding.EncodeToString(protoTx.Signatures[0]),
fmt.Sprintf("--%s=json", tmcli.OutputFlag),
},
false, "",
},
}
for _, tc := range testCases {
tc := tc
s.Run(tc.name, func() {
cmd := authcli.QueryTxCmd()
clientCtx := val.ClientCtx
out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args)
if tc.expectErr {
s.Require().Error(err)
s.Require().Contains(err.Error(), tc.expectErrStr)
} else {
var result sdk.TxResponse
s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &result))
s.Require().NotNil(result.Height)
}
})
}
}
func (s *IntegrationTestSuite) TestCLISendGenerateSignAndBroadcast() {
val1 := s.network.Validators[0]
@ -679,7 +797,7 @@ func (s *IntegrationTestSuite) TestCLIMultisign() {
sign1File := testutil.WriteToNewTempFile(s.T(), account1Signature.String())
// Sign with account1
// Sign with account2
account2Signature, err := TxSignExec(val1.ClientCtx, account2.GetAddress(), multiGeneratedTxFile.Name(), "--multisig", multisigInfo.GetAddress().String())
s.Require().NoError(err)