diff --git a/CHANGELOG.md b/CHANGELOG.md index 7945d661057c..1b505678d5ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -169,6 +169,10 @@ Buffers for state serialization instead of Amino. * (server) [\#5709](https://github.com/cosmos/cosmos-sdk/pull/5709) There are two new flags for pruning, `--pruning-keep-every` and `--pruning-snapshot-every` as an alternative to `--pruning`. They allow to fine tune the strategy for pruning the state. * (crypto/keys) [\#5739](https://github.com/cosmos/cosmos-sdk/pull/5739) Print an error message if the password input failed. +* (client) [\#5810](https://github.com/cosmos/cosmos-sdk/pull/5810) Added a new `--offline` flag that allows commands to +be executed without an internet connection. Previously, `--generate-only` served this purpose in addition to only allowing +txs to be generated. Now, `--generate-only` solely allows txs to be generated without being broadcasted and disallows +Keybase use and `--offline` allows the use of Keybase but does not allow any functionality that requires an online connection. ## [v0.38.1] - 2020-02-11 diff --git a/client/context/context.go b/client/context/context.go index ae0576122602..80813b7655cd 100644 --- a/client/context/context.go +++ b/client/context/context.go @@ -43,6 +43,7 @@ type CLIContext struct { UseLedger bool Simulate bool GenerateOnly bool + Offline bool Indent bool SkipConfirm bool @@ -67,7 +68,8 @@ func NewCLIContextWithInputAndFrom(input io.Reader, from string) CLIContext { os.Exit(1) } - if !genOnly { + offline := viper.GetBool(flags.FlagOffline) + if !offline { nodeURI = viper.GetString(flags.FlagNode) if nodeURI != "" { rpc, err = rpcclient.NewHTTP(nodeURI, "/websocket") @@ -93,6 +95,7 @@ func NewCLIContextWithInputAndFrom(input io.Reader, from string) CLIContext { BroadcastMode: viper.GetString(flags.FlagBroadcastMode), Simulate: viper.GetBool(flags.FlagDryRun), GenerateOnly: genOnly, + Offline: offline, FromAddress: fromAddress, FromName: fromName, Indent: viper.GetBool(flags.FlagIndentResponse), @@ -286,7 +289,7 @@ func GetFromFields(input io.Reader, from string, genOnly bool) (sdk.AccAddress, if genOnly { addr, err := sdk.AccAddressFromBech32(from) if err != nil { - return nil, "", errors.Wrap(err, "must provide a valid Bech32 address for generate-only") + return nil, "", errors.Wrap(err, "must provide a valid Bech32 address in generate-only mode") } return addr, "", nil diff --git a/client/context/context_test.go b/client/context/context_test.go new file mode 100644 index 000000000000..3b6ea0e71af5 --- /dev/null +++ b/client/context/context_test.go @@ -0,0 +1,68 @@ +package context + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/spf13/viper" + "github.com/stretchr/testify/require" + + "github.com/cosmos/cosmos-sdk/client/flags" +) + +func TestCLIContext_WithOffline(t *testing.T) { + viper.Set(flags.FlagOffline, true) + viper.Set(flags.FlagNode, "tcp://localhost:26657") + + ctx := NewCLIContext() + require.True(t, ctx.Offline) + require.Nil(t, ctx.Client) + + viper.Reset() + + viper.Set(flags.FlagOffline, false) + viper.Set(flags.FlagNode, "tcp://localhost:26657") + + ctx = NewCLIContext() + require.False(t, ctx.Offline) + require.NotNil(t, ctx.Client) +} + +func TestCLIContext_WithGenOnly(t *testing.T) { + viper.Set(flags.FlagGenerateOnly, true) + + validFromAddr := "cosmos1q7380u26f7ntke3facjmynajs4umlr329vr4ja" + fromAddr, err := sdk.AccAddressFromBech32(validFromAddr) + require.NoError(t, err) + + tests := []struct { + name string + from string + expectedFromAddr sdk.AccAddress + expectedFromName string + }{ + { + name: "valid from", + from: validFromAddr, + expectedFromAddr: fromAddr, + expectedFromName: "", + }, + { + name: "empty from", + from: "", + expectedFromAddr: nil, + expectedFromName: "", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + ctx := NewCLIContextWithFrom(tt.from) + + require.Equal(t, tt.expectedFromAddr, ctx.FromAddress) + require.Equal(t, tt.expectedFromName, ctx.FromName) + }) + } +} diff --git a/client/context/query.go b/client/context/query.go index cfe08969f285..4d335c85d512 100644 --- a/client/context/query.go +++ b/client/context/query.go @@ -22,7 +22,7 @@ import ( // error is returned. func (ctx CLIContext) GetNode() (rpcclient.Client, error) { if ctx.Client == nil { - return nil, errors.New("no RPC client defined") + return nil, errors.New("no RPC client is defined in offline mode") } return ctx.Client, nil diff --git a/client/flags/flags.go b/client/flags/flags.go index 746b512b0697..d331a44bbcb6 100644 --- a/client/flags/flags.go +++ b/client/flags/flags.go @@ -57,6 +57,7 @@ const ( FlagBroadcastMode = "broadcast-mode" FlagDryRun = "dry-run" FlagGenerateOnly = "generate-only" + FlagOffline = "offline" FlagIndentResponse = "indent" FlagListenAddr = "laddr" FlagMaxOpenConnections = "max-open" @@ -114,7 +115,8 @@ func PostCommands(cmds ...*cobra.Command) []*cobra.Command { c.Flags().StringP(FlagBroadcastMode, "b", BroadcastSync, "Transaction broadcasting mode (sync|async|block)") c.Flags().Bool(FlagTrustNode, true, "Trust connected full node (don't verify proofs for responses)") c.Flags().Bool(FlagDryRun, false, "ignore the --gas flag and perform a simulation of a transaction, but don't broadcast it") - c.Flags().Bool(FlagGenerateOnly, false, "Build an unsigned transaction and write it to STDOUT (when enabled, the local Keybase is not accessible and the node operates offline)") + c.Flags().Bool(FlagGenerateOnly, false, "Build an unsigned transaction and write it to STDOUT (when enabled, the local Keybase is not accessible)") + c.Flags().Bool(FlagOffline, false, "Offline mode (does not allow any online functionality") c.Flags().BoolP(FlagSkipConfirmation, "y", false, "Skip tx broadcasting prompt confirmation") c.Flags().String(FlagKeyringBackend, DefaultKeyringBackend, "Select keyring's backend (os|file|test)") diff --git a/x/auth/client/cli/broadcast.go b/x/auth/client/cli/broadcast.go index 288b7c7de911..5f1e4757d1d7 100644 --- a/x/auth/client/cli/broadcast.go +++ b/x/auth/client/cli/broadcast.go @@ -1,6 +1,7 @@ package cli import ( + "errors" "strings" "github.com/spf13/cobra" @@ -26,6 +27,11 @@ $ tx broadcast ./mytxn.json Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) (err error) { cliCtx := context.NewCLIContext().WithCodec(cdc) + + if cliCtx.Offline { + return errors.New("cannot broadcast tx during offline mode") + } + stdTx, err := client.ReadStdTxFromFile(cliCtx.Codec, args[0]) if err != nil { return diff --git a/x/auth/client/cli/broadcast_test.go b/x/auth/client/cli/broadcast_test.go new file mode 100644 index 000000000000..d75a0b0c8908 --- /dev/null +++ b/x/auth/client/cli/broadcast_test.go @@ -0,0 +1,45 @@ +package cli + +import ( + "io/ioutil" + "path/filepath" + "testing" + + "github.com/spf13/viper" + "github.com/stretchr/testify/require" + "github.com/tendermint/go-amino" + + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/tests" +) + +func TestGetBroadcastCommand_OfflineFlag(t *testing.T) { + codec := amino.NewCodec() + cmd := GetBroadcastCommand(codec) + + viper.Set(flags.FlagOffline, true) + + err := cmd.RunE(nil, []string{}) + require.EqualError(t, err, "cannot broadcast tx during offline mode") +} + +func TestGetBroadcastCommand_WithoutOfflineFlag(t *testing.T) { + codec := amino.NewCodec() + cmd := GetBroadcastCommand(codec) + + viper.Set(flags.FlagOffline, false) + + testDir, cleanFunc := tests.NewTestCaseDir(t) + t.Cleanup(cleanFunc) + + // Create new file with tx + txContents := []byte("{\"type\":\"cosmos-sdk/StdTx\",\"value\":{\"msg\":[{\"type\":\"cosmos-sdk/MsgSend\",\"value\":{\"from_address\":\"cosmos1cxlt8kznps92fwu3j6npahx4mjfutydyene2qw\",\"to_address\":\"cosmos1wc8mpr8m3sy3ap3j7fsgqfzx36um05pystems4\",\"amount\":[{\"denom\":\"stake\",\"amount\":\"10000\"}]}}],\"fee\":{\"amount\":[],\"gas\":\"200000\"},\"signatures\":null,\"memo\":\"\"}}") + txFileName := filepath.Join(testDir, "tx.json") + err := ioutil.WriteFile(txFileName, txContents, 0644) + require.NoError(t, err) + + err = cmd.RunE(cmd, []string{txFileName}) + + // We test it tries to broadcast but we set unsupported tx to get the error. + require.EqualError(t, err, "unsupported return type ; supported types: sync, async, block") +} diff --git a/x/auth/client/cli/tx_multisign.go b/x/auth/client/cli/tx_multisign.go index f5bf16ca279e..6018ce2a7a4f 100644 --- a/x/auth/client/cli/tx_multisign.go +++ b/x/auth/client/cli/tx_multisign.go @@ -51,7 +51,6 @@ recommended to set such parameters manually. } cmd.Flags().Bool(flagSigOnly, false, "Print only the generated signature, then exit") - cmd.Flags().Bool(flagOffline, false, "Offline mode. Do not query a full node") cmd.Flags().String(flagOutfile, "", "The document will be written to the given file instead of STDOUT") // Add the flags here and return the command @@ -85,7 +84,7 @@ func makeMultiSignCmd(cdc *codec.Codec) func(cmd *cobra.Command, args []string) cliCtx := context.NewCLIContextWithInput(inBuf).WithCodec(cdc) txBldr := types.NewTxBuilderFromCLI(inBuf) - if !viper.GetBool(flagOffline) { + if !cliCtx.Offline { accnum, seq, err := types.NewAccountRetriever(client.Codec, cliCtx).GetAccountNumberSequence(multisigInfo.GetAddress()) if err != nil { return err diff --git a/x/auth/client/cli/tx_sign.go b/x/auth/client/cli/tx_sign.go index 6d95c47ef4ce..62100b02335f 100644 --- a/x/auth/client/cli/tx_sign.go +++ b/x/auth/client/cli/tx_sign.go @@ -22,7 +22,6 @@ const ( flagMultisig = "multisig" flagAppend = "append" flagValidateSigs = "validate-signatures" - flagOffline = "offline" flagSigOnly = "signature-only" flagOutfile = "output-document" ) @@ -71,12 +70,7 @@ be generated via the 'multisign' command. "Print the addresses that must sign the transaction, those who have already signed it, and make sure that signatures are in the correct order", ) cmd.Flags().Bool(flagSigOnly, false, "Print only the generated signature, then exit") - cmd.Flags().Bool( - flagOffline, false, - "Offline mode; Do not query a full node. --account and --sequence options would be required if offline is set", - ) cmd.Flags().String(flagOutfile, "", "The document will be written to the given file instead of STDOUT") - cmd = flags.PostCommands(cmd)[0] cmd.MarkFlagRequired(flags.FlagFrom) @@ -86,7 +80,7 @@ be generated via the 'multisign' command. func preSignCmd(cmd *cobra.Command, _ []string) { // Conditionally mark the account and sequence numbers required as no RPC // query will be done. - if viper.GetBool(flagOffline) { + if viper.GetBool(flags.FlagOffline) { cmd.MarkFlagRequired(flags.FlagAccountNumber) cmd.MarkFlagRequired(flags.FlagSequence) } @@ -100,12 +94,11 @@ func makeSignCmd(cdc *codec.Codec) func(cmd *cobra.Command, args []string) error } inBuf := bufio.NewReader(cmd.InOrStdin()) - offline := viper.GetBool(flagOffline) cliCtx := context.NewCLIContextWithInput(inBuf).WithCodec(cdc) txBldr := types.NewTxBuilderFromCLI(inBuf) if viper.GetBool(flagValidateSigs) { - if !printAndValidateSigs(cliCtx, txBldr.ChainID(), stdTx, offline) { + if !printAndValidateSigs(cliCtx, txBldr.ChainID(), stdTx, cliCtx.Offline) { return fmt.Errorf("signatures validation failed") } @@ -124,14 +117,13 @@ func makeSignCmd(cdc *codec.Codec) func(cmd *cobra.Command, args []string) error if err != nil { return err } - newTx, err = client.SignStdTxWithSignerAddress( - txBldr, cliCtx, multisigAddr, cliCtx.GetFromName(), stdTx, offline, + txBldr, cliCtx, multisigAddr, cliCtx.GetFromName(), stdTx, cliCtx.Offline, ) generateSignatureOnly = true } else { appendSig := viper.GetBool(flagAppend) && !generateSignatureOnly - newTx, err = client.SignStdTx(txBldr, cliCtx, cliCtx.GetFromName(), stdTx, appendSig, offline) + newTx, err = client.SignStdTx(txBldr, cliCtx, cliCtx.GetFromName(), stdTx, appendSig, cliCtx.Offline) } if err != nil { diff --git a/x/auth/client/tx.go b/x/auth/client/tx.go index d9212bed5fbe..9036d7444d9f 100644 --- a/x/auth/client/tx.go +++ b/x/auth/client/tx.go @@ -327,8 +327,8 @@ func PrepareTxBuilder(txBldr authtypes.TxBuilder, cliCtx context.CLIContext) (au func buildUnsignedStdTxOffline(txBldr authtypes.TxBuilder, cliCtx context.CLIContext, msgs []sdk.Msg) (stdTx authtypes.StdTx, err error) { if txBldr.SimulateAndExecute() { - if cliCtx.GenerateOnly { - return stdTx, errors.New("cannot estimate gas with generate-only") + if cliCtx.Offline { + return stdTx, errors.New("cannot estimate gas in offline mode") } txBldr, err = EnrichWithGas(txBldr, cliCtx, msgs) diff --git a/x/distribution/client/cli/tx.go b/x/distribution/client/cli/tx.go index 35d12391d057..892058f6b38b 100644 --- a/x/distribution/client/cli/tx.go +++ b/x/distribution/client/cli/tx.go @@ -150,8 +150,8 @@ $ %s tx distribution withdraw-all-rewards --from mykey // The transaction cannot be generated offline since it requires a query // to get all the validators. - if cliCtx.GenerateOnly { - return fmt.Errorf("command disabled with the provided flag: %s", flags.FlagGenerateOnly) + if cliCtx.Offline { + return fmt.Errorf("cannot generate tx in offline mode") } msgs, err := common.WithdrawAllDelegatorRewards(cliCtx, queryRoute, delAddr)