From 29948a27bb441ba3ba23272529d6c20809e53d29 Mon Sep 17 00:00:00 2001 From: Alessio Treglia Date: Fri, 5 Jun 2020 15:35:41 +0200 Subject: [PATCH] x/auth: add sign-batch command The command processes list of transactions from file (one StdTx each line), generate signed transactions or signatures and print their JSON encoding, delimited by '\n'. As the signatures are generated, the command incremnts the sequence number automatically. Author: @jgimeno Reviewed-by: @alessio --- CHANGELOG.md | 1 + Makefile | 8 +- simapp/cmd/simcli/main.go | 1 + x/auth/client/cli/cli_test.go | 51 +++++++++++ x/auth/client/cli/tx.go | 1 + x/auth/client/cli/tx_sign.go | 135 +++++++++++++++++++++++++++++- x/auth/client/testutil/helpers.go | 8 ++ x/auth/client/tx.go | 35 ++++++++ x/auth/client/tx_test.go | 51 ++++++++++- 9 files changed, 285 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e952b10bf2a..3a43707714a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -149,6 +149,7 @@ be used to retrieve the actual proposal `Content`. Also the `NewMsgSubmitProposa * (x/capability) [\#5828](https://github.com/cosmos/cosmos-sdk/pull/5828) Capability module integration as outlined in [ADR 3 - Dynamic Capability Store](https://github.com/cosmos/tree/master/docs/architecture/adr-003-dynamic-capability-store.md). * (x/params) [\#6005](https://github.com/cosmos/cosmos-sdk/pull/6005) Add new CLI command for querying raw x/params parameters by subspace and key. * (x/ibc) [\#5769](https://github.com/cosmos/cosmos-sdk/pull/5769) [ICS 009 - Loopback Client](https://github.com/cosmos/ics/tree/master/spec/ics-009-loopback-client) subpackage +* (x/auth) [\6350](https://github.com/cosmos/cosmos-sdk/pull/6350) New sign-batch command to sign StdTx batch files. ### Bug Fixes diff --git a/Makefile b/Makefile index 5841acd8cbaa..e8e3f62e7d64 100644 --- a/Makefile +++ b/Makefile @@ -48,13 +48,17 @@ mocks: $(MOCKS_DIR) $(MOCKS_DIR): mkdir -p $(MOCKS_DIR) -distclean: +distclean: clean rm -rf \ gitian-build-darwin/ \ gitian-build-linux/ \ gitian-build-windows/ \ .gitian-builder-cache/ -.PHONY: distclean + +clean: + rm -rf $(BUILDDIR)/ + +.PHONY: distclean clean ############################################################################### ### Tools & Dependencies ### diff --git a/simapp/cmd/simcli/main.go b/simapp/cmd/simcli/main.go index 234e5498871a..ee849b9189f3 100644 --- a/simapp/cmd/simcli/main.go +++ b/simapp/cmd/simcli/main.go @@ -132,6 +132,7 @@ func txCmd(cdc *codec.Codec) *cobra.Command { bankcmd.NewSendTxCmd(clientCtx), flags.LineBreak, authcmd.GetSignCommand(cdc), + authcmd.GetSignBatchCommand(cdc), authcmd.GetMultiSignCommand(cdc), authcmd.GetValidateSignaturesCommand(cdc), flags.LineBreak, diff --git a/x/auth/client/cli/cli_test.go b/x/auth/client/cli/cli_test.go index 66a7cc9f28ab..b831ee348a72 100644 --- a/x/auth/client/cli/cli_test.go +++ b/x/auth/client/cli/cli_test.go @@ -73,6 +73,57 @@ func TestCLIValidateSignatures(t *testing.T) { f.Cleanup() } +func TestCLISignBatch(t *testing.T) { + t.Parallel() + f := cli.InitFixtures(t) + + fooAddr := f.KeyAddress(cli.KeyFoo) + barAddr := f.KeyAddress(cli.KeyBar) + + sendTokens := sdk.TokensFromConsensusPower(10) + success, generatedStdTx, stderr := bankcli.TxSend(f, fooAddr.String(), barAddr, sdk.NewCoin(cli.Denom, sendTokens), "--generate-only") + + require.True(t, success) + require.Empty(t, stderr) + + // Write the output to disk + batchfile, cleanup1 := tests.WriteToNewTempFile(t, strings.Repeat(generatedStdTx, 3)) + t.Cleanup(cleanup1) + + // sign-batch file - offline is set but account-number and sequence are not + success, _, stderr = testutil.TxSignBatch(f, cli.KeyFoo, batchfile.Name(), "--offline") + require.Contains(t, stderr, "required flag(s) \"account-number\", \"sequence\" not set") + require.False(t, success) + + // sign-batch file + success, stdout, stderr := testutil.TxSignBatch(f, cli.KeyFoo, batchfile.Name()) + require.True(t, success) + require.Empty(t, stderr) + require.Equal(t, 3, len(strings.Split(strings.Trim(stdout, "\n"), "\n"))) + + // sign-batch file + success, stdout, stderr = testutil.TxSignBatch(f, cli.KeyFoo, batchfile.Name(), "--signature-only") + require.True(t, success) + require.Empty(t, stderr) + require.Equal(t, 3, len(strings.Split(strings.Trim(stdout, "\n"), "\n"))) + + malformedFile, cleanup2 := tests.WriteToNewTempFile(t, fmt.Sprintf("%smalformed", generatedStdTx)) + t.Cleanup(cleanup2) + + // sign-batch file + success, stdout, stderr = testutil.TxSignBatch(f, cli.KeyFoo, malformedFile.Name()) + require.False(t, success) + require.Equal(t, 1, len(strings.Split(strings.Trim(stdout, "\n"), "\n"))) + require.Equal(t, "ERROR: cannot parse disfix JSON wrapper: invalid character 'm' looking for beginning of value\n", stderr) + + // sign-batch file + success, stdout, _ = testutil.TxSignBatch(f, cli.KeyFoo, malformedFile.Name(), "--signature-only") + require.False(t, success) + require.Equal(t, 1, len(strings.Split(strings.Trim(stdout, "\n"), "\n"))) + + f.Cleanup() +} + func TestCLISendGenerateSignAndBroadcast(t *testing.T) { t.Parallel() f := cli.InitFixtures(t) diff --git a/x/auth/client/cli/tx.go b/x/auth/client/cli/tx.go index 742ab3547189..132d61ed3080 100644 --- a/x/auth/client/cli/tx.go +++ b/x/auth/client/cli/tx.go @@ -21,6 +21,7 @@ func GetTxCmd(cdc *codec.Codec) *cobra.Command { GetMultiSignCommand(cdc), GetSignCommand(cdc), GetValidateSignaturesCommand(cdc), + GetSignBatchCommand(cdc), ) return txCmd } diff --git a/x/auth/client/cli/tx_sign.go b/x/auth/client/cli/tx_sign.go index adc3ef8a297e..dbeabf05600a 100644 --- a/x/auth/client/cli/tx_sign.go +++ b/x/auth/client/cli/tx_sign.go @@ -1,16 +1,18 @@ package cli import ( + "bufio" "fmt" "os" "github.com/spf13/cobra" "github.com/spf13/viper" + "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/x/auth/client" + authclient "github.com/cosmos/cosmos-sdk/x/auth/client" "github.com/cosmos/cosmos-sdk/x/auth/types" ) @@ -20,6 +22,133 @@ const ( flagSigOnly = "signature-only" ) +// GetSignBatchCommand returns the transaction sign-batch command. +func GetSignBatchCommand(codec *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "sign-batch [file]", + Short: "Sign transaction batch files", + Long: `Sign batch files of transactions generated with --generate-only. +The command processes list of transactions from file (one StdTx each line), generate +signed transactions or signatures and print their JSON encoding, delimited by '\n'. +As the signatures are generated, the command updates the sequence number accordingly. + +If the flag --signature-only flag is set, it will output a JSON representation +of the generated signature only. + +The --offline flag makes sure that the client will not reach out to full node. +As a result, the account and the sequence number queries will not be performed and +it is required to set such parameters manually. Note, invalid values will cause +the transaction to fail. The sequence will be incremented automatically for each +transaction that is signed. + +The --multisig= flag generates a signature on behalf of a multisig +account key. It implies --signature-only. +`, + PreRun: preSignCmd, + RunE: makeSignBatchCmd(codec), + Args: cobra.ExactArgs(1), + } + + cmd.Flags().String( + flagMultisig, "", + "Address of the multisig account on behalf of which the transaction shall be signed", + ) + cmd.Flags().String(flags.FlagOutputDocument, "", "The document will be written to the given file instead of STDOUT") + cmd.Flags().Bool(flagSigOnly, true, "Print only the generated signature, then exit") + cmd = flags.PostCommands(cmd)[0] + cmd.MarkFlagRequired(flags.FlagFrom) + + return cmd +} + +func makeSignBatchCmd(cdc *codec.Codec) func(cmd *cobra.Command, args []string) error { + return func(cmd *cobra.Command, args []string) error { + inBuf := bufio.NewReader(cmd.InOrStdin()) + clientCtx := client.NewContextWithInput(inBuf).WithCodec(cdc) + txBldr := types.NewTxBuilderFromCLI(inBuf) + generateSignatureOnly := viper.GetBool(flagSigOnly) + + var ( + err error + multisigAddr sdk.AccAddress + infile = os.Stdin + ) + + // validate multisig address if there's any + if viper.GetString(flagMultisig) != "" { + multisigAddr, err = sdk.AccAddressFromBech32(viper.GetString(flagMultisig)) + if err != nil { + return err + } + } + + // prepare output document + closeFunc, err := setOutputFile(cmd) + if err != nil { + return err + } + + defer closeFunc() + clientCtx.WithOutput(cmd.OutOrStdout()) + + if args[0] != "-" { + infile, err = os.Open(args[0]) + if err != nil { + return err + } + } + + scanner := authclient.NewBatchScanner(cdc, infile) + + for sequence := txBldr.Sequence(); scanner.Scan(); sequence++ { + var stdTx types.StdTx + + unsignedStdTx := scanner.StdTx() + txBldr = txBldr.WithSequence(sequence) + + if multisigAddr.Empty() { + stdTx, err = authclient.SignStdTx(txBldr, clientCtx, viper.GetString(flags.FlagFrom), unsignedStdTx, false, true) + } else { + stdTx, err = authclient.SignStdTxWithSignerAddress(txBldr, clientCtx, multisigAddr, clientCtx.GetFromName(), unsignedStdTx, true) + } + + if err != nil { + return err + } + + json, err := getSignatureJSON(cdc, stdTx, clientCtx.Indent, generateSignatureOnly) + if err != nil { + return err + } + + cmd.Printf("%s\n", json) + } + + if err := scanner.UnmarshalErr(); err != nil { + return err + } + + return scanner.Err() + } +} + +func setOutputFile(cmd *cobra.Command) (func(), error) { + outputDoc := viper.GetString(flags.FlagOutputDocument) + if outputDoc == "" { + cmd.SetOut(cmd.OutOrStdout()) + return func() {}, nil + } + + fp, err := os.OpenFile(outputDoc, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + return func() {}, err + } + + cmd.SetOut(fp) + + return func() { fp.Close() }, nil +} + // GetSignCommand returns the transaction sign command. func GetSignCommand(codec *codec.Codec) *cobra.Command { cmd := &cobra.Command{ @@ -89,13 +218,13 @@ func makeSignCmd(cdc *codec.Codec) func(cmd *cobra.Command, args []string) error if err != nil { return err } - newTx, err = client.SignStdTxWithSignerAddress( + newTx, err = authclient.SignStdTxWithSignerAddress( txBldr, clientCtx, multisigAddr, clientCtx.GetFromName(), stdTx, clientCtx.Offline, ) generateSignatureOnly = true } else { appendSig := viper.GetBool(flagAppend) && !generateSignatureOnly - newTx, err = client.SignStdTx(txBldr, clientCtx, clientCtx.GetFromName(), stdTx, appendSig, clientCtx.Offline) + newTx, err = authclient.SignStdTx(txBldr, clientCtx, clientCtx.GetFromName(), stdTx, appendSig, clientCtx.Offline) } if err != nil { diff --git a/x/auth/client/testutil/helpers.go b/x/auth/client/testutil/helpers.go index 20faccf95ac8..d04c51b9dbd8 100644 --- a/x/auth/client/testutil/helpers.go +++ b/x/auth/client/testutil/helpers.go @@ -44,3 +44,11 @@ func TxMultisign(f *cli.Fixtures, fileName, name string, signaturesFiles []strin ) return cli.ExecuteWriteRetStdStreams(f.T, cli.AddFlags(cmd, flags)) } + +func TxSignBatch(f *cli.Fixtures, signer, fileName string, flags ...string) (bool, string, string) { + cmd := fmt.Sprintf("%s tx sign-batch %v --keyring-backend=test --from=%s %v", f.SimcliBinary, f.Flags(), signer, fileName) + + return cli.ExecuteWriteRetStdStreams(f.T, cli.AddFlags(cmd, flags), clientkeys.DefaultKeyPass) +} + +// DONTCOVER diff --git a/x/auth/client/tx.go b/x/auth/client/tx.go index a503854e7594..d8dfb531b051 100644 --- a/x/auth/client/tx.go +++ b/x/auth/client/tx.go @@ -4,6 +4,7 @@ import ( "bufio" "bytes" "fmt" + "io" "io/ioutil" "os" "strings" @@ -250,6 +251,40 @@ func ReadStdTxFromFile(cdc *codec.Codec, filename string) (stdTx authtypes.StdTx return } +// NewBatchScanner returns a new BatchScanner to read newline-delimited StdTx transactions from r. +func NewBatchScanner(cdc *codec.Codec, r io.Reader) *BatchScanner { + return &BatchScanner{Scanner: bufio.NewScanner(r), cdc: cdc} +} + +// BatchScanner provides a convenient interface for reading batch data such as a file +// of newline-delimited JSON encoded StdTx. +type BatchScanner struct { + *bufio.Scanner + stdTx authtypes.StdTx + cdc *codec.Codec + unmarshalErr error +} + +// StdTx returns the most recent StdTx unmarshalled by a call to Scan. +func (bs BatchScanner) StdTx() authtypes.StdTx { return bs.stdTx } + +// UnmarshalErr returns the first unmarshalling error that was encountered by the scanner. +func (bs BatchScanner) UnmarshalErr() error { return bs.unmarshalErr } + +// Scan advances the Scanner to the next line. +func (bs *BatchScanner) Scan() bool { + if !bs.Scanner.Scan() { + return false + } + + if err := bs.cdc.UnmarshalJSON(bs.Bytes(), &bs.stdTx); err != nil && bs.unmarshalErr == nil { + bs.unmarshalErr = err + return false + } + + return true +} + func populateAccountFromState( txBldr authtypes.TxBuilder, clientCtx client.Context, addr sdk.AccAddress, ) (authtypes.TxBuilder, error) { diff --git a/x/auth/client/tx_test.go b/x/auth/client/tx_test.go index 740f87499b30..3e4bd6adec79 100644 --- a/x/auth/client/tx_test.go +++ b/x/auth/client/tx_test.go @@ -5,10 +5,10 @@ import ( "errors" "io/ioutil" "os" + "strings" "testing" "github.com/stretchr/testify/require" - "github.com/tendermint/tendermint/crypto/ed25519" "github.com/cosmos/cosmos-sdk/codec" @@ -117,6 +117,7 @@ func TestConfiguredTxEncoder(t *testing.T) { } func TestReadStdTxFromFile(t *testing.T) { + t.Parallel() cdc := codec.New() sdk.RegisterCodec(cdc) @@ -135,6 +136,54 @@ func TestReadStdTxFromFile(t *testing.T) { require.Equal(t, decodedTx.Memo, "foomemo") } +func TestBatchScanner_Scan(t *testing.T) { + t.Parallel() + cdc := codec.New() + sdk.RegisterCodec(cdc) + + batch1 := `{"msg":[],"fee":{"amount":[{"denom":"atom","amount":"150"}],"gas":"50000"},"signatures":[],"memo":"foomemo"} +{"msg":[],"fee":{"amount":[{"denom":"atom","amount":"150"}],"gas":"10000"},"signatures":[],"memo":"foomemo"} +{"msg":[],"fee":{"amount":[{"denom":"atom","amount":"1"}],"gas":"10000"},"signatures":[],"memo":"foomemo"} +` + batch2 := `{"msg":[],"fee":{"amount":[{"denom":"atom","amount":"150"}],"gas":"50000"},"signatures":[],"memo":"foomemo"} +malformed +{"msg":[],"fee":{"amount":[{"denom":"atom","amount":"1"}],"gas":"10000"},"signatures":[],"memo":"foomemo"} +` + batch3 := `{"msg":[],"fee":{"amount":[{"denom":"atom","amount":"150"}],"gas":"50000"},"signatures":[],"memo":"foomemo"} +{"msg":[],"fee":{"amount":[{"denom":"atom","amount":"1"}],"gas":"10000"},"signatures":[],"memo":"foomemo"}` + batch4 := `{"msg":[],"fee":{"amount":[{"denom":"atom","amount":"150"}],"gas":"50000"},"signatures":[],"memo":"foomemo"} + +{"msg":[],"fee":{"amount":[{"denom":"atom","amount":"1"}],"gas":"10000"},"signatures":[],"memo":"foomemo"} +` + tests := []struct { + name string + batch string + wantScannerError bool + wantUnmarshalError bool + numTxs int + }{ + {"good batch", batch1, false, false, 3}, + {"malformed", batch2, false, true, 1}, + {"missing trailing newline", batch3, false, false, 2}, + {"empty line", batch4, false, true, 1}, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + scanner, i := NewBatchScanner(cdc, strings.NewReader(tt.batch)), 0 + for scanner.Scan() { + _ = scanner.StdTx() + i++ + } + + require.Equal(t, tt.wantScannerError, scanner.Err() != nil) + require.Equal(t, tt.wantUnmarshalError, scanner.UnmarshalErr() != nil) + require.Equal(t, tt.numTxs, i) + }) + } +} + func compareEncoders(t *testing.T, expected sdk.TxEncoder, actual sdk.TxEncoder) { msgs := []sdk.Msg{sdk.NewTestMsg(addr)} tx := authtypes.NewStdTx(msgs, authtypes.StdFee{}, []authtypes.StdSignature{}, "")