x/auth: add sign-batch command (#6350)
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 increments the sequence number automatically. Author: @jgimeno Reviewed-by: @alessio
This commit is contained in:
parent
b618e0a827
commit
65ea305336
|
@ -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
|
||||
|
||||
|
|
8
Makefile
8
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 ###
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -21,6 +21,7 @@ func GetTxCmd(cdc *codec.Codec) *cobra.Command {
|
|||
GetMultiSignCommand(cdc),
|
||||
GetSignCommand(cdc),
|
||||
GetValidateSignaturesCommand(cdc),
|
||||
GetSignBatchCommand(cdc),
|
||||
)
|
||||
return txCmd
|
||||
}
|
||||
|
|
|
@ -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=<multisig_key> 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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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{}, "")
|
||||
|
|
Loading…
Reference in New Issue