diff --git a/CHANGELOG.md b/CHANGELOG.md index 7294176ad9c2..61159d524ab5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,7 +53,8 @@ Ref: https://keepachangelog.com/en/1.0.0/ * [\#10208](https://github.com/cosmos/cosmos-sdk/pull/10208) Add `TipsTxMiddleware` for transferring tips. * [\#10379](https://github.com/cosmos/cosmos-sdk/pull/10379) Add validation to `x/upgrade` CLI `software-upgrade` command `--plan-info` value. * [\#10561](https://github.com/cosmos/cosmos-sdk/pull/10561) Add configurable IAVL cache size to app.toml -* [\10507](https://github.com/cosmos/cosmos-sdk/pull/10507) Add middleware for tx priority. +* [\#10507](https://github.com/cosmos/cosmos-sdk/pull/10507) Add middleware for tx priority. +* [\#10311](https://github.com/cosmos/cosmos-sdk/pull/10311) Adds cli to use tips transactions. It adds an `--aux` flag to all CLI tx commands to generate the aux signer data (with optional tip), and a new `tx aux-to-fee` subcommand to let the fee payer gather aux signer data and broadcast the tx ### Improvements diff --git a/client/cmd.go b/client/cmd.go index 70f354bc2ddf..f1bf8d52084c 100644 --- a/client/cmd.go +++ b/client/cmd.go @@ -264,6 +264,24 @@ func readTxCommandFlags(clientCtx Context, flagSet *pflag.FlagSet) (Context, err } } + if !clientCtx.IsAux || flagSet.Changed(flags.FlagAux) { + isAux, _ := flagSet.GetBool(flags.FlagAux) + clientCtx = clientCtx.WithAux(isAux) + if isAux { + // If the user didn't explicity set an --output flag, use JSON by + // default. + if clientCtx.OutputFormat == "" || !flagSet.Changed(cli.OutputFlag) { + clientCtx = clientCtx.WithOutputFormat("json") + } + + // If the user didn't explicity set a --sign-mode flag, use + // DIRECT_AUX by default. + if clientCtx.SignModeStr == "" || !flagSet.Changed(flags.FlagSignMode) { + clientCtx = clientCtx.WithSignModeStr(flags.SignModeDirectAux) + } + } + } + return clientCtx, nil } diff --git a/client/context.go b/client/context.go index 56ef46ff0a88..0357c25d68f4 100644 --- a/client/context.go +++ b/client/context.go @@ -49,6 +49,9 @@ type Context struct { FeePayer sdk.AccAddress FeeGranter sdk.AccAddress Viper *viper.Viper + + // IsAux is true when the signer is an auxiliary signer (e.g. the tipper). + IsAux bool // TODO: Deprecated (remove). LegacyAmino *codec.LegacyAmino @@ -245,6 +248,12 @@ func (ctx Context) WithViper(prefix string) Context { return ctx } +// WithAux returns a copy of the context with an updated IsAux value. +func (ctx Context) WithAux(isAux bool) Context { + ctx.IsAux = isAux + return ctx +} + // PrintString prints the raw string to ctx.Output if it's defined, otherwise to os.Stdout func (ctx Context) PrintString(str string) error { return ctx.PrintBytes([]byte(str)) diff --git a/client/flags/flags.go b/client/flags/flags.go index 5ddffd453c57..a76559074861 100644 --- a/client/flags/flags.go +++ b/client/flags/flags.go @@ -35,6 +35,8 @@ const ( SignModeDirect = "direct" // SignModeLegacyAminoJSON is the value of the --sign-mode flag for SIGN_MODE_LEGACY_AMINO_JSON SignModeLegacyAminoJSON = "amino-json" + // SignModeDirectAux is the value of the --sign-mode flag for SIGN_MODE_DIRECT_AUX + SignModeDirectAux = "direct-aux" ) // List of CLI flags @@ -73,6 +75,9 @@ const ( FlagFeePayer = "fee-payer" FlagFeeGranter = "fee-granter" FlagReverse = "reverse" + FlagTip = "tip" + FlagAux = "aux" + FlagTipper = "tipper" // Tendermint logging flags FlagLogLevel = "log_level" @@ -111,10 +116,13 @@ func AddTxFlagsToCmd(cmd *cobra.Command) { cmd.Flags().Bool(FlagOffline, false, "Offline mode (does not allow any online functionality)") cmd.Flags().BoolP(FlagSkipConfirmation, "y", false, "Skip tx broadcasting prompt confirmation") cmd.Flags().String(FlagKeyringBackend, DefaultKeyringBackend, "Select keyring's backend (os|file|kwallet|pass|test|memory)") - cmd.Flags().String(FlagSignMode, "", "Choose sign mode (direct|amino-json), this is an advanced feature") + cmd.Flags().String(FlagSignMode, "", "Choose sign mode (direct|amino-json|direct-aux), this is an advanced feature") cmd.Flags().Uint64(FlagTimeoutHeight, 0, "Set a block timeout height to prevent the tx from being committed past a certain height") cmd.Flags().String(FlagFeePayer, "", "Fee payer pays fees for the transaction instead of deducting from the signer") cmd.Flags().String(FlagFeeGranter, "", "Fee granter grants fees for the transaction") + cmd.Flags().String(FlagTip, "", "Tip is the amount that is going to be transferred to the fee payer on the target chain. This flag is only valid when used with --aux") + cmd.Flags().String(FlagTipper, "", "Tipper will pay for tips for executing the tx on the target chain. This flag is only valid when used with --aux") + cmd.Flags().Bool(FlagAux, false, "Generate aux signer data instead of sending a tx") // --gas can accept integers and "auto" cmd.Flags().String(FlagGas, "", fmt.Sprintf("gas limit to set per-transaction; set to %q to calculate sufficient gas automatically (default %d)", GasFlagAuto, DefaultGasLimit)) diff --git a/client/tx/factory.go b/client/tx/factory.go index 3dc994b3917d..6d62d6264135 100644 --- a/client/tx/factory.go +++ b/client/tx/factory.go @@ -12,6 +12,7 @@ import ( "github.com/cosmos/cosmos-sdk/crypto/keyring" "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/tx" "github.com/cosmos/cosmos-sdk/types/tx/signing" ) @@ -29,6 +30,7 @@ type Factory struct { chainID string memo string fees sdk.Coins + tip *tx.Tip gasPrices sdk.DecCoins signMode signing.SignMode simulateAndExecute bool @@ -44,6 +46,8 @@ func NewFactoryCLI(clientCtx client.Context, flagSet *pflag.FlagSet) Factory { signMode = signing.SignMode_SIGN_MODE_DIRECT case flags.SignModeLegacyAminoJSON: signMode = signing.SignMode_SIGN_MODE_LEGACY_AMINO_JSON + case flags.SignModeDirectAux: + signMode = signing.SignMode_SIGN_MODE_DIRECT_AUX } accNum, _ := flagSet.GetUint64(flags.FlagAccountNumber) @@ -73,6 +77,10 @@ func NewFactoryCLI(clientCtx client.Context, flagSet *pflag.FlagSet) Factory { feesStr, _ := flagSet.GetString(flags.FlagFees) f = f.WithFees(feesStr) + tipsStr, _ := flagSet.GetString(flags.FlagTip) + tipper, _ := flagSet.GetString(flags.FlagTipper) + f = f.WithTips(tipsStr, tipper) + gasPricesStr, _ := flagSet.GetString(flags.FlagGasPrices) f = f.WithGasPrices(gasPricesStr) @@ -130,6 +138,20 @@ func (f Factory) WithFees(fees string) Factory { return f } +// WithTips returns a copy of the Factory with an updated tip. +func (f Factory) WithTips(tip string, tipper string) Factory { + parsedTips, err := sdk.ParseCoinsNormalized(tip) + if err != nil { + panic(err) + } + + f.tip = &tx.Tip{ + Tipper: tipper, + Amount: parsedTips, + } + return f +} + // WithGasPrices returns a copy of the Factory with updated gas prices. func (f Factory) WithGasPrices(gasPrices string) Factory { parsedGasPrices, err := sdk.ParseDecCoins(gasPrices) @@ -254,12 +276,12 @@ func (f Factory) PrintUnsignedTx(clientCtx client.Context, msgs ...sdk.Msg) erro _, _ = fmt.Fprintf(os.Stderr, "%s\n", GasEstimateResponse{GasEstimate: f.Gas()}) } - tx, err := f.BuildUnsignedTx(msgs...) + unsignedTx, err := f.BuildUnsignedTx(msgs...) if err != nil { return err } - json, err := clientCtx.TxConfig.TxJSONEncoder()(tx.GetTx()) + json, err := clientCtx.TxConfig.TxJSONEncoder()(unsignedTx.GetTx()) if err != nil { return err } diff --git a/client/tx/tx.go b/client/tx/tx.go index 1d3fcb3af013..b1b7c3c181ac 100644 --- a/client/tx/tx.go +++ b/client/tx/tx.go @@ -24,6 +24,7 @@ import ( // or sign it and broadcast it returning an error upon failure. func GenerateOrBroadcastTxCLI(clientCtx client.Context, flagSet *pflag.FlagSet, msgs ...sdk.Msg) error { txf := NewFactoryCLI(clientCtx, flagSet) + return GenerateOrBroadcastTxWithFactory(clientCtx, txf, msgs...) } @@ -40,6 +41,16 @@ func GenerateOrBroadcastTxWithFactory(clientCtx client.Context, txf Factory, msg } } + // If the --aux flag is set, we simply generate and print the AuxSignerData. + if clientCtx.IsAux { + auxSignerData, err := makeAuxSignerData(clientCtx, txf, msgs...) + if err != nil { + return err + } + + return clientCtx.PrintProto(&auxSignerData) + } + if clientCtx.GenerateOnly { return txf.PrintUnsignedTx(clientCtx, msgs...) } @@ -335,3 +346,75 @@ type GasEstimateResponse struct { func (gr GasEstimateResponse) String() string { return fmt.Sprintf("gas estimate: %d", gr.GasEstimate) } + +// makeAuxSignerData generates an AuxSignerData from the client inputs. +func makeAuxSignerData(clientCtx client.Context, f Factory, msgs ...sdk.Msg) (tx.AuxSignerData, error) { + b := NewAuxTxBuilder() + fromAddress, name, _, err := client.GetFromFields(clientCtx.Keyring, clientCtx.From, false) + if err != nil { + return tx.AuxSignerData{}, err + } + + b.SetAddress(fromAddress.String()) + if clientCtx.Offline { + b.SetAccountNumber(f.accountNumber) + b.SetSequence(f.sequence) + } else { + accNum, seq, err := clientCtx.AccountRetriever.GetAccountNumberSequence(clientCtx, fromAddress) + if err != nil { + return tx.AuxSignerData{}, err + } + b.SetAccountNumber(accNum) + b.SetSequence(seq) + } + + err = b.SetMsgs(msgs...) + if err != nil { + return tx.AuxSignerData{}, err + } + + if f.tip != nil { + if f.tip.Tipper == "" { + return tx.AuxSignerData{}, sdkerrors.Wrap(errors.New("tipper flag required"), "tipper") + } else { + if _, err := sdk.AccAddressFromBech32(f.tip.Tipper); err != nil { + return tx.AuxSignerData{}, sdkerrors.ErrInvalidAddress.Wrap("tipper must be a bech32 address") + } + b.SetTip(f.tip) + } + } + + err = b.SetSignMode(f.SignMode()) + if err != nil { + return tx.AuxSignerData{}, err + } + + key, err := clientCtx.Keyring.Key(name) + if err != nil { + return tx.AuxSignerData{}, err + } + + pub, err := key.GetPubKey() + if err != nil { + return tx.AuxSignerData{}, err + } + + err = b.SetPubKey(pub) + if err != nil { + return tx.AuxSignerData{}, err + } + + b.SetChainID(clientCtx.ChainID) + signBz, err := b.GetSignBytes() + if err != nil { + return tx.AuxSignerData{}, err + } + + sig, _, err := clientCtx.Keyring.Sign(name, signBz) + if err != nil { + return tx.AuxSignerData{}, err + } + b.SetSignature(sig) + + return b.GetAuxSignerData() +} diff --git a/client/tx/tx_test.go b/client/tx/tx_test.go index a05c9310bd48..181c857f9000 100644 --- a/client/tx/tx_test.go +++ b/client/tx/tx_test.go @@ -224,6 +224,7 @@ func TestSign(t *testing.T) { {"direct: should overwrite multi-signers tx with DIRECT sig", txfDirect, txb2, from1, true, []cryptotypes.PubKey{pubKey1}, nil}, } + var prevSigs []signingtypes.SignatureV2 for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { diff --git a/client/tx_config.go b/client/tx_config.go index e49dc1a6e603..5c5fd2695e40 100644 --- a/client/tx_config.go +++ b/client/tx_config.go @@ -41,10 +41,10 @@ type ( SetSignatures(signatures ...signingtypes.SignatureV2) error SetMemo(memo string) SetFeeAmount(amount sdk.Coins) + SetFeePayer(feePayer sdk.AccAddress) SetGasLimit(limit uint64) SetTip(tip *tx.Tip) SetTimeoutHeight(height uint64) - SetFeePayer(feePayer sdk.AccAddress) SetFeeGranter(feeGranter sdk.AccAddress) AddAuxSignerData(tx.AuxSignerData) error } diff --git a/simapp/simd/cmd/root.go b/simapp/simd/cmd/root.go index d93615b8cd90..eee610d941c7 100644 --- a/simapp/simd/cmd/root.go +++ b/simapp/simd/cmd/root.go @@ -215,6 +215,7 @@ func txCommand() *cobra.Command { authcmd.GetBroadcastCommand(), authcmd.GetEncodeCommand(), authcmd.GetDecodeCommand(), + authcmd.GetAuxToFeeCommand(), ) simapp.ModuleBasics.AddTxCommands(cmd) diff --git a/x/auth/client/cli/tips.go b/x/auth/client/cli/tips.go new file mode 100644 index 000000000000..5e356b6dc03d --- /dev/null +++ b/x/auth/client/cli/tips.go @@ -0,0 +1,85 @@ +package cli + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/flags" + clienttx "github.com/cosmos/cosmos-sdk/client/tx" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/types/tx" + authclient "github.com/cosmos/cosmos-sdk/x/auth/client" +) + +func GetAuxToFeeCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "aux-to-fee ", + Short: "includes the aux signer data in the tx, broadcast the tx, and sends the tip amount to the broadcaster", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + auxSignerData := tx.AuxSignerData{} + err = readAuxSignerData(clientCtx.Codec, &auxSignerData, args[0]) + if err != nil { + return err + } + + f := clienttx.NewFactoryCLI(clientCtx, cmd.Flags()) + + txBuilder := clientCtx.TxConfig.NewTxBuilder() + err = txBuilder.AddAuxSignerData(auxSignerData) + if err != nil { + return err + } + + txBuilder.SetFeePayer(clientCtx.FromAddress) + txBuilder.SetFeeAmount(f.Fees()) + txBuilder.SetGasLimit(f.Gas()) + + if clientCtx.GenerateOnly { + json, err := clientCtx.TxConfig.TxJSONEncoder()(txBuilder.GetTx()) + if err != nil { + return err + } + return clientCtx.PrintString(fmt.Sprintf("%s\n", json)) + } + + err = authclient.SignTx(f, clientCtx, clientCtx.FromName, txBuilder, clientCtx.Offline, false) + if err != nil { + return err + } + + txBytes, err := clientCtx.TxConfig.TxEncoder()(txBuilder.GetTx()) + if err != nil { + return err + } + + // broadcast to a Tendermint node + res, err := clientCtx.BroadcastTx(txBytes) + if err != nil { + return err + } + + return clientCtx.PrintProto(res) + }, + } + + flags.AddTxFlagsToCmd(cmd) + return cmd +} + +func readAuxSignerData(cdc codec.Codec, auxSignerData *tx.AuxSignerData, filename string) error { + bytes, err := os.ReadFile(filename) + if err != nil { + return err + } + + return cdc.UnmarshalJSON(bytes, auxSignerData) +} diff --git a/x/auth/client/testutil/helpers.go b/x/auth/client/testutil/helpers.go index 6d68ef9236f2..afbf18d20ba4 100644 --- a/x/auth/client/testutil/helpers.go +++ b/x/auth/client/testutil/helpers.go @@ -106,4 +106,13 @@ func TxMultiSignBatchExec(clientCtx client.Context, filename string, from string return clitestutil.ExecTestCLICmd(clientCtx, cli.GetMultiSignBatchCmd(), args) } +// TxAuxToFeeExec executes `GetAuxToFeeCommand` cli command with given args. +func TxAuxToFeeExec(clientCtx client.Context, filename string, extraArgs ...string) (testutil.BufferWriter, error) { + args := []string{ + filename, + } + + return clitestutil.ExecTestCLICmd(clientCtx, cli.GetAuxToFeeCommand(), append(args, extraArgs...)) +} + // DONTCOVER diff --git a/x/auth/client/testutil/suite.go b/x/auth/client/testutil/suite.go index 9fbb3b31d7f6..29d604651754 100644 --- a/x/auth/client/testutil/suite.go +++ b/x/auth/client/testutil/suite.go @@ -34,6 +34,8 @@ import ( bankcli "github.com/cosmos/cosmos-sdk/x/bank/client/testutil" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" "github.com/cosmos/cosmos-sdk/x/genutil/client/cli" + govtestutil "github.com/cosmos/cosmos-sdk/x/gov/client/testutil" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" ) type IntegrationTestSuite struct { @@ -898,6 +900,7 @@ func (s *IntegrationTestSuite) TestCLIMultisignSortSignatures() { s.Require().NoError(err) intialCoins := balRes.Balances + s.Require().NoError(s.network.WaitForNextBlock()) // Send coins from validator to multisig. sendTokens := sdk.NewInt64Coin(s.cfg.BondDenom, 10) _, err = s.createBankMsg( @@ -986,10 +989,12 @@ func (s *IntegrationTestSuite) TestCLIMultisign() { // Send coins from validator to multisig. sendTokens := sdk.NewInt64Coin(s.cfg.BondDenom, 10) + s.Require().NoError(s.network.WaitForNextBlock()) _, err = s.createBankMsg( val1, addr, sdk.NewCoins(sendTokens), ) + s.Require().NoError(s.network.WaitForNextBlock()) s.Require().NoError(err) s.Require().NoError(s.network.WaitForNextBlock()) @@ -999,7 +1004,7 @@ func (s *IntegrationTestSuite) TestCLIMultisign() { var balRes banktypes.QueryAllBalancesResponse err = val1.ClientCtx.Codec.UnmarshalJSON(resp.Bytes(), &balRes) s.Require().NoError(err) - s.Require().Equal(sendTokens.Amount, balRes.Balances.AmountOf(s.cfg.BondDenom)) + s.Require().True(sendTokens.Amount.Equal(balRes.Balances.AmountOf(s.cfg.BondDenom))) // Generate multisig transaction. multiGeneratedTx, err := bankcli.MsgSendExec( @@ -1117,7 +1122,6 @@ func (s *IntegrationTestSuite) TestSignBatchMultisig() { file2 := testutil.WriteToNewTempFile(s.T(), res.String()) _, err = TxMultiSignExec(val.ClientCtx, multisigRecord.Name, filename.Name(), file1.Name(), file2.Name()) s.Require().NoError(err) - } func (s *IntegrationTestSuite) TestMultisignBatch() { @@ -1460,6 +1464,333 @@ func (s *IntegrationTestSuite) TestSignWithMultiSignersAminoJSON() { require.Equal(sdk.NewCoins(val0Coin, val1Coin), queryRes.Balances) } +func (s *IntegrationTestSuite) TestAuxSigner() { + require := s.Require() + val := s.network.Validators[0] + val0Coin := sdk.NewCoin(fmt.Sprintf("%stoken", val.Moniker), sdk.NewInt(10)) + + testCases := []struct { + name string + args []string + expectErr bool + }{ + { + "error with SIGN_MODE_DIRECT_AUX and --aux unset", + []string{ + fmt.Sprintf("--%s=%s", flags.FlagSignMode, flags.SignModeDirectAux), + }, + true, + }, + { + "no error with SIGN_MDOE_DIRECT_AUX mode and generate-only set (ignores generate-only)", + []string{ + fmt.Sprintf("--%s=%s", flags.FlagSignMode, flags.SignModeDirectAux), + fmt.Sprintf("--%s=true", flags.FlagGenerateOnly), + }, + false, + }, + { + "no error with SIGN_MDOE_DIRECT_AUX mode and generate-only, tip flag set", + []string{ + fmt.Sprintf("--%s=%s", flags.FlagSignMode, flags.SignModeDirectAux), + fmt.Sprintf("--%s=true", flags.FlagGenerateOnly), + fmt.Sprintf("--%s=%s", flags.FlagTip, val0Coin.String()), + }, + false, + }, + } + + for _, tc := range testCases { + tc := tc + s.Run(tc.name, func() { + _, err := govtestutil.MsgSubmitProposal( + val.ClientCtx, + val.Address.String(), + "test", + "test desc", + govtypes.ProposalTypeText, + tc.args..., + ) + if tc.expectErr { + require.Error(err) + } else { + require.NoError(err) + } + }) + } +} + +func (s *IntegrationTestSuite) TestAuxToFee() { + require := s.Require() + val := s.network.Validators[0] + + kb := s.network.Validators[0].ClientCtx.Keyring + acc, _, err := kb.NewMnemonic("tipperAccount", keyring.English, sdk.FullFundraiserPath, keyring.DefaultBIP39Passphrase, hd.Secp256k1) + require.NoError(err) + + tipper, err := acc.GetAddress() + require.NoError(err) + tipperInitialBal := sdk.NewCoin(fmt.Sprintf("%stoken", val.Moniker), sdk.NewInt(10000)) + + feePayer := val.Address + fee := sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(1000)) + tip := sdk.NewCoin(fmt.Sprintf("%stoken", val.Moniker), sdk.NewInt(1000)) + + s.Require().NoError(s.network.WaitForNextBlock()) + _, err = s.createBankMsg(val, tipper, sdk.NewCoins(tipperInitialBal)) + require.NoError(err) + s.Require().NoError(s.network.WaitForNextBlock()) + + bal := s.getBalances(val.ClientCtx, tipper, tip.Denom) + s.Require().True(bal.Equal(tipperInitialBal.Amount)) + + testCases := []struct { + name string + tipper sdk.AccAddress + feePayer sdk.AccAddress + tip sdk.Coin + expectErrAux bool + expectErrBroadCast bool + errMsg string + tipperArgs []string + feePayerArgs []string + }{ + { + name: "when --aux and --sign-mode = direct set: error", + tipper: tipper, + feePayer: feePayer, + tip: tip, + tipperArgs: []string{ + fmt.Sprintf("--%s=%s", flags.FlagSignMode, flags.SignModeDirect), + fmt.Sprintf("--%s=%s", flags.FlagTip, tip), + fmt.Sprintf("--%s=%s", flags.FlagTipper, tipper.String()), + fmt.Sprintf("--%s=true", flags.FlagAux), + }, + expectErrAux: true, + feePayerArgs: []string{ + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastSync), + fmt.Sprintf("--%s=%s", flags.FlagFrom, feePayer), + fmt.Sprintf("--%s=%s", flags.FlagFees, fee.String()), + }, + }, + { + name: "when --aux and --sign-mode = direct set: error", + tipper: tipper, + feePayer: feePayer, + tip: tip, + tipperArgs: []string{ + fmt.Sprintf("--%s=%s", flags.FlagSignMode, flags.SignModeDirect), + fmt.Sprintf("--%s=%s", flags.FlagTip, tip), + fmt.Sprintf("--%s=true", flags.FlagAux), + }, + expectErrAux: true, + feePayerArgs: []string{ + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastSync), + fmt.Sprintf("--%s=%s", flags.FlagFrom, feePayer), + fmt.Sprintf("--%s=%s", flags.FlagFees, fee.String()), + }, + }, + { + name: "both tipper, fee payer uses AMINO: no error", + tipper: tipper, + feePayer: feePayer, + tip: tip, + tipperArgs: []string{ + fmt.Sprintf("--%s=%s", flags.FlagSignMode, flags.SignModeLegacyAminoJSON), + fmt.Sprintf("--%s=%s", flags.FlagTip, tip), + fmt.Sprintf("--%s=%s", flags.FlagTipper, tipper.String()), + fmt.Sprintf("--%s=true", flags.FlagAux), + }, + feePayerArgs: []string{ + fmt.Sprintf("--%s=%s", flags.FlagSignMode, flags.SignModeLegacyAminoJSON), + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock), + fmt.Sprintf("--%s=%s", flags.FlagFrom, feePayer), + fmt.Sprintf("--%s=%s", flags.FlagFees, fee.String()), + }, + }, + { + name: "tipper uses DIRECT_AUX, fee payer uses AMINO: no error", + tipper: tipper, + feePayer: feePayer, + tip: tip, + tipperArgs: []string{ + fmt.Sprintf("--%s=%s", flags.FlagSignMode, flags.SignModeDirectAux), + fmt.Sprintf("--%s=%s", flags.FlagTip, tip), + fmt.Sprintf("--%s=%s", flags.FlagTipper, tipper.String()), + fmt.Sprintf("--%s=true", flags.FlagAux), + }, + feePayerArgs: []string{ + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagSignMode, flags.SignModeLegacyAminoJSON), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock), + fmt.Sprintf("--%s=%s", flags.FlagFrom, feePayer), + fmt.Sprintf("--%s=%s", flags.FlagFees, fee.String()), + }, + }, + { + name: "--tip flag unset: no error", + tipper: tipper, + feePayer: feePayer, + tip: sdk.Coin{Denom: fmt.Sprintf("%stoken", val.Moniker), Amount: sdk.NewInt(0)}, + tipperArgs: []string{ + fmt.Sprintf("--%s=%s", flags.FlagSignMode, flags.SignModeDirectAux), + fmt.Sprintf("--%s=%s", flags.FlagTipper, tipper.String()), + fmt.Sprintf("--%s=true", flags.FlagAux), + }, + feePayerArgs: []string{ + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagSignMode, flags.SignModeLegacyAminoJSON), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock), + fmt.Sprintf("--%s=%s", flags.FlagFrom, feePayer), + fmt.Sprintf("--%s=%s", flags.FlagFees, fee.String()), + }, + }, + { + name: "legacy amino json: no error", + tipper: tipper, + feePayer: feePayer, + tip: tip, + tipperArgs: []string{ + fmt.Sprintf("--%s=%s", flags.FlagSignMode, flags.SignModeLegacyAminoJSON), + fmt.Sprintf("--%s=%s", flags.FlagTip, tip), + fmt.Sprintf("--%s=%s", flags.FlagTipper, tipper.String()), + fmt.Sprintf("--%s=true", flags.FlagAux), + }, + feePayerArgs: []string{ + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock), + fmt.Sprintf("--%s=%s", flags.FlagFrom, feePayer), + fmt.Sprintf("--%s=%s", flags.FlagFees, fee.String()), + }, + }, + { + name: "tipper uses direct aux, fee payer uses direct: happy case", + tipper: tipper, + feePayer: feePayer, + tip: tip, + tipperArgs: []string{ + fmt.Sprintf("--%s=%s", flags.FlagSignMode, flags.SignModeDirectAux), + fmt.Sprintf("--%s=%s", flags.FlagTip, tip), + fmt.Sprintf("--%s=%s", flags.FlagTipper, tipper.String()), + fmt.Sprintf("--%s=true", flags.FlagAux), + }, + feePayerArgs: []string{ + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock), + fmt.Sprintf("--%s=%s", flags.FlagFrom, feePayer), + fmt.Sprintf("--%s=%s", flags.FlagFees, fee.String()), + }, + }, + { + name: "wrong tipper address: error", + tipper: tipper, + feePayer: feePayer, + tip: tip, + tipperArgs: []string{ + fmt.Sprintf("--%s=%s", flags.FlagSignMode, flags.SignModeDirectAux), + fmt.Sprintf("--%s=%s", flags.FlagTip, tip), + fmt.Sprintf("--%s=%s", flags.FlagTipper, "foobar"), + fmt.Sprintf("--%s=true", flags.FlagAux), + }, + expectErrAux: true, + feePayerArgs: []string{ + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock), + fmt.Sprintf("--%s=%s", flags.FlagFrom, feePayer), + fmt.Sprintf("--%s=%s", flags.FlagFees, fee.String()), + }, + }, + { + name: "wrong denom in tip: error", + tipper: tipper, + feePayer: feePayer, + tip: sdk.Coin{Denom: fmt.Sprintf("%stoken", val.Moniker), Amount: sdk.NewInt(0)}, + tipperArgs: []string{ + fmt.Sprintf("--%s=%s", flags.FlagTip, "1000wrongDenom"), + fmt.Sprintf("--%s=%s", flags.FlagSignMode, flags.SignModeDirectAux), + fmt.Sprintf("--%s=%s", flags.FlagTipper, tipper.String()), + fmt.Sprintf("--%s=true", flags.FlagAux), + }, + feePayerArgs: []string{ + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagSignMode, flags.SignModeDirect), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastSync), + fmt.Sprintf("--%s=%s", flags.FlagFrom, feePayer), + fmt.Sprintf("--%s=%s", flags.FlagFees, fee.String()), + }, + errMsg: "insufficient funds", + }, + { + name: "insufficient fees: error", + tipper: tipper, + feePayer: feePayer, + tip: sdk.Coin{Denom: fmt.Sprintf("%stoken", val.Moniker), Amount: sdk.NewInt(0)}, + tipperArgs: []string{ + fmt.Sprintf("--%s=%s", flags.FlagTip, tip), + fmt.Sprintf("--%s=%s", flags.FlagSignMode, flags.SignModeDirectAux), + fmt.Sprintf("--%s=%s", flags.FlagTipper, tipper.String()), + fmt.Sprintf("--%s=true", flags.FlagAux), + }, + feePayerArgs: []string{ + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagSignMode, flags.SignModeDirect), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastSync), + fmt.Sprintf("--%s=%s", flags.FlagFrom, feePayer), + }, + errMsg: "insufficient fees", + }, + } + + for _, tc := range testCases { + tc := tc + s.Run(tc.name, func() { + res, err := govtestutil.MsgSubmitProposal( + val.ClientCtx, + tipper.String(), + "test", + "test desc", + govtypes.ProposalTypeText, + tc.tipperArgs..., + ) + + if tc.expectErrAux { + require.Error(err) + } else { + require.NoError(err) + genTxFile := testutil.WriteToNewTempFile(s.T(), string(res.Bytes())) + + // broadcast the tx + res, err = TxAuxToFeeExec( + val.ClientCtx, + genTxFile.Name(), + tc.feePayerArgs..., + ) + + var txRes sdk.TxResponse + s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(res.Bytes(), &txRes)) + + if tc.expectErrBroadCast { + require.Error(err) + } else if tc.errMsg != "" { + require.Contains(txRes.RawLog, tc.errMsg) + } else { + require.NoError(err) + + s.Require().Equal(uint32(0), txRes.Code) + s.Require().NotNil(int64(0), txRes.Height) + + bal = s.getBalances(val.ClientCtx, tipper, tc.tip.Denom) + tipperInitialBal = tipperInitialBal.Sub(tc.tip) + s.Require().True(bal.Equal(tipperInitialBal.Amount)) + } + } + }) + } +} + func (s *IntegrationTestSuite) createBankMsg(val *network.Validator, toAddr sdk.AccAddress, amount sdk.Coins, extraFlags ...string) (testutil.BufferWriter, error) { flags := []string{fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastSync), @@ -1470,3 +1801,14 @@ func (s *IntegrationTestSuite) createBankMsg(val *network.Validator, toAddr sdk. flags = append(flags, extraFlags...) return bankcli.MsgSendExec(val.ClientCtx, val.Address, toAddr, amount, flags...) } + +func (s *IntegrationTestSuite) getBalances(clientCtx client.Context, addr sdk.AccAddress, denom string) sdk.Int { + resp, err := bankcli.QueryBalancesExec(clientCtx, addr) + s.Require().NoError(err) + + var balRes banktypes.QueryAllBalancesResponse + err = clientCtx.Codec.UnmarshalJSON(resp.Bytes(), &balRes) + s.Require().NoError(err) + startTokens := balRes.Balances.AmountOf(denom) + return startTokens +} diff --git a/x/auth/migrations/legacytx/stdtx.go b/x/auth/migrations/legacytx/stdtx.go index 33f7328da5e4..bfe8502a3ddb 100644 --- a/x/auth/migrations/legacytx/stdtx.go +++ b/x/auth/migrations/legacytx/stdtx.go @@ -7,7 +7,6 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" "github.com/cosmos/cosmos-sdk/types/tx" - txtypes "github.com/cosmos/cosmos-sdk/types/tx" "github.com/cosmos/cosmos-sdk/types/tx/signing" ) @@ -107,28 +106,28 @@ func (tx StdTx) GetMsgs() []sdk.Msg { return tx.Msgs } // ValidateBasic does a simple and lightweight validation check that doesn't // require access to any other information. -func (tx StdTx) ValidateBasic() error { - stdSigs := tx.GetSignatures() +func (stdTx StdTx) ValidateBasic() error { + stdSigs := stdTx.GetSignatures() - if tx.Fee.Gas > txtypes.MaxGasWanted { + if stdTx.Fee.Gas > tx.MaxGasWanted { return sdkerrors.Wrapf( sdkerrors.ErrInvalidRequest, - "invalid gas supplied; %d > %d", tx.Fee.Gas, txtypes.MaxGasWanted, + "invalid gas supplied; %d > %d", stdTx.Fee.Gas, tx.MaxGasWanted, ) } - if tx.Fee.Amount.IsAnyNegative() { + if stdTx.Fee.Amount.IsAnyNegative() { return sdkerrors.Wrapf( sdkerrors.ErrInsufficientFee, - "invalid fee provided: %s", tx.Fee.Amount, + "invalid fee provided: %s", stdTx.Fee.Amount, ) } if len(stdSigs) == 0 { return sdkerrors.ErrNoSignatures } - if len(stdSigs) != len(tx.GetSigners()) { + if len(stdSigs) != len(stdTx.GetSigners()) { return sdkerrors.Wrapf( sdkerrors.ErrUnauthorized, - "wrong number of signers; expected %d, got %d", len(tx.GetSigners()), len(stdSigs), + "wrong number of signers; expected %d, got %d", len(stdTx.GetSigners()), len(stdSigs), ) } diff --git a/x/auth/migrations/legacytx/stdtx_test.go b/x/auth/migrations/legacytx/stdtx_test.go index 650f6f4b155c..c883a2faa2ea 100644 --- a/x/auth/migrations/legacytx/stdtx_test.go +++ b/x/auth/migrations/legacytx/stdtx_test.go @@ -12,7 +12,6 @@ import ( cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" kmultisig "github.com/cosmos/cosmos-sdk/crypto/keys/multisig" - "github.com/cosmos/cosmos-sdk/crypto/types" cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" "github.com/cosmos/cosmos-sdk/testutil/testdata" sdk "github.com/cosmos/cosmos-sdk/types" @@ -255,7 +254,7 @@ func TestSignatureV2Conversions(t *testing.T) { pubKey, pubKey2, }) dummy2 := []byte("dummySig2") - bitArray := types.NewCompactBitArray(2) + bitArray := cryptotypes.NewCompactBitArray(2) bitArray.SetIndex(0, true) bitArray.SetIndex(1, true) msigData := &signing.MultiSignatureData{ diff --git a/x/auth/signing/sig_verifiable_tx.go b/x/auth/signing/sig_verifiable_tx.go index 698d2ac918cb..1686ab93bd32 100644 --- a/x/auth/signing/sig_verifiable_tx.go +++ b/x/auth/signing/sig_verifiable_tx.go @@ -17,7 +17,7 @@ type SigVerifiableTx interface { } // Tx defines a transaction interface that supports all standard message, signature -// fee, memo, and auxiliary interfaces. +// fee, memo, tips, and auxiliary interfaces. type Tx interface { SigVerifiableTx diff --git a/x/auth/tx/builder.go b/x/auth/tx/builder.go index 9d67f767c0fd..edcfd630ca82 100644 --- a/x/auth/tx/builder.go +++ b/x/auth/tx/builder.go @@ -36,6 +36,7 @@ type wrapper struct { var ( _ authsigning.Tx = &wrapper{} _ client.TxBuilder = &wrapper{} + _ tx.TipTx = &wrapper{} _ middleware.HasExtensionOptionsTx = &wrapper{} _ ExtensionOptionsTxBuilder = &wrapper{} _ tx.TipTx = &wrapper{} diff --git a/x/auth/tx/mode_handler.go b/x/auth/tx/mode_handler.go index bb59fdc29a0e..19e34df49a24 100644 --- a/x/auth/tx/mode_handler.go +++ b/x/auth/tx/mode_handler.go @@ -10,8 +10,8 @@ import ( // DefaultSignModes are the default sign modes enabled for protobuf transactions. var DefaultSignModes = []signingtypes.SignMode{ signingtypes.SignMode_SIGN_MODE_DIRECT, - signingtypes.SignMode_SIGN_MODE_LEGACY_AMINO_JSON, signingtypes.SignMode_SIGN_MODE_DIRECT_AUX, + signingtypes.SignMode_SIGN_MODE_LEGACY_AMINO_JSON, } // makeSignModeHandler returns the default protobuf SignModeHandler supporting