diff --git a/CHANGELOG.md b/CHANGELOG.md index df1281a89..520deb210 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,9 +45,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 2cb951922..13860bba1 100644 --- a/client/tx/factory.go +++ b/client/tx/factory.go @@ -1,11 +1,17 @@ package tx import ( + "errors" + "fmt" + "os" + "github.com/spf13/pflag" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" "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" ) @@ -189,3 +195,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 8e20be369..90c49514f 100644 --- a/client/tx/tx.go +++ b/client/tx/tx.go @@ -14,7 +14,6 @@ import ( "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" "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" @@ -221,66 +220,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 b095d05b9..9a4c60d07 100644 --- a/client/tx/tx_test.go +++ b/client/tx/tx_test.go @@ -45,6 +45,7 @@ func (m mockContext) Invoke(grpcCtx gocontext.Context, method string, req, reply return nil } + func (mockContext) NewStream(gocontext.Context, *grpc.StreamDesc, string, ...grpc.CallOption) (grpc.ClientStream, error) { panic("not implemented") } @@ -97,6 +98,13 @@ func TestCalculateGas(t *testing.T) { 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). @@ -104,7 +112,8 @@ func TestBuildSimTx(t *testing.T) { WithFees("50stake"). WithMemo("memo"). WithChainID("test-chain"). - WithSignMode(txCfg.SignModeHandler().DefaultMode()) + WithSignMode(txCfg.SignModeHandler().DefaultMode()). + WithKeybase(kb) msg := banktypes.NewMsgSend(sdk.AccAddress("from"), sdk.AccAddress("to"), nil) bz, err := tx.BuildSimTx(txf, msg) @@ -113,6 +122,14 @@ func TestBuildSimTx(t *testing.T) { } func TestBuildUnsignedTx(t *testing.T) { + 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). @@ -137,8 +154,8 @@ func TestSign(t *testing.T) { kr, err := keyring.New(t.Name(), "test", t.TempDir(), nil) requireT.NoError(err) - var from1 = "test_key1" - var from2 = "test_key2" + from1 := "test_key1" + from2 := "test_key2" // create a new key using a mnemonic generator and test if we can reuse seed to recreate that account _, seed, err := kr.NewMnemonic(from1, keyring.English, path, keyring.DefaultBIP39Passphrase, hd.Secp256k1) @@ -169,6 +186,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) @@ -185,35 +203,71 @@ func TestSign(t *testing.T) { expectedPKs []cryptotypes.PubKey matchingSigs []int // if not nil, check matching signature against old ones. }{ - {"should fail if txf without keyring", - txfNoKeybase, txb, from1, true, nil, nil}, - {"should fail for non existing key", - txfAmino, txb, "unknown", true, nil, nil}, - {"amino: should succeed with keyring", - txfAmino, txbSimple, from1, true, []cryptotypes.PubKey{pubKey1}, nil}, - {"direct: should succeed with keyring", - txfDirect, txbSimple, from1, true, []cryptotypes.PubKey{pubKey1}, nil}, + { + "should fail if txf without keyring", + txfNoKeybase, txb, from1, true, nil, nil, + }, + { + "should fail for non existing key", + txfAmino, txb, "unknown", true, nil, nil, + }, + { + "amino: should succeed with keyring", + txfAmino, txbSimple, from1, true, + []cryptotypes.PubKey{pubKey1}, + nil, + }, + { + "direct: should succeed with keyring", + txfDirect, txbSimple, from1, true, + []cryptotypes.PubKey{pubKey1}, + nil, + }, /**** test double sign Amino mode ****/ - {"amino: should sign multi-signers tx", - txfAmino, txb, from1, true, []cryptotypes.PubKey{pubKey1}, nil}, - {"amino: should append a second signature and not overwrite", - txfAmino, txb, from2, false, []cryptotypes.PubKey{pubKey1, pubKey2}, []int{0, 0}}, - {"amino: should overwrite a signature", - txfAmino, txb, from2, true, []cryptotypes.PubKey{pubKey2}, []int{1, 0}}, + { + "amino: should sign multi-signers tx", + txfAmino, txb, from1, true, + []cryptotypes.PubKey{pubKey1}, + nil, + }, + { + "amino: should append a second signature and not overwrite", + txfAmino, txb, from2, false, + []cryptotypes.PubKey{pubKey1, pubKey2}, + []int{0, 0}, + }, + { + "amino: should overwrite a signature", + txfAmino, txb, from2, true, + []cryptotypes.PubKey{pubKey2}, + []int{1, 0}, + }, /**** test double sign Direct mode signing transaction with more than 2 signers should fail in DIRECT mode ****/ - {"direct: should fail to append a signature with different mode", - txfDirect, txb, from1, false, []cryptotypes.PubKey{}, nil}, - {"direct: should fail to sign multi-signers tx", - txfDirect, txb2, from1, false, []cryptotypes.PubKey{}, nil}, - {"direct: should fail to overwrite multi-signers tx", - txfDirect, txb2, from1, true, []cryptotypes.PubKey{}, nil}, + { + "direct: should fail to append a signature with different mode", + txfDirect, txb, from1, false, + []cryptotypes.PubKey{}, + nil, + }, + { + "direct: should fail to sign multi-signers tx", + txfDirect, txb2, from1, false, + []cryptotypes.PubKey{}, + nil, + }, + { + "direct: should fail to overwrite multi-signers tx", + txfDirect, txb2, from1, true, + []cryptotypes.PubKey{}, + nil, + }, } var prevSigs []signingtypes.SignatureV2 for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { + 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)