diff --git a/CHANGELOG.md b/CHANGELOG.md index a739b8767615..135b0949910c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### 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 diff --git a/client/tx/factory.go b/client/tx/factory.go index 2cb951922d0a..8bf053905f0e 100644 --- a/client/tx/factory.go +++ b/client/tx/factory.go @@ -6,6 +6,11 @@ import ( "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/crypto/keyring" +<<<<<<< HEAD +======= + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" +>>>>>>> e65719060 (fix: remove hardcoded pubkey from tx simulation (#11282)) sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/tx/signing" ) @@ -189,3 +194,160 @@ func (f Factory) WithTimeoutHeight(height uint64) Factory { f.timeoutHeight = height return f } +<<<<<<< HEAD +======= + +// 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.offline && f.generateOnly { + if f.chainID != "" { + return nil, fmt.Errorf("chain ID cannot be used when offline and generate-only flags are set") + } + } else 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 ( + ok bool + pk cryptotypes.PubKey = &secp256k1.PubKey{} // use default public key type + ) + + if f.keybase != nil { + records, _ := f.keybase.List() + if len(records) == 0 { + return nil, errors.New("cannot build signature for simulation, key records slice is empty") + } + + // take the first record just for simulation purposes + pk, ok = records[0].PubKey.GetCachedValue().(cryptotypes.PubKey) + if !ok { + return nil, errors.New("cannot build signature for simulation, failed to convert proto Any to public key") + } + } + + // 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 +} +>>>>>>> e65719060 (fix: remove hardcoded pubkey from tx simulation (#11282)) diff --git a/client/tx/tx_test.go b/client/tx/tx_test.go index b095d05b9e5d..8440d5993f2e 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") } @@ -96,6 +97,14 @@ func TestCalculateGas(t *testing.T) { func TestBuildSimTx(t *testing.T) { txCfg := NewTestTxConfig() + encCfg := simapp.MakeTestEncodingConfig() + + kb, err := keyring.New(t.Name(), "test", t.TempDir(), nil, encCfg.Codec) + 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). @@ -104,7 +113,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,13 +123,23 @@ func TestBuildSimTx(t *testing.T) { } func TestBuildUnsignedTx(t *testing.T) { + encCfg := simapp.MakeTestEncodingConfig() + kb, err := keyring.New(t.Name(), "test", t.TempDir(), nil, encCfg.Codec) + 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") + WithChainID("test-chain"). + WithKeybase(kb) msg := banktypes.NewMsgSend(sdk.AccAddress("from"), sdk.AccAddress("to"), nil) tx, err := tx.BuildUnsignedTx(txf, msg) @@ -137,8 +157,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) @@ -185,24 +205,49 @@ 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 +<<<<<<< HEAD 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}, @@ -210,6 +255,41 @@ func TestSign(t *testing.T) { txfDirect, txb2, from1, false, []cryptotypes.PubKey{}, nil}, {"direct: should fail to overwrite multi-signers tx", txfDirect, txb2, from1, true, []cryptotypes.PubKey{}, nil}, +======= + signing transaction with 2 or more DIRECT signers should fail in DIRECT mode ****/ + { + "direct: should append a DIRECT signature with existing AMINO", + // txb already has 1 AMINO signature + 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, + []cryptotypes.PubKey{}, + nil, + }, + { + "direct: should overwrite multi-signers tx with DIRECT sig", + txfDirect, txb2, from1, true, + []cryptotypes.PubKey{pubKey1}, + nil, + }, +>>>>>>> e65719060 (fix: remove hardcoded pubkey from tx simulation (#11282)) } var prevSigs []signingtypes.SignatureV2 for _, tc := range testCases {