diff --git a/CHANGELOG.md b/CHANGELOG.md index fa3be10acba..5110be01667 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,9 +46,9 @@ Ref: https://keepachangelog.com/en/1.0.0/ * (x/auth) [\#10880](https://github.com/cosmos/cosmos-sdk/pull/10880) Added a new query to the tx query service that returns a block with transactions fully decoded. * [#11314](https://github.com/cosmos/cosmos-sdk/pull/11314) Add state rollback command. - ### Bug Fixes +* (client) [\#11283](https://github.com/cosmos/cosmos-sdk/issues/11283) Support multiple keys for tx simulation and setting automatic gas for txs. * (store) [\#11177](https://github.com/cosmos/cosmos-sdk/pull/11177) Update the prune `everything` strategy to store the last two heights. * (store) [\#11117](https://github.com/cosmos/cosmos-sdk/pull/11117) Fix data race in store trace component * (x/authz) [\#11252](https://github.com/cosmos/cosmos-sdk/pull/11252) Allow insufficient funds error for authz simulation diff --git a/client/tx/factory.go b/client/tx/factory.go index 83cbf6df3a3..7547e2ba58f 100644 --- a/client/tx/factory.go +++ b/client/tx/factory.go @@ -1,6 +1,10 @@ package tx import ( + "errors" + "fmt" + "os" + "github.com/spf13/pflag" "cosmossdk.io/math" @@ -9,6 +13,8 @@ import ( "github.com/cosmos/cosmos-sdk/client/flags" codectypes "github.com/cosmos/cosmos-sdk/codec/types" "github.com/cosmos/cosmos-sdk/crypto/keyring" + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/tx/signing" ) @@ -241,3 +247,147 @@ func (f Factory) WithTimeoutHeight(height uint64) Factory { f.timeoutHeight = height return f } + +// BuildUnsignedTx builds a transaction to be signed given a set of messages. +// Once created, the fee, memo, and messages are set. +func (f Factory) BuildUnsignedTx(msgs ...sdk.Msg) (client.TxBuilder, error) { + if f.chainID == "" { + return nil, fmt.Errorf("chain ID required but not specified") + } + + fees := f.fees + + if !f.gasPrices.IsZero() { + if !fees.IsZero() { + return nil, errors.New("cannot provide both fees and gas prices") + } + + glDec := sdk.NewDec(int64(f.gas)) + + // Derive the fees based on the provided gas prices, where + // fee = ceil(gasPrice * gasLimit). + fees = make(sdk.Coins, len(f.gasPrices)) + + for i, gp := range f.gasPrices { + fee := gp.Amount.Mul(glDec) + fees[i] = sdk.NewCoin(gp.Denom, fee.Ceil().RoundInt()) + } + } + + tx := f.txConfig.NewTxBuilder() + + if err := tx.SetMsgs(msgs...); err != nil { + return nil, err + } + + tx.SetMemo(f.memo) + tx.SetFeeAmount(fees) + tx.SetGasLimit(f.gas) + tx.SetTimeoutHeight(f.TimeoutHeight()) + + return tx, nil +} + +// PrintUnsignedTx will generate an unsigned transaction and print it to the writer +// specified by ctx.Output. If simulation was requested, the gas will be +// simulated and also printed to the same writer before the transaction is +// printed. +func (f Factory) PrintUnsignedTx(clientCtx client.Context, msgs ...sdk.Msg) error { + if f.SimulateAndExecute() { + if clientCtx.Offline { + return errors.New("cannot estimate gas in offline mode") + } + + _, adjusted, err := CalculateGas(clientCtx, f, msgs...) + if err != nil { + return err + } + + f = f.WithGas(adjusted) + _, _ = fmt.Fprintf(os.Stderr, "%s\n", GasEstimateResponse{GasEstimate: f.Gas()}) + } + + unsignedTx, err := f.BuildUnsignedTx(msgs...) + if err != nil { + return err + } + + json, err := clientCtx.TxConfig.TxJSONEncoder()(unsignedTx.GetTx()) + if err != nil { + return err + } + + return clientCtx.PrintString(fmt.Sprintf("%s\n", json)) +} + +// BuildSimTx creates an unsigned tx with an empty single signature and returns +// the encoded transaction or an error if the unsigned transaction cannot be +// built. +func (f Factory) BuildSimTx(msgs ...sdk.Msg) ([]byte, error) { + txb, err := f.BuildUnsignedTx(msgs...) + if err != nil { + return nil, err + } + + // use the first element from the list of keys in order to generate a valid + // pubkey that supports multiple algorithms + + var pk cryptotypes.PubKey = &secp256k1.PubKey{} // use default public key type + + if f.keybase != nil { + infos, _ := f.keybase.List() + if len(infos) == 0 { + return nil, errors.New("cannot build signature for simulation, key infos slice is empty") + } + + // take the first info record just for simulation purposes + pk = infos[0].GetPubKey() + } + + // Create an empty signature literal as the ante handler will populate with a + // sentinel pubkey. + sig := signing.SignatureV2{ + PubKey: pk, + Data: &signing.SingleSignatureData{ + SignMode: f.signMode, + }, + Sequence: f.Sequence(), + } + if err := txb.SetSignatures(sig); err != nil { + return nil, err + } + + return f.txConfig.TxEncoder()(txb.GetTx()) +} + +// Prepare ensures the account defined by ctx.GetFromAddress() exists and +// if the account number and/or the account sequence number are zero (not set), +// they will be queried for and set on the provided Factory. A new Factory with +// the updated fields will be returned. +func (f Factory) Prepare(clientCtx client.Context) (Factory, error) { + fc := f + + from := clientCtx.GetFromAddress() + + if err := fc.accountRetriever.EnsureExists(clientCtx, from); err != nil { + return fc, err + } + + initNum, initSeq := fc.accountNumber, fc.sequence + if initNum == 0 || initSeq == 0 { + num, seq, err := fc.accountRetriever.GetAccountNumberSequence(clientCtx, from) + if err != nil { + return fc, err + } + + if initNum == 0 { + fc = fc.WithAccountNumber(num) + } + + if initSeq == 0 { + fc = fc.WithSequence(seq) + } + } + + return fc, nil +} diff --git a/client/tx/tx.go b/client/tx/tx.go index 524dbb0ccc5..8c56c74607d 100644 --- a/client/tx/tx.go +++ b/client/tx/tx.go @@ -13,7 +13,6 @@ import ( "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/input" - "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" @@ -235,66 +234,14 @@ func WriteGeneratedTxResponse( // transaction is initially created via the provided factory's generator. Once // created, the fee, memo, and messages are set. func BuildUnsignedTx(txf Factory, msgs ...sdk.Msg) (client.TxBuilder, error) { - if txf.chainID == "" { - return nil, fmt.Errorf("chain ID required but not specified") - } - - fees := txf.fees - - if !txf.gasPrices.IsZero() { - if !fees.IsZero() { - return nil, errors.New("cannot provide both fees and gas prices") - } - - glDec := sdk.NewDec(int64(txf.gas)) - - // Derive the fees based on the provided gas prices, where - // fee = ceil(gasPrice * gasLimit). - fees = make(sdk.Coins, len(txf.gasPrices)) - - for i, gp := range txf.gasPrices { - fee := gp.Amount.Mul(glDec) - fees[i] = sdk.NewCoin(gp.Denom, fee.Ceil().RoundInt()) - } - } - - tx := txf.txConfig.NewTxBuilder() - - if err := tx.SetMsgs(msgs...); err != nil { - return nil, err - } - - tx.SetMemo(txf.memo) - tx.SetFeeAmount(fees) - tx.SetGasLimit(txf.gas) - tx.SetTimeoutHeight(txf.TimeoutHeight()) - - return tx, nil + return txf.BuildUnsignedTx(msgs...) } // BuildSimTx creates an unsigned tx with an empty single signature and returns // the encoded transaction or an error if the unsigned transaction cannot be // built. func BuildSimTx(txf Factory, msgs ...sdk.Msg) ([]byte, error) { - txb, err := BuildUnsignedTx(txf, msgs...) - if err != nil { - return nil, err - } - - // Create an empty signature literal as the ante handler will populate with a - // sentinel pubkey. - sig := signing.SignatureV2{ - PubKey: &secp256k1.PubKey{}, - Data: &signing.SingleSignatureData{ - SignMode: txf.signMode, - }, - Sequence: txf.Sequence(), - } - if err := txb.SetSignatures(sig); err != nil { - return nil, err - } - - return txf.txConfig.TxEncoder()(txb.GetTx()) + return txf.BuildSimTx(msgs...) } // CalculateGas simulates the execution of a transaction and returns the diff --git a/client/tx/tx_test.go b/client/tx/tx_test.go index 16a79472aaf..5568c0f10f5 100644 --- a/client/tx/tx_test.go +++ b/client/tx/tx_test.go @@ -58,7 +58,7 @@ func (m mockContext) Invoke(_ context.Context, _ string, _, reply interface{}, _ return nil } -func (mockContext) NewStream(context.Context, *grpc.StreamDesc, string, ...grpc.CallOption) (grpc.ClientStream, error) { +func (mockContext) NewStream(gocontext.Context, *grpc.StreamDesc, string, ...grpc.CallOption) (grpc.ClientStream, error) { panic("not implemented") } @@ -108,23 +108,25 @@ func TestCalculateGas(t *testing.T) { } } -func mockTxFactory(txCfg client.TxConfig) Factory { - return Factory{}. +func TestBuildSimTx(t *testing.T) { + txCfg := NewTestTxConfig() + + kb, err := keyring.New(t.Name(), "test", t.TempDir(), nil) + require.NoError(t, err) + + path := hd.CreateHDPath(118, 0, 0).String() + _, _, err = kb.NewMnemonic("test_key1", keyring.English, path, keyring.DefaultBIP39Passphrase, hd.Secp256k1) + require.NoError(t, err) + + txf := tx.Factory{}. WithTxConfig(txCfg). WithAccountNumber(50). WithSequence(23). WithFees("50stake"). WithMemo("memo"). - WithChainID("test-chain") -} - -func TestBuildSimTx(t *testing.T) { - txCfg, cdc := newTestTxConfig() - defaultSignMode, err := signing.APISignModeToInternal(txCfg.SignModeHandler().DefaultMode()) - require.NoError(t, err) - - kb, err := keyring.New(t.Name(), "test", t.TempDir(), nil, cdc) - require.NoError(t, err) + WithChainID("test-chain"). + WithSignMode(txCfg.SignModeHandler().DefaultMode()). + WithKeybase(kb) msg := banktypes.NewMsgSend(sdk.AccAddress("from"), sdk.AccAddress("to"), nil) bz, err := tx.BuildSimTx(txf, msg) @@ -133,12 +135,22 @@ func TestBuildSimTx(t *testing.T) { } func TestBuildUnsignedTx(t *testing.T) { - txConfig, cdc := newTestTxConfig() - kb, err := keyring.New(t.Name(), "test", t.TempDir(), nil, cdc) + kb, err := keyring.New(t.Name(), "test", t.TempDir(), nil) require.NoError(t, err) path := hd.CreateHDPath(118, 0, 0).String() + _, _, err = kb.NewMnemonic("test_key1", keyring.English, path, keyring.DefaultBIP39Passphrase, hd.Secp256k1) + require.NoError(t, err) + + txf := tx.Factory{}. + WithTxConfig(NewTestTxConfig()). + WithAccountNumber(50). + WithSequence(23). + WithFees("50stake"). + WithMemo("memo"). + WithChainID("test-chain") + msg := banktypes.NewMsgSend(sdk.AccAddress("from"), sdk.AccAddress("to"), nil) tx, err := tx.BuildUnsignedTx(txf, msg) require.NoError(t, err) @@ -255,6 +267,7 @@ func TestSign(t *testing.T) { WithSignMode(signingtypes.SignMode_SIGN_MODE_LEGACY_AMINO_JSON) msg1 := banktypes.NewMsgSend(info1.GetAddress(), sdk.AccAddress("to"), nil) msg2 := banktypes.NewMsgSend(info2.GetAddress(), sdk.AccAddress("to"), nil) + txb, err := tx.BuildUnsignedTx(txfNoKeybase, msg1, msg2) requireT.NoError(err) txb2, err := tx.BuildUnsignedTx(txfNoKeybase, msg1, msg2) @@ -317,45 +330,31 @@ func TestSign(t *testing.T) { }, /**** test double sign Direct mode - signing transaction with 2 or more DIRECT signers should fail in DIRECT mode ****/ + signing transaction with more than 2 signers should fail in DIRECT mode ****/ { - "direct: should append a DIRECT signature with existing AMINO", - // txb already has 1 AMINO signature + "direct: should fail to append a signature with different mode", txfDirect, txb, from1, false, - []cryptotypes.PubKey{pubKey2, pubKey1}, - nil, - }, - { - "direct: should add single DIRECT sig in multi-signers tx", - txfDirect, txb2, from1, false, - []cryptotypes.PubKey{pubKey1}, - nil, - }, - { - "direct: should fail to append 2nd DIRECT sig in multi-signers tx", - txfDirect, txb2, from2, false, []cryptotypes.PubKey{}, nil, }, { - "amino: should append 2nd AMINO sig in multi-signers tx with 1 DIRECT sig", - // txb2 already has 1 DIRECT signature - txfAmino, txb2, from2, false, + "direct: should fail to sign multi-signers tx", + txfDirect, txb2, from1, false, []cryptotypes.PubKey{}, nil, }, { - "direct: should overwrite multi-signers tx with DIRECT sig", + "direct: should fail to overwrite multi-signers tx", txfDirect, txb2, from1, true, - []cryptotypes.PubKey{pubKey1}, + []cryptotypes.PubKey{}, nil, }, } var prevSigs []signingtypes.SignatureV2 for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - err = Sign(clientCtx, tc.txf, tc.from, tc.txb, tc.overwrite) + t.Run(tc.name, func(_ *testing.T) { + err = tx.Sign(tc.txf, tc.from, tc.txb, tc.overwrite) if len(tc.expectedPKs) == 0 { requireT.Error(err) } else {