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

Sign Batch Transactions #2

Merged
merged 13 commits into from
May 25, 2020
Merged
101 changes: 101 additions & 0 deletions x/auth/client/cli/batch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package cli

import (
"fmt"
"io"
"os"

"github.com/pkg/errors"
"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/client/keys"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/x/auth/client/utils"
"github.com/cosmos/cosmos-sdk/x/auth/types"
)

const (
FlagPassPhrase = "passphrase"
)

func GetBatchSignCommand(codec *codec.Codec) *cobra.Command {
cmd := &cobra.Command{
Use: "sign-batch [in-file]",
Short: "Sign many standard transactions generated offline",
Long: `Sign a list of transactions created with the --generate-only flag.
It will read StdSignDoc JSONs from [in-file], one transaction per line, and
produce a file of JSON encoded StdSignatures, one per line.

This command is intended to work offline for security purposes.`,
PreRun: preSignCmd,
RunE: makeBatchSignCmd(codec),
Args: cobra.ExactArgs(1),
}

cmd.Flags().String(FlagPassPhrase, "", "The passphrase of the key needed to sign the transaction.")
cmd.Flags().String(client.FlagOutputDocument, "",
"write the resulto to the given file instead of the default location")

return flags.PostCommands(cmd)[0]
}

func makeBatchSignCmd(cdc *codec.Codec) func(cmd *cobra.Command, args []string) error {
return func(cmd *cobra.Command, args []string) error {
kb, err := keys.NewKeyBaseFromDir(viper.GetString(flags.FlagHome))
if err != nil {
return err
}

out, err := setOutput()
if err != nil {
return errors.Wrap(err, "error with output")
}

from, err := kb.Get(viper.GetString(flags.FlagFrom))
if err != nil {
return errors.Wrap(err, "key not found")
}

txs, err := utils.ReadStdTxsFromFile(cdc, args[0])
if err != nil {
return errors.Wrap(err, "error extracting txs from file")
}

passphrase := viper.GetString(FlagPassPhrase)
for _, tx := range txs {
signature, err := types.MakeSignature(nil, from.GetName(), passphrase, tx)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("error signing tx %d", tx.Sequence))
}

json, err := cdc.MarshalJSON(signature)
if err != nil {
return errors.Wrap(err, "error marshalling signature")
}

_, err = fmt.Fprintf(out, "%s\n", json)
if err != nil {
return errors.Wrap(err, "error writing to output")
}
}

return nil
}
}

func setOutput() (io.Writer, error) {
outputFlag := viper.GetString(client.FlagOutputDocument)
if outputFlag == "" {
return os.Stdout, nil
}

out, err := os.OpenFile(outputFlag, os.O_RDWR, 0644)
if err != nil {
return nil, err
}

return out, nil
}
237 changes: 237 additions & 0 deletions x/auth/client/cli/batch_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
package cli_test

import (
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"testing"

"github.com/spf13/viper"
"github.com/stretchr/testify/require"
"github.com/tendermint/go-amino"
"github.com/tendermint/tendermint/crypto/multisig"

"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/client/keys"
"github.com/cosmos/cosmos-sdk/codec"
keys2 "github.com/cosmos/cosmos-sdk/crypto/keys"
"github.com/cosmos/cosmos-sdk/tests"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth"
"github.com/cosmos/cosmos-sdk/x/auth/client/cli"
"github.com/cosmos/cosmos-sdk/x/staking"
"github.com/cosmos/go-bip39"
"github.com/tendermint/tendermint/crypto"
)

const passphrase = "012345678"

func TestGetBatchSignCommand(t *testing.T) {
cdc := amino.NewCodec()
sdk.RegisterCodec(cdc)
codec.RegisterCrypto(cdc)
staking.RegisterCodec(cdc)

cmd := cli.GetBatchSignCommand(cdc)

tempDir, cleanFunc := tests.NewTestCaseDir(t)
t.Cleanup(cleanFunc)

outputFile, err := os.Create(filepath.Join(tempDir, "the-output"))
require.NoError(t, err)
defer outputFile.Close()

kb, _, err := createKeybaseWithMultisigAccount(tempDir)
require.NoError(t, err)

multiInfo, err := kb.Get("multi")
require.NoError(t, err)

viper.Reset()
viper.Set(flags.FlagHome, tempDir)
viper.Set(flags.FlagFrom, "acc1")
viper.Set(cli.FlagMultisig, multiInfo.GetAddress())
viper.Set(cli.FlagPassPhrase, passphrase)
viper.Set(flags.FlagOutputDocument, outputFile.Name())

cmd.SetArgs([]string{
"./testdata/txs.json",
})

err = cmd.Execute()
require.NoError(t, err)

// Validate Result
inputFile, err := os.Open("./testdata/txs.json")
require.NoError(t, err)

validateSignatures(t, cdc, inputFile, outputFile)
}

func validateSignatures(t *testing.T, cdc *codec.Codec, inputFile io.Reader, outputFile io.Reader) {
inputData, err := ioutil.ReadAll(inputFile)
require.NoError(t, err)

outputData, err := ioutil.ReadAll(outputFile)
require.NoError(t, err)

txs := extractTxs(t, cdc, inputData)
signatures := extractSignatures(t, cdc, outputData)

if len(txs) != len(signatures) {
t.Errorf("must be same amount of txs and signatures: '%d' txs, '%d' signatures", len(txs), len(signatures))
}

for i := 0; i < len(txs); i++ {
require.True(t, signatures[i].PubKey.VerifyBytes(txs[i].Bytes(), signatures[i].Signature))
}
}

func extractTxs(t *testing.T, cdc *codec.Codec, inputData []byte) []auth.StdSignMsg {
inputLines := strings.Split(string(inputData), "\n")

var parsedTxs []auth.StdSignMsg
for _, txLine := range inputLines {
if len(txLine) == 0 {
break
}

var parsedTx auth.StdSignMsg

err := cdc.UnmarshalJSON([]byte(txLine), &parsedTx)
if err != nil {
t.Errorf("error extracting tx: %s", err)
}

parsedTxs = append(parsedTxs, parsedTx)
}

return parsedTxs
}

func extractSignatures(t *testing.T, cdc *codec.Codec, outputData []byte) []auth.StdSignature {
outputLines := strings.Split(string(outputData), "\n")

var parsedSigs []auth.StdSignature
for _, sigLine := range outputLines {
if len(sigLine) == 0 {
break
}

var parsedSig auth.StdSignature

err := cdc.UnmarshalJSON([]byte(sigLine), &parsedSig)
if err != nil {
t.Errorf("error extracting tx: %s", err)
}

parsedSigs = append(parsedSigs, parsedSig)
}

return parsedSigs
}

func createKeybaseWithMultisigAccount(dir string) (keys2.Keybase, []crypto.PubKey, error) {
kb, err := keys.NewKeyBaseFromDir(dir)
if err != nil {
return nil, nil, err
}

var pubKeys []crypto.PubKey
for i := 0; i < 4; i++ {
entropySeed, err := bip39.NewEntropy(256)
if err != nil {
return nil, nil, err
}

mnemonic, err := bip39.NewMnemonic(entropySeed[:])
if err != nil {
return nil, nil, err
}

account, err := kb.CreateAccount(
fmt.Sprintf("acc%d", i),
mnemonic,
"",
passphrase,
0,
0,
)
if err != nil {
return nil, nil, err
}

pubKeys = append(pubKeys, account.GetPubKey())
}

pk := multisig.NewPubKeyMultisigThreshold(2, pubKeys)
if _, err := kb.CreateMulti("multi", pk); err != nil {
return nil, nil, err
}

return kb, pubKeys, nil
}

func TestGetBatchSignCommand_Error(t *testing.T) {
tts := []struct {
name string
errorContains string
keybasePrep func(tempDir string)
providedFlags map[string]interface{}
}{
{
name: "not existing key",
errorContains: "key not found: Key not-existing not found",
keybasePrep: func(tempDir string) {
},
providedFlags: map[string]interface{}{
flags.FlagFrom: "not-existing",
},
},
{
name: "invalid passphrase",
errorContains: "invalid account password",
keybasePrep: func(tempDir string) {
createKeybaseWithMultisigAccount(tempDir)
},
providedFlags: map[string]interface{}{
flags.FlagFrom: "acc1",
},
},
}

cdc := amino.NewCodec()
sdk.RegisterCodec(cdc)
staking.RegisterCodec(cdc)

for _, tt := range tts {
tt := tt
tempDir, cleanFunc := tests.NewTestCaseDir(t)

t.Run(tt.name, func(t *testing.T) {
defer cleanFunc()

cmd := cli.GetBatchSignCommand(cdc)

tt.keybasePrep(tempDir)

viper.Reset()
viper.Set(flags.FlagHome, tempDir)

for key, val := range tt.providedFlags {
viper.Set(key, val)
}

cmd.SetArgs([]string{
"./testdata/txs.json",
})

err := cmd.Execute()
require.Error(t, err)
require.Contains(t, err.Error(), tt.errorContains)
})
}
}
2 changes: 2 additions & 0 deletions x/auth/client/cli/testdata/txs.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{"account_number":"24263","chain_id":"cosmoshub-3","fee":{"amount":[{"amount":"100000","denom":"uatom"}],"gas":"333000"},"memo":" ","msgs":[{"type":"cosmos-sdk/MsgUndelegate","value":{"amount":{"amount":"1300000000000","denom":"uatom"},"delegator_address":"cosmos176m2p8l3fps3dal7h8gf9jvrv98tu3rqfdht86","validator_address":"cosmosvaloper1qs8tnw2t8l6amtzvdemnnsq9dzk0ag0z52uzay"}}],"sequence":"37"}
{"account_number":"24263","chain_id":"cosmoshub-3","fee":{"amount":[{"amount":"100000","denom":"uatom"}],"gas":"333000"},"memo":" ","msgs":[{"type":"cosmos-sdk/MsgUndelegate","value":{"amount":{"amount":"1300000000000","denom":"uatom"},"delegator_address":"cosmos176m2p8l3fps3dal7h8gf9jvrv98tu3rqfdht86","validator_address":"cosmosvaloper13sduv92y3xdhy3rpmhakrc3v7t37e7ps9l0kpv"}}],"sequence":"38"}
6 changes: 3 additions & 3 deletions x/auth/client/cli/tx_sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (
)

const (
flagMultisig = "multisig"
FlagMultisig = "multisig"
flagAppend = "append"
flagValidateSigs = "validate-signatures"
flagOffline = "offline"
Expand Down Expand Up @@ -58,7 +58,7 @@ be generated via the 'multisign' command.
}

cmd.Flags().String(
flagMultisig, "",
FlagMultisig, "",
"Address of the multisig account on behalf of which the transaction shall be signed",
)
cmd.Flags().Bool(
Expand Down Expand Up @@ -113,7 +113,7 @@ func makeSignCmd(cdc *codec.Codec) func(cmd *cobra.Command, args []string) error
// if --signature-only is on, then override --append
var newTx types.StdTx
generateSignatureOnly := viper.GetBool(flagSigOnly)
multisigAddrStr := viper.GetString(flagMultisig)
multisigAddrStr := viper.GetString(FlagMultisig)

if multisigAddrStr != "" {
var multisigAddr sdk.AccAddress
Expand Down
2 changes: 2 additions & 0 deletions x/auth/client/utils/testdata/txs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{"account_number":"24263","chain_id":"cosmoshub-3","fee":{"amount":[{"amount":"100000","denom":"uatom"}],"gas":"333000"},"memo":" ","msgs":[{"type":"cosmos-sdk/MsgUndelegate","value":{"amount":{"amount":"1300000000000","denom":"uatom"},"delegator_address":"cosmos176m2p8l3fps3dal7h8gf9jvrv98tu3rqfdht86","validator_address":"cosmosvaloper1qs8tnw2t8l6amtzvdemnnsq9dzk0ag0z52uzay"}}],"sequence":"37"}
{"account_number":"24263","chain_id":"cosmoshub-3","fee":{"amount":[{"amount":"100000","denom":"uatom"}],"gas":"333000"},"memo":" ","msgs":[{"type":"cosmos-sdk/MsgUndelegate","value":{"amount":{"amount":"1300000000000","denom":"uatom"},"delegator_address":"cosmos176m2p8l3fps3dal7h8gf9jvrv98tu3rqfdht86","validator_address":"cosmosvaloper13sduv92y3xdhy3rpmhakrc3v7t37e7ps9l0kpv"}}],"sequence":"38"}
Loading