diff --git a/PENDING.md b/PENDING.md index 1fdbd0ec9b9d..d4931e9b1e57 100644 --- a/PENDING.md +++ b/PENDING.md @@ -95,6 +95,11 @@ IMPROVEMENTS * [\#3158](https://github.com/cosmos/cosmos-sdk/pull/3158) Validate slashing genesis * [\#3172](https://github.com/cosmos/cosmos-sdk/pull/3172) Support minimum fees in a local testnet. * [\#3250](https://github.com/cosmos/cosmos-sdk/pull/3250) Refactor integration tests and increase coverage + * [\#3248](https://github.com/cosmos/cosmos-sdk/issues/3248) Refactor tx fee + model: + * Validators specify minimum gas prices instead of minimum fees + * Clients may provide either fees or gas prices directly + * The gas prices of a tx must meet a validator's minimum * [\#2859](https://github.com/cosmos/cosmos-sdk/issues/2859) Rename `TallyResult` in gov proposals to `FinalTallyResult` * [\#3286](https://github.com/cosmos/cosmos-sdk/pull/3286) Fix `gaiad gentx` printout of account's addresses, i.e. user bech32 instead of hex. diff --git a/baseapp/baseapp.go b/baseapp/baseapp.go index 509493443422..cfeb97214eeb 100644 --- a/baseapp/baseapp.go +++ b/baseapp/baseapp.go @@ -74,8 +74,9 @@ type BaseApp struct { // TODO move this in the future to baseapp param store on main store. consensusParams *abci.ConsensusParams - // spam prevention - minimumFees sdk.Coins + // The minimum gas prices a validator is willing to accept for processing a + // transaction. This is mainly used for DoS and spam prevention. + minGasPrices sdk.DecCoins // flag for sealing sealed bool @@ -213,13 +214,17 @@ func (app *BaseApp) initFromMainStore(mainKey *sdk.KVStoreKey) error { return nil } -func (app *BaseApp) setMinimumFees(fees sdk.Coins) { app.minimumFees = fees } +func (app *BaseApp) setMinGasPrices(gasPrices sdk.DecCoins) { + app.minGasPrices = gasPrices +} // NewContext returns a new Context with the correct store, the given header, and nil txBytes. func (app *BaseApp) NewContext(isCheckTx bool, header abci.Header) sdk.Context { if isCheckTx { - return sdk.NewContext(app.checkState.ms, header, true, app.Logger).WithMinimumFees(app.minimumFees) + return sdk.NewContext(app.checkState.ms, header, true, app.Logger). + WithMinGasPrices(app.minGasPrices) } + return sdk.NewContext(app.deliverState.ms, header, false, app.Logger) } @@ -240,7 +245,7 @@ func (app *BaseApp) setCheckState(header abci.Header) { ms := app.cms.CacheMultiStore() app.checkState = &state{ ms: ms, - ctx: sdk.NewContext(ms, header, true, app.Logger).WithMinimumFees(app.minimumFees), + ctx: sdk.NewContext(ms, header, true, app.Logger).WithMinGasPrices(app.minGasPrices), } } @@ -455,8 +460,9 @@ func handleQueryCustom(app *BaseApp, path []string, req abci.RequestQuery) (res } // Cache wrap the commit-multistore for safety. - ctx := sdk.NewContext(app.cms.CacheMultiStore(), app.checkState.ctx.BlockHeader(), true, app.Logger). - WithMinimumFees(app.minimumFees) + ctx := sdk.NewContext( + app.cms.CacheMultiStore(), app.checkState.ctx.BlockHeader(), true, app.Logger, + ).WithMinGasPrices(app.minGasPrices) // Passes the rest of the path as an argument to the querier. // For example, in the path "custom/gov/proposal/test", the gov querier gets []string{"proposal", "test"} as the path diff --git a/baseapp/options.go b/baseapp/options.go index 8729e5ac1287..8e6687335a3f 100644 --- a/baseapp/options.go +++ b/baseapp/options.go @@ -18,13 +18,14 @@ func SetPruning(opts sdk.PruningOptions) func(*BaseApp) { return func(bap *BaseApp) { bap.cms.SetPruning(opts) } } -// SetMinimumFees returns an option that sets the minimum fees on the app. -func SetMinimumFees(minFees string) func(*BaseApp) { - fees, err := sdk.ParseCoins(minFees) +// SetMinimumGasPrices returns an option that sets the minimum gas prices on the app. +func SetMinGasPrices(gasPricesStr string) func(*BaseApp) { + gasPrices, err := sdk.ParseDecCoins(gasPricesStr) if err != nil { - panic(fmt.Sprintf("invalid minimum fees: %v", err)) + panic(fmt.Sprintf("invalid minimum gas prices: %v", err)) } - return func(bap *BaseApp) { bap.setMinimumFees(fees) } + + return func(bap *BaseApp) { bap.setMinGasPrices(gasPrices) } } func (app *BaseApp) SetName(name string) { diff --git a/client/flags.go b/client/flags.go index d0d4bc10d8ab..50890635c4ef 100644 --- a/client/flags.go +++ b/client/flags.go @@ -30,6 +30,7 @@ const ( FlagSequence = "sequence" FlagMemo = "memo" FlagFees = "fees" + FlagGasPrices = "gas-prices" FlagAsync = "async" FlagJson = "json" FlagPrintResponse = "print-response" @@ -79,6 +80,7 @@ func PostCommands(cmds ...*cobra.Command) []*cobra.Command { c.Flags().Uint64(FlagSequence, 0, "Sequence number to sign the tx") c.Flags().String(FlagMemo, "", "Memo to send along with transaction") c.Flags().String(FlagFees, "", "Fees to pay along with transaction; eg: 10stake,1atom") + c.Flags().String(FlagGasPrices, "", "Gas prices to determine the transaction fee (e.g. 0.00001stake)") c.Flags().String(FlagNode, "tcp://localhost:26657", ": to tendermint rpc interface for this chain") c.Flags().Bool(FlagUseLedger, false, "Use a connected Ledger device") c.Flags().Float64(FlagGasAdjustment, DefaultGasAdjustment, "adjustment factor to be multiplied against the estimate returned by the tx simulation; if the gas limit is set manually this flag is ignored ") diff --git a/client/lcd/lcd_test.go b/client/lcd/lcd_test.go index ba85b95e3b15..4dcd4cea7d33 100644 --- a/client/lcd/lcd_test.go +++ b/client/lcd/lcd_test.go @@ -262,7 +262,8 @@ func TestCoinSendGenerateSignAndBroadcast(t *testing.T) { payload := authrest.SignBody{ Tx: msg, BaseReq: utils.NewBaseReq( - name1, pw, "", viper.GetString(client.FlagChainID), "", "", accnum, sequence, nil, false, false, + name1, pw, "", viper.GetString(client.FlagChainID), "", "", + accnum, sequence, nil, nil, false, false, ), } json, err := cdc.MarshalJSON(payload) diff --git a/client/lcd/swagger-ui/swagger.yaml b/client/lcd/swagger-ui/swagger.yaml index a52f80c8be8d..eed9d7e27691 100644 --- a/client/lcd/swagger-ui/swagger.yaml +++ b/client/lcd/swagger-ui/swagger.yaml @@ -2238,6 +2238,10 @@ definitions: type: array items: $ref: "#/definitions/Coin" + gas_prices: + type: array + items: + $ref: "#/definitions/DecCoin" generate_only: type: boolean example: false diff --git a/client/lcd/test_helpers.go b/client/lcd/test_helpers.go index 0e6a0cbc5116..6cc3309586d2 100644 --- a/client/lcd/test_helpers.go +++ b/client/lcd/test_helpers.go @@ -647,7 +647,7 @@ func doSign(t *testing.T, port, name, password, chainID string, accnum, sequence payload := authrest.SignBody{ Tx: msg, BaseReq: utils.NewBaseReq( - name, password, "", chainID, "", "", accnum, sequence, nil, false, false, + name, password, "", chainID, "", "", accnum, sequence, nil, nil, false, false, ), } json, err := cdc.MarshalJSON(payload) @@ -703,8 +703,9 @@ func doTransferWithGas(t *testing.T, port, seed, name, memo, password string, ad sequence := acc.GetSequence() chainID := viper.GetString(client.FlagChainID) - baseReq := utils.NewBaseReq(name, password, memo, chainID, gas, - fmt.Sprintf("%f", gasAdjustment), accnum, sequence, fees, + baseReq := utils.NewBaseReq( + name, password, memo, chainID, gas, + fmt.Sprintf("%f", gasAdjustment), accnum, sequence, fees, nil, generateOnly, simulate, ) @@ -736,7 +737,7 @@ func doDelegate(t *testing.T, port, name, password string, accnum := acc.GetAccountNumber() sequence := acc.GetSequence() chainID := viper.GetString(client.FlagChainID) - baseReq := utils.NewBaseReq(name, password, "", chainID, "", "", accnum, sequence, fees, false, false) + baseReq := utils.NewBaseReq(name, password, "", chainID, "", "", accnum, sequence, fees, nil, false, false) msg := msgDelegationsInput{ BaseReq: baseReq, DelegatorAddr: delAddr, @@ -770,7 +771,7 @@ func doUndelegate(t *testing.T, port, name, password string, accnum := acc.GetAccountNumber() sequence := acc.GetSequence() chainID := viper.GetString(client.FlagChainID) - baseReq := utils.NewBaseReq(name, password, "", chainID, "", "", accnum, sequence, fees, false, false) + baseReq := utils.NewBaseReq(name, password, "", chainID, "", "", accnum, sequence, fees, nil, false, false) msg := msgUndelegateInput{ BaseReq: baseReq, DelegatorAddr: delAddr, @@ -806,7 +807,7 @@ func doBeginRedelegation(t *testing.T, port, name, password string, sequence := acc.GetSequence() chainID := viper.GetString(client.FlagChainID) - baseReq := utils.NewBaseReq(name, password, "", chainID, "", "", accnum, sequence, fees, false, false) + baseReq := utils.NewBaseReq(name, password, "", chainID, "", "", accnum, sequence, fees, nil, false, false) msg := msgBeginRedelegateInput{ BaseReq: baseReq, @@ -1037,7 +1038,7 @@ func doSubmitProposal(t *testing.T, port, seed, name, password string, proposerA accnum := acc.GetAccountNumber() sequence := acc.GetSequence() chainID := viper.GetString(client.FlagChainID) - baseReq := utils.NewBaseReq(name, password, "", chainID, "", "", accnum, sequence, fees, false, false) + baseReq := utils.NewBaseReq(name, password, "", chainID, "", "", accnum, sequence, fees, nil, false, false) pr := postProposalReq{ Title: "Test", @@ -1133,7 +1134,7 @@ func doDeposit(t *testing.T, port, seed, name, password string, proposerAddr sdk accnum := acc.GetAccountNumber() sequence := acc.GetSequence() chainID := viper.GetString(client.FlagChainID) - baseReq := utils.NewBaseReq(name, password, "", chainID, "", "", accnum, sequence, fees, false, false) + baseReq := utils.NewBaseReq(name, password, "", chainID, "", "", accnum, sequence, fees, nil, false, false) dr := depositReq{ Depositor: proposerAddr, @@ -1187,7 +1188,7 @@ func doVote(t *testing.T, port, seed, name, password string, proposerAddr sdk.Ac accnum := acc.GetAccountNumber() sequence := acc.GetSequence() chainID := viper.GetString(client.FlagChainID) - baseReq := utils.NewBaseReq(name, password, "", chainID, "", "", accnum, sequence, fees, false, false) + baseReq := utils.NewBaseReq(name, password, "", chainID, "", "", accnum, sequence, fees, nil, false, false) vr := voteReq{ Voter: proposerAddr, @@ -1319,7 +1320,7 @@ func getSigningInfo(t *testing.T, port string, validatorPubKey string) slashing. func doUnjail(t *testing.T, port, seed, name, password string, valAddr sdk.ValAddress, fees sdk.Coins) (resultTx ctypes.ResultBroadcastTxCommit) { chainID := viper.GetString(client.FlagChainID) - baseReq := utils.NewBaseReq(name, password, "", chainID, "", "", 1, 1, fees, false, false) + baseReq := utils.NewBaseReq(name, password, "", chainID, "", "", 1, 1, fees, nil, false, false) ur := unjailReq{ BaseReq: baseReq, diff --git a/client/utils/rest.go b/client/utils/rest.go index 09663291e55b..f6bd18a4f583 100644 --- a/client/utils/rest.go +++ b/client/utils/rest.go @@ -101,23 +101,25 @@ func WriteGenerateStdTxResponse(w http.ResponseWriter, cdc *codec.Codec, txBldr // BaseReq defines a structure that can be embedded in other request structures // that all share common "base" fields. type BaseReq struct { - Name string `json:"name"` - Password string `json:"password"` - Memo string `json:"memo"` - ChainID string `json:"chain_id"` - AccountNumber uint64 `json:"account_number"` - Sequence uint64 `json:"sequence"` - Fees sdk.Coins `json:"fees"` - Gas string `json:"gas"` - GasAdjustment string `json:"gas_adjustment"` - GenerateOnly bool `json:"generate_only"` - Simulate bool `json:"simulate"` + Name string `json:"name"` + Password string `json:"password"` + Memo string `json:"memo"` + ChainID string `json:"chain_id"` + AccountNumber uint64 `json:"account_number"` + Sequence uint64 `json:"sequence"` + Fees sdk.Coins `json:"fees"` + GasPrices sdk.DecCoins `json:"gas_prices"` + Gas string `json:"gas"` + GasAdjustment string `json:"gas_adjustment"` + GenerateOnly bool `json:"generate_only"` + Simulate bool `json:"simulate"` } // NewBaseReq creates a new basic request instance and sanitizes its values func NewBaseReq( name, password, memo, chainID string, gas, gasAdjustment string, - accNumber, seq uint64, fees sdk.Coins, genOnly, simulate bool) BaseReq { + accNumber, seq uint64, fees sdk.Coins, gasPrices sdk.DecCoins, genOnly, simulate bool, +) BaseReq { return BaseReq{ Name: strings.TrimSpace(name), @@ -125,6 +127,7 @@ func NewBaseReq( Memo: strings.TrimSpace(memo), ChainID: strings.TrimSpace(chainID), Fees: fees, + GasPrices: gasPrices, Gas: strings.TrimSpace(gas), GasAdjustment: strings.TrimSpace(gasAdjustment), AccountNumber: accNumber, @@ -136,11 +139,10 @@ func NewBaseReq( // Sanitize performs basic sanitization on a BaseReq object. func (br BaseReq) Sanitize() BaseReq { - newBr := NewBaseReq( + return NewBaseReq( br.Name, br.Password, br.Memo, br.ChainID, br.Gas, br.GasAdjustment, - br.AccountNumber, br.Sequence, br.Fees, br.GenerateOnly, br.Simulate, + br.AccountNumber, br.Sequence, br.Fees, br.GasPrices, br.GenerateOnly, br.Simulate, ) - return newBr } // ValidateBasic performs basic validation of a BaseReq. If custom validation @@ -152,18 +154,28 @@ func (br BaseReq) ValidateBasic(w http.ResponseWriter) bool { case len(br.Password) == 0: WriteErrorResponse(w, http.StatusUnauthorized, "password required but not specified") return false + case len(br.ChainID) == 0: WriteErrorResponse(w, http.StatusUnauthorized, "chain-id required but not specified") return false - case !br.Fees.IsValid(): - WriteErrorResponse(w, http.StatusPaymentRequired, "invalid or insufficient fees") + + case !br.Fees.IsZero() && !br.GasPrices.IsZero(): + // both fees and gas prices were provided + WriteErrorResponse(w, http.StatusBadRequest, "cannot provide both fees and gas prices") + return false + + case !br.Fees.IsValid() && !br.GasPrices.IsValid(): + // neither fees or gas prices were provided + WriteErrorResponse(w, http.StatusPaymentRequired, "invalid fees or gas prices provided") return false } } + if len(br.Name) == 0 { WriteErrorResponse(w, http.StatusUnauthorized, "name required but not specified") return false } + return true } @@ -203,8 +215,12 @@ func ReadRESTReq(w http.ResponseWriter, r *http.Request, cdc *codec.Codec, req i // supplied messages. Finally, it broadcasts the signed transaction to a node. // // NOTE: Also see CompleteAndBroadcastTxCli. -// NOTE: Also see x/staking/client/rest/tx.go delegationsRequestHandlerFn. -func CompleteAndBroadcastTxREST(w http.ResponseWriter, r *http.Request, cliCtx context.CLIContext, baseReq BaseReq, msgs []sdk.Msg, cdc *codec.Codec) { +// NOTE: Also see x/stake/client/rest/tx.go delegationsRequestHandlerFn. +func CompleteAndBroadcastTxREST( + w http.ResponseWriter, r *http.Request, cliCtx context.CLIContext, + baseReq BaseReq, msgs []sdk.Msg, cdc *codec.Codec, +) { + gasAdjustment, ok := ParseFloat64OrReturnBadRequest(w, baseReq.GasAdjustment, client.DefaultGasAdjustment) if !ok { return @@ -216,9 +232,11 @@ func CompleteAndBroadcastTxREST(w http.ResponseWriter, r *http.Request, cliCtx c return } - txBldr := authtxb.NewTxBuilder(GetTxEncoder(cdc), baseReq.AccountNumber, + txBldr := authtxb.NewTxBuilder( + GetTxEncoder(cdc), baseReq.AccountNumber, baseReq.Sequence, gas, gasAdjustment, baseReq.Simulate, - baseReq.ChainID, baseReq.Memo, baseReq.Fees) + baseReq.ChainID, baseReq.Memo, baseReq.Fees, baseReq.GasPrices, + ) if baseReq.Simulate || simulateAndExecute { if gasAdjustment < 0 { diff --git a/cmd/gaia/cli_test/cli_test.go b/cmd/gaia/cli_test/cli_test.go index 32711945611a..6b644840599b 100644 --- a/cmd/gaia/cli_test/cli_test.go +++ b/cmd/gaia/cli_test/cli_test.go @@ -50,36 +50,30 @@ func TestGaiaCLIMinimumFees(t *testing.T) { f := InitFixtures(t) // start gaiad server with minimum fees - fees := fmt.Sprintf("--minimum_fees=%s,%s", sdk.NewInt64Coin(feeDenom, 2), sdk.NewInt64Coin(denom, 2)) + minGasPrice, _ := sdk.NewDecFromStr("0.000006") + fees := fmt.Sprintf( + "--minimum_gas_prices=%s,%s", + sdk.NewDecCoinFromDec(feeDenom, minGasPrice), + sdk.NewDecCoinFromDec(fee2Denom, minGasPrice), + ) proc := f.GDStart(fees) defer proc.Stop(false) barAddr := f.KeyAddress(keyBar) - // fooAddr := f.KeyAddress(keyFoo) - - // Check the amount of coins in the foo account to ensure that the right amount exists - fooAcc := f.QueryAccount(f.KeyAddress(keyFoo)) - require.Equal(t, int64(50), fooAcc.GetCoins().AmountOf(denom).Int64()) // Send a transaction that will get rejected - success, _, _ := f.TxSend(keyFoo, barAddr, sdk.NewInt64Coin(denom, 10)) + success, _, _ := f.TxSend(keyFoo, barAddr, sdk.NewInt64Coin(fee2Denom, 10)) require.False(f.T, success) tests.WaitForNextNBlocksTM(1, f.Port) - // Ensure tx w/ correct fees (staking) pass - txFees := fmt.Sprintf("--fees=%s", sdk.NewInt64Coin(denom, 23)) - success, _, _ = f.TxSend(keyFoo, barAddr, sdk.NewInt64Coin(denom, 10), txFees) + // Ensure tx w/ correct fees pass + txFees := fmt.Sprintf("--fees=%s,%s", sdk.NewInt64Coin(feeDenom, 2), sdk.NewInt64Coin(fee2Denom, 2)) + success, _, _ = f.TxSend(keyFoo, barAddr, sdk.NewInt64Coin(fee2Denom, 10), txFees) require.True(f.T, success) tests.WaitForNextNBlocksTM(1, f.Port) - // Ensure tx w/ correct fees (feetoken) pass - txFees = fmt.Sprintf("--fees=%s", sdk.NewInt64Coin(feeDenom, 23)) - success, _, _ = f.TxSend(keyFoo, barAddr, sdk.NewInt64Coin(feeDenom, 10), txFees) - require.True(f.T, success) - tests.WaitForNextNBlocksTM(2, f.Port) - - // Ensure tx w/ improper fees (footoken) fails - txFees = fmt.Sprintf("--fees=%s", sdk.NewInt64Coin(fooDenom, 23)) + // Ensure tx w/ improper fees fails + txFees = fmt.Sprintf("--fees=%s", sdk.NewInt64Coin(feeDenom, 5)) success, _, _ = f.TxSend(keyFoo, barAddr, sdk.NewInt64Coin(fooDenom, 10), txFees) require.False(f.T, success) @@ -87,12 +81,46 @@ func TestGaiaCLIMinimumFees(t *testing.T) { f.Cleanup() } +func TestGaiaCLIGasPrices(t *testing.T) { + t.Parallel() + f := InitFixtures(t) + + // start gaiad server with minimum fees + minGasPrice, _ := sdk.NewDecFromStr("0.000006") + proc := f.GDStart(fmt.Sprintf("--minimum_gas_prices=%s", sdk.NewDecCoinFromDec(feeDenom, minGasPrice))) + defer proc.Stop(false) + + barAddr := f.KeyAddress(keyBar) + + // insufficient gas prices (tx fails) + badGasPrice, _ := sdk.NewDecFromStr("0.000003") + success, _, _ := f.TxSend( + keyFoo, barAddr, sdk.NewInt64Coin(fooDenom, 50), + fmt.Sprintf("--gas-prices=%s", sdk.NewDecCoinFromDec(feeDenom, badGasPrice))) + require.False(t, success) + + // wait for a block confirmation + tests.WaitForNextNBlocksTM(1, f.Port) + + // sufficient gas prices (tx passes) + success, _, _ = f.TxSend( + keyFoo, barAddr, sdk.NewInt64Coin(fooDenom, 50), + fmt.Sprintf("--gas-prices=%s", sdk.NewDecCoinFromDec(feeDenom, minGasPrice))) + require.True(t, success) + + // wait for a block confirmation + tests.WaitForNextNBlocksTM(1, f.Port) + + f.Cleanup() +} + func TestGaiaCLIFeesDeduction(t *testing.T) { t.Parallel() f := InitFixtures(t) // start gaiad server with minimum fees - proc := f.GDStart(fmt.Sprintf("--minimum_fees=%s", sdk.NewInt64Coin(fooDenom, 1))) + minGasPrice, _ := sdk.NewDecFromStr("0.000006") + proc := f.GDStart(fmt.Sprintf("--minimum_gas_prices=%s", sdk.NewDecCoinFromDec(feeDenom, minGasPrice))) defer proc.Stop(false) // Save key addresses for later use @@ -100,12 +128,12 @@ func TestGaiaCLIFeesDeduction(t *testing.T) { barAddr := f.KeyAddress(keyBar) fooAcc := f.QueryAccount(fooAddr) - require.Equal(t, int64(1000), fooAcc.GetCoins().AmountOf(fooDenom).Int64()) + fooAmt := fooAcc.GetCoins().AmountOf(fooDenom) // test simulation success, _, _ := f.TxSend( keyFoo, barAddr, sdk.NewInt64Coin(fooDenom, 1000), - fmt.Sprintf("--fees=%s", sdk.NewInt64Coin(fooDenom, 1)), "--dry-run") + fmt.Sprintf("--fees=%s", sdk.NewInt64Coin(feeDenom, 2)), "--dry-run") require.True(t, success) // Wait for a block @@ -113,12 +141,12 @@ func TestGaiaCLIFeesDeduction(t *testing.T) { // ensure state didn't change fooAcc = f.QueryAccount(fooAddr) - require.Equal(t, int64(1000), fooAcc.GetCoins().AmountOf(fooDenom).Int64()) + require.Equal(t, fooAmt.Int64(), fooAcc.GetCoins().AmountOf(fooDenom).Int64()) // insufficient funds (coins + fees) tx fails success, _, _ = f.TxSend( - keyFoo, barAddr, sdk.NewInt64Coin(fooDenom, 1000), - fmt.Sprintf("--fees=%s", sdk.NewInt64Coin(fooDenom, 1))) + keyFoo, barAddr, sdk.NewInt64Coin(fooDenom, 10000000), + fmt.Sprintf("--fees=%s", sdk.NewInt64Coin(feeDenom, 2))) require.False(t, success) // Wait for a block @@ -126,12 +154,12 @@ func TestGaiaCLIFeesDeduction(t *testing.T) { // ensure state didn't change fooAcc = f.QueryAccount(fooAddr) - require.Equal(t, int64(1000), fooAcc.GetCoins().AmountOf(fooDenom).Int64()) + require.Equal(t, fooAmt.Int64(), fooAcc.GetCoins().AmountOf(fooDenom).Int64()) // test success (transfer = coins + fees) success, _, _ = f.TxSend( keyFoo, barAddr, sdk.NewInt64Coin(fooDenom, 500), - fmt.Sprintf("--fees=%s", sdk.NewInt64Coin(fooDenom, 300))) + fmt.Sprintf("--fees=%s", sdk.NewInt64Coin(feeDenom, 2))) require.True(t, success) f.Cleanup() diff --git a/cmd/gaia/cli_test/test_helpers.go b/cmd/gaia/cli_test/test_helpers.go index f46582e129e2..7e1b1b9797db 100644 --- a/cmd/gaia/cli_test/test_helpers.go +++ b/cmd/gaia/cli_test/test_helpers.go @@ -30,14 +30,16 @@ const ( denom = "stake" keyFoo = "foo" keyBar = "bar" - keyBaz = "baz" - keyFooBarBaz = "foobarbaz" fooDenom = "footoken" feeDenom = "feetoken" + fee2Denom = "fee2token" + keyBaz = "baz" + keyFooBarBaz = "foobarbaz" ) var startCoins = sdk.Coins{ - sdk.NewInt64Coin(feeDenom, 1000), + sdk.NewInt64Coin(feeDenom, 1000000), + sdk.NewInt64Coin(fee2Denom, 1000000), sdk.NewInt64Coin(fooDenom, 1000), sdk.NewInt64Coin(denom, 150), } diff --git a/cmd/gaia/cmd/gaiad/main.go b/cmd/gaia/cmd/gaiad/main.go index dbeb6afa3e07..1cb65b3c75ca 100644 --- a/cmd/gaia/cmd/gaiad/main.go +++ b/cmd/gaia/cmd/gaiad/main.go @@ -4,9 +4,6 @@ import ( "encoding/json" "io" - "github.com/cosmos/cosmos-sdk/baseapp" - "github.com/cosmos/cosmos-sdk/store" - "github.com/spf13/cobra" "github.com/spf13/viper" @@ -16,9 +13,11 @@ import ( "github.com/tendermint/tendermint/libs/log" tmtypes "github.com/tendermint/tendermint/types" + "github.com/cosmos/cosmos-sdk/baseapp" "github.com/cosmos/cosmos-sdk/cmd/gaia/app" gaiaInit "github.com/cosmos/cosmos-sdk/cmd/gaia/init" "github.com/cosmos/cosmos-sdk/server" + "github.com/cosmos/cosmos-sdk/store" sdk "github.com/cosmos/cosmos-sdk/types" ) @@ -56,9 +55,10 @@ func main() { } func newApp(logger log.Logger, db dbm.DB, traceStore io.Writer) abci.Application { - return app.NewGaiaApp(logger, db, traceStore, true, + return app.NewGaiaApp( + logger, db, traceStore, true, baseapp.SetPruning(store.NewPruningOptions(viper.GetString("pruning"))), - baseapp.SetMinimumFees(viper.GetString("minimum_fees")), + baseapp.SetMinGasPrices(viper.GetString(server.FlagMinGasPrices)), ) } diff --git a/cmd/gaia/init/testnet.go b/cmd/gaia/init/testnet.go index 5865827bb053..111a58032697 100644 --- a/cmd/gaia/init/testnet.go +++ b/cmd/gaia/init/testnet.go @@ -35,7 +35,6 @@ var ( flagNodeDaemonHome = "node-daemon-home" flagNodeCliHome = "node-cli-home" flagStartingIPAddress = "starting-ip-address" - flagMinimumFees = "minimum-fees" ) const nodeDirPerm = 0755 @@ -82,7 +81,8 @@ Example: client.FlagChainID, "", "genesis file chain-id, if left blank will be randomly created", ) cmd.Flags().String( - flagMinimumFees, fmt.Sprintf("1%s", stakingtypes.DefaultBondDenom), "Validator minimum fees", + server.FlagMinGasPrices, fmt.Sprintf("0.000006%s", stakingtypes.DefaultBondDenom), + "Minimum gas prices to accept for transactions; All fees in a tx must meet this minimum (e.g. 0.01photino,0.001stake)", ) return cmd @@ -104,7 +104,7 @@ func initTestnet(config *tmconfig.Config, cdc *codec.Codec) error { valPubKeys := make([]crypto.PubKey, numValidators) gaiaConfig := srvconfig.DefaultConfig() - gaiaConfig.MinFees = viper.GetString(flagMinimumFees) + gaiaConfig.MinGasPrices = viper.GetString(server.FlagMinGasPrices) var ( accs []app.GenesisAccount diff --git a/docs/gaia/gaiacli.md b/docs/gaia/gaiacli.md index 4484ca52ff99..d489df33b869 100644 --- a/docs/gaia/gaiacli.md +++ b/docs/gaia/gaiacli.md @@ -128,6 +128,33 @@ gaiacli keys show --multisig-threshold K name1 name2 name3 [...] For more information regarding how to generate, sign and broadcast transactions with a multi signature account see [Multisig Transactions](#multisig-transactions). +### Fees & Gas + +Each transaction may either supply fees or gas prices, but not both. Most users +will typically provide fees as this is the cost you will end up incurring for +the transaction being included in the ledger. + +Validator's have a minimum gas price (multi-denom) configuration and they use +this value when when determining if they should include the transaction in a block +during `CheckTx`, where `gasPrices >= minGasPrices`. Note, your transaction must +supply fees that match all the denominations the validator requires. + +__Note__: With such a mechanism in place, validators may start to prioritize +txs by `gasPrice` in the mempool, so providing higher fees or gas prices may yield +higher tx priority. + +e.g. + +```bash +gaiacli tx send ... --fees=100photino +``` + +or + +```bash +gaiacli tx send ... --gas-prices=0.000001stake +``` + ### Account #### Get Tokens diff --git a/server/config/config.go b/server/config/config.go index 239097b13737..e7b2d97faec8 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -7,13 +7,15 @@ import ( ) const ( - defaultMinimumFees = "" + defaultMinGasPrices = "" ) // BaseConfig defines the server's basic configuration type BaseConfig struct { - // Tx minimum fee - MinFees string `mapstructure:"minimum_fees"` + // The minimum gas prices a validator is willing to accept for processing a + // transaction. A transaction's fees must meet the minimum of each denomination + // specified in this config (e.g. 0.01photino,0.0001stake). + MinGasPrices string `mapstructure:"minimum_gas_prices"` } // Config defines the server's top level configuration @@ -21,17 +23,27 @@ type Config struct { BaseConfig `mapstructure:",squash"` } -// SetMinimumFee sets the minimum fee. -func (c *Config) SetMinimumFees(fees sdk.Coins) { c.MinFees = fees.String() } +// SetMinGasPrices sets the validator's minimum gas prices. +func (c *Config) SetMinGasPrices(gasPrices sdk.DecCoins) { + c.MinGasPrices = gasPrices.String() +} -// SetMinimumFee sets the minimum fee. -func (c *Config) MinimumFees() sdk.Coins { - fees, err := sdk.ParseCoins(c.MinFees) +// GetMinGasPrices returns the validator's minimum gas prices based on the set +// configuration. +func (c *Config) GetMinGasPrices() sdk.DecCoins { + gasPrices, err := sdk.ParseDecCoins(c.MinGasPrices) if err != nil { - panic(fmt.Sprintf("invalid minimum fees: %v", err)) + panic(fmt.Sprintf("invalid minimum gas prices: %v", err)) } - return fees + + return gasPrices } // DefaultConfig returns server's default configuration. -func DefaultConfig() *Config { return &Config{BaseConfig{MinFees: defaultMinimumFees}} } +func DefaultConfig() *Config { + return &Config{ + BaseConfig{ + MinGasPrices: defaultMinGasPrices, + }, + } +} diff --git a/server/config/config_test.go b/server/config/config_test.go index d68d84415eb2..c2f8832756b8 100644 --- a/server/config/config_test.go +++ b/server/config/config_test.go @@ -10,11 +10,11 @@ import ( func TestDefaultConfig(t *testing.T) { cfg := DefaultConfig() - require.True(t, cfg.MinimumFees().IsZero()) + require.True(t, cfg.GetMinGasPrices().IsZero()) } func TestSetMinimumFees(t *testing.T) { cfg := DefaultConfig() - cfg.SetMinimumFees(sdk.Coins{sdk.NewCoin("foo", sdk.NewInt(100))}) - require.Equal(t, "100foo", cfg.MinFees) + cfg.SetMinGasPrices(sdk.DecCoins{sdk.NewDecCoin("foo", 5)}) + require.Equal(t, "5.000000000000000000foo", cfg.MinGasPrices) } diff --git a/server/config/toml.go b/server/config/toml.go index fc2078cb44ec..481faf6eca2a 100644 --- a/server/config/toml.go +++ b/server/config/toml.go @@ -13,8 +13,10 @@ const defaultConfigTemplate = `# This is a TOML config file. ##### main base config options ##### -# Validators reject any tx from the mempool with less than the minimum fee per gas. -minimum_fees = "{{ .BaseConfig.MinFees }}" +# The minimum gas prices a validator is willing to accept for processing a +# transaction. A transaction's fees must meet the minimum of each denomination +# specified in this config (e.g. 0.01photino,0.0001stake). +minimum_gas_prices = "{{ .BaseConfig.MinGasPrices }}" ` var configTemplate *template.Template @@ -34,7 +36,8 @@ func ParseConfig() (*Config, error) { return conf, err } -// WriteConfigFile renders config using the template and writes it to configFilePath. +// WriteConfigFile renders config using the template and writes it to +// configFilePath. func WriteConfigFile(configFilePath string, config *Config) { var buffer bytes.Buffer diff --git a/server/start.go b/server/start.go index 2b94bb934a6d..b91a6a8dceb7 100644 --- a/server/start.go +++ b/server/start.go @@ -15,12 +15,13 @@ import ( "github.com/tendermint/tendermint/proxy" ) +// Tendermint full-node start flags const ( flagWithTendermint = "with-tendermint" flagAddress = "address" flagTraceStore = "trace-store" flagPruning = "pruning" - flagMinimumFees = "minimum_fees" + FlagMinGasPrices = "minimum_gas_prices" ) // StartCmd runs the service passed in, either stand-alone or in-process with @@ -47,7 +48,10 @@ func StartCmd(ctx *Context, appCreator AppCreator) *cobra.Command { cmd.Flags().String(flagAddress, "tcp://0.0.0.0:26658", "Listen address") cmd.Flags().String(flagTraceStore, "", "Enable KVStore tracing to an output file") cmd.Flags().String(flagPruning, "syncable", "Pruning strategy: syncable, nothing, everything") - cmd.Flags().String(flagMinimumFees, "", "Minimum fees validator will accept for transactions") + cmd.Flags().String( + FlagMinGasPrices, "", + "Minimum gas prices to accept for transactions; All fees in a tx must meet this minimum (e.g. 0.01photino,0.0001stake)", + ) // add support for all Tendermint-specific command line options tcmd.AddNodeFlags(cmd) diff --git a/types/coin.go b/types/coin.go index 21fa291f89ca..6fc0e8c5a3cc 100644 --- a/types/coin.go +++ b/types/coin.go @@ -299,41 +299,6 @@ func (coins Coins) IsAllLTE(coinsB Coins) bool { return coinsB.IsAllGTE(coins) } -// IsAnyGTE returns true iff coins contains at least one denom that is present -// at a greater or equal amount in coinsB; it returns false otherwise. -// -// NOTE: IsAnyGTE operates under the invariant that coins are sorted by -// denominations. -func (coins Coins) IsAnyGTE(coinsB Coins) bool { - if len(coinsB) == 0 { - return false - } - - j := 0 - for _, coin := range coins { - searchOther := true // terminator in case coins breaks the sorted invariant - - for j < len(coinsB) && searchOther { - switch strings.Compare(coin.Denom, coinsB[j].Denom) { - case -1: - // coin denom in less than the current other coin, so move to next coin - searchOther = false - case 0: - if coin.IsGTE(coinsB[j]) { - return true - } - - fallthrough // skip to next other coin - case 1: - // coin denom is greater than the current other coin, so move to next other coin - j++ - } - } - } - - return false -} - // IsZero returns true if there are no coins or all coins are zero. func (coins Coins) IsZero() bool { for _, coin := range coins { @@ -492,10 +457,12 @@ func (coins Coins) Sort() Coins { var ( // Denominations can be 3 ~ 16 characters long. - reDnm = `[[:alpha:]][[:alnum:]]{2,15}` - reAmt = `[[:digit:]]+` - reSpc = `[[:space:]]*` - reCoin = regexp.MustCompile(fmt.Sprintf(`^(%s)%s(%s)$`, reAmt, reSpc, reDnm)) + reDnm = `[[:alpha:]][[:alnum:]]{2,15}` + reAmt = `[[:digit:]]+` + reDecAmt = `[[:digit:]]*\.[[:digit:]]+` + reSpc = `[[:space:]]*` + reCoin = regexp.MustCompile(fmt.Sprintf(`^(%s)%s(%s)$`, reAmt, reSpc, reDnm)) + reDecCoin = regexp.MustCompile(fmt.Sprintf(`^(%s)%s(%s)$`, reDecAmt, reSpc, reDnm)) ) // ParseCoin parses a cli input for one coin type, returning errors if invalid. diff --git a/types/coin_test.go b/types/coin_test.go index fb2d67acac78..3f69ad2ccff4 100644 --- a/types/coin_test.go +++ b/types/coin_test.go @@ -368,22 +368,6 @@ func TestCoinsLTE(t *testing.T) { assert.True(t, Coins{}.IsAllLTE(Coins{{"a", one}})) } -func TestCoinsIsAnyGTE(t *testing.T) { - one := NewInt(1) - two := NewInt(2) - - assert.False(t, Coins{}.IsAnyGTE(Coins{})) - assert.False(t, Coins{{"a", one}}.IsAnyGTE(Coins{})) - assert.False(t, Coins{}.IsAnyGTE(Coins{{"a", one}})) - assert.False(t, Coins{{"a", one}}.IsAnyGTE(Coins{{"a", two}})) - assert.True(t, Coins{{"a", one}, {"b", two}}.IsAnyGTE(Coins{{"a", two}, {"b", one}})) - assert.True(t, Coins{{"a", one}}.IsAnyGTE(Coins{{"a", one}})) - assert.True(t, Coins{{"a", two}}.IsAnyGTE(Coins{{"a", one}})) - assert.True(t, Coins{{"a", one}}.IsAnyGTE(Coins{{"a", one}, {"b", two}})) - assert.True(t, Coins{{"a", one}, {"b", two}}.IsAnyGTE(Coins{{"a", one}, {"b", one}})) - assert.True(t, Coins{{"a", one}, {"b", one}}.IsAnyGTE(Coins{{"a", one}, {"b", two}})) -} - func TestParse(t *testing.T) { one := NewInt(1) diff --git a/types/context.go b/types/context.go index 71e1f5303e64..3b815ee20319 100644 --- a/types/context.go +++ b/types/context.go @@ -47,7 +47,7 @@ func NewContext(ms MultiStore, header abci.Header, isCheckTx bool, logger log.Lo c = c.WithLogger(logger) c = c.WithVoteInfos(nil) c = c.WithGasMeter(NewInfiniteGasMeter()) - c = c.WithMinimumFees(Coins{}) + c = c.WithMinGasPrices(DecCoins{}) c = c.WithConsensusParams(nil) return c } @@ -141,7 +141,7 @@ const ( contextKeyVoteInfos contextKeyGasMeter contextKeyBlockGasMeter - contextKeyMinimumFees + contextKeyMinGasPrices contextKeyConsensusParams ) @@ -169,7 +169,7 @@ func (c Context) BlockGasMeter() GasMeter { return c.Value(contextKeyBlockGasMet func (c Context) IsCheckTx() bool { return c.Value(contextKeyIsCheckTx).(bool) } -func (c Context) MinimumFees() Coins { return c.Value(contextKeyMinimumFees).(Coins) } +func (c Context) MinGasPrices() DecCoins { return c.Value(contextKeyMinGasPrices).(DecCoins) } func (c Context) ConsensusParams() *abci.ConsensusParams { return c.Value(contextKeyConsensusParams).(*abci.ConsensusParams) @@ -222,8 +222,8 @@ func (c Context) WithIsCheckTx(isCheckTx bool) Context { return c.withValue(contextKeyIsCheckTx, isCheckTx) } -func (c Context) WithMinimumFees(minFees Coins) Context { - return c.withValue(contextKeyMinimumFees, minFees) +func (c Context) WithMinGasPrices(gasPrices DecCoins) Context { + return c.withValue(contextKeyMinGasPrices, gasPrices) } func (c Context) WithConsensusParams(params *abci.ConsensusParams) Context { diff --git a/types/context_test.go b/types/context_test.go index a824184bd8fe..7edaa6e20270 100644 --- a/types/context_test.go +++ b/types/context_test.go @@ -163,7 +163,7 @@ func TestContextWithCustom(t *testing.T) { logger := NewMockLogger() voteinfos := []abci.VoteInfo{{}} meter := types.NewGasMeter(10000) - minFees := types.Coins{types.NewInt64Coin("feetoken", 1)} + minGasPrices := types.DecCoins{types.NewDecCoin("feetoken", 1)} ctx = types.NewContext(nil, header, ischeck, logger) require.Equal(t, header, ctx.BlockHeader()) @@ -174,7 +174,7 @@ func TestContextWithCustom(t *testing.T) { WithTxBytes(txbytes). WithVoteInfos(voteinfos). WithGasMeter(meter). - WithMinimumFees(minFees) + WithMinGasPrices(minGasPrices) require.Equal(t, height, ctx.BlockHeight()) require.Equal(t, chainid, ctx.ChainID()) require.Equal(t, ischeck, ctx.IsCheckTx()) @@ -182,5 +182,5 @@ func TestContextWithCustom(t *testing.T) { require.Equal(t, logger, ctx.Logger()) require.Equal(t, voteinfos, ctx.VoteInfos()) require.Equal(t, meter, ctx.GasMeter()) - require.Equal(t, minFees, types.Coins{types.NewInt64Coin("feetoken", 1)}) + require.Equal(t, minGasPrices, ctx.MinGasPrices()) } diff --git a/types/dec_coin.go b/types/dec_coin.go index 62b0bea1dceb..b70a1f476592 100644 --- a/types/dec_coin.go +++ b/types/dec_coin.go @@ -2,9 +2,15 @@ package types import ( "fmt" + "sort" "strings" + + "github.com/pkg/errors" ) +// ---------------------------------------------------------------------------- +// Decimal Coin + // Coins which can have additional decimal points type DecCoin struct { Denom string `json:"denom"` @@ -12,6 +18,13 @@ type DecCoin struct { } func NewDecCoin(denom string, amount int64) DecCoin { + if amount < 0 { + panic(fmt.Sprintf("negative decimal coin amount: %v\n", amount)) + } + if strings.ToLower(denom) != denom { + panic(fmt.Sprintf("denom cannot contain upper case characters: %s\n", denom)) + } + return DecCoin{ Denom: denom, Amount: NewDec(amount), @@ -19,6 +32,13 @@ func NewDecCoin(denom string, amount int64) DecCoin { } func NewDecCoinFromDec(denom string, amount Dec) DecCoin { + if amount.LT(ZeroDec()) { + panic(fmt.Sprintf("negative decimal coin amount: %v\n", amount)) + } + if strings.ToLower(denom) != denom { + panic(fmt.Sprintf("denom cannot contain upper case characters: %s\n", denom)) + } + return DecCoin{ Denom: denom, Amount: amount, @@ -26,6 +46,13 @@ func NewDecCoinFromDec(denom string, amount Dec) DecCoin { } func NewDecCoinFromCoin(coin Coin) DecCoin { + if coin.Amount.LT(ZeroInt()) { + panic(fmt.Sprintf("negative decimal coin amount: %v\n", coin.Amount)) + } + if strings.ToLower(coin.Denom) != coin.Denom { + panic(fmt.Sprintf("denom cannot contain upper case characters: %s\n", coin.Denom)) + } + return DecCoin{ Denom: coin.Denom, Amount: NewDecFromInt(coin.Amount), @@ -55,7 +82,21 @@ func (coin DecCoin) TruncateDecimal() (Coin, DecCoin) { return NewCoin(coin.Denom, truncated), DecCoin{coin.Denom, change} } -//_______________________________________________________________________ +// IsPositive returns true if coin amount is positive. +// +// TODO: Remove once unsigned integers are used. +func (coin DecCoin) IsPositive() bool { + return coin.Amount.IsPositive() +} + +// String implements the Stringer interface for DecCoin. It returns a +// human-readable representation of a decimal coin. +func (coin DecCoin) String() string { + return fmt.Sprintf("%v%v", coin.Amount, coin.Denom) +} + +// ---------------------------------------------------------------------------- +// Decimal Coins // coins with decimal type DecCoins []DecCoin @@ -68,6 +109,21 @@ func NewDecCoins(coins Coins) DecCoins { return dcs } +// String implements the Stringer interface for DecCoins. It returns a +// human-readable representation of decimal coins. +func (coins DecCoins) String() string { + if len(coins) == 0 { + return "" + } + + out := "" + for _, coin := range coins { + out += fmt.Sprintf("%v,", coin.String()) + } + + return out[:len(out)-1] +} + // return the coins with trunctated decimals, and return the change func (coins DecCoins) TruncateDecimal() (Coins, DecCoins) { changeSum := DecCoins{} @@ -201,3 +257,115 @@ func (coins DecCoins) IsZero() bool { } return true } + +// IsValid asserts the DecCoins are sorted, have positive amount, and Denom +// does not contain upper case characters. +func (coins DecCoins) IsValid() bool { + switch len(coins) { + case 0: + return true + + case 1: + if strings.ToLower(coins[0].Denom) != coins[0].Denom { + return false + } + return coins[0].IsPositive() + + default: + // check single coin case + if !(DecCoins{coins[0]}).IsValid() { + return false + } + + lowDenom := coins[0].Denom + for _, coin := range coins[1:] { + if strings.ToLower(coin.Denom) != coin.Denom { + return false + } + if coin.Denom <= lowDenom { + return false + } + if !coin.IsPositive() { + return false + } + + // we compare each coin against the last denom + lowDenom = coin.Denom + } + + return true + } +} + +//----------------------------------------------------------------------------- +// Sorting + +var _ sort.Interface = Coins{} + +//nolint +func (coins DecCoins) Len() int { return len(coins) } +func (coins DecCoins) Less(i, j int) bool { return coins[i].Denom < coins[j].Denom } +func (coins DecCoins) Swap(i, j int) { coins[i], coins[j] = coins[j], coins[i] } + +// Sort is a helper function to sort the set of decimal coins in-place. +func (coins DecCoins) Sort() DecCoins { + sort.Sort(coins) + return coins +} + +// ---------------------------------------------------------------------------- +// Parsing + +// ParseDecCoin parses a decimal coin from a string, returning an error if +// invalid. An empty string is considered invalid. +func ParseDecCoin(coinStr string) (coin DecCoin, err error) { + coinStr = strings.TrimSpace(coinStr) + + matches := reDecCoin.FindStringSubmatch(coinStr) + if matches == nil { + return DecCoin{}, fmt.Errorf("invalid decimal coin expression: %s", coinStr) + } + + amountStr, denomStr := matches[1], matches[2] + + amount, err := NewDecFromStr(amountStr) + if err != nil { + return DecCoin{}, errors.Wrap(err, fmt.Sprintf("failed to parse decimal coin amount: %s", amountStr)) + } + + if denomStr != strings.ToLower(denomStr) { + return DecCoin{}, fmt.Errorf("denom cannot contain upper case characters: %s", denomStr) + } + + return NewDecCoinFromDec(denomStr, amount), nil +} + +// ParseDecCoins will parse out a list of decimal coins separated by commas. +// If nothing is provided, it returns nil DecCoins. Returned decimal coins are +// sorted. +func ParseDecCoins(coinsStr string) (coins DecCoins, err error) { + coinsStr = strings.TrimSpace(coinsStr) + if len(coinsStr) == 0 { + return nil, nil + } + + coinStrs := strings.Split(coinsStr, ",") + for _, coinStr := range coinStrs { + coin, err := ParseDecCoin(coinStr) + if err != nil { + return nil, err + } + + coins = append(coins, coin) + } + + // sort coins for determinism + coins.Sort() + + // validate coins before returning + if !coins.IsValid() { + return nil, fmt.Errorf("parsed decimal coins are invalid: %#v", coins) + } + + return coins, nil +} diff --git a/types/dec_coin_test.go b/types/dec_coin_test.go index fe6907395527..b2502ced1de8 100644 --- a/types/dec_coin_test.go +++ b/types/dec_coin_test.go @@ -3,30 +3,80 @@ package types import ( "testing" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +func TestNewDecCoin(t *testing.T) { + require.NotPanics(t, func() { + NewDecCoin("a", 5) + }) + require.NotPanics(t, func() { + NewDecCoin("a", 0) + }) + require.Panics(t, func() { + NewDecCoin("A", 5) + }) + require.Panics(t, func() { + NewDecCoin("a", -5) + }) +} + +func TestNewDecCoinFromDec(t *testing.T) { + require.NotPanics(t, func() { + NewDecCoinFromDec("a", NewDec(5)) + }) + require.NotPanics(t, func() { + NewDecCoinFromDec("a", ZeroDec()) + }) + require.Panics(t, func() { + NewDecCoinFromDec("A", NewDec(5)) + }) + require.Panics(t, func() { + NewDecCoinFromDec("a", NewDec(-5)) + }) +} + +func TestNewDecCoinFromCoin(t *testing.T) { + require.NotPanics(t, func() { + NewDecCoinFromCoin(Coin{"a", NewInt(5)}) + }) + require.NotPanics(t, func() { + NewDecCoinFromCoin(Coin{"a", NewInt(0)}) + }) + require.Panics(t, func() { + NewDecCoinFromCoin(Coin{"A", NewInt(5)}) + }) + require.Panics(t, func() { + NewDecCoinFromCoin(Coin{"a", NewInt(-5)}) + }) +} + +func TestDecCoinIsPositive(t *testing.T) { + dc := NewDecCoin("a", 5) + require.True(t, dc.IsPositive()) + + dc = NewDecCoin("a", 0) + require.False(t, dc.IsPositive()) +} + func TestPlusDecCoin(t *testing.T) { - decCoinA1 := DecCoin{"A", NewDecWithPrec(11, 1)} - decCoinA2 := DecCoin{"A", NewDecWithPrec(22, 1)} - decCoinB1 := DecCoin{"B", NewDecWithPrec(11, 1)} + decCoinA1 := NewDecCoinFromDec("a", NewDecWithPrec(11, 1)) + decCoinA2 := NewDecCoinFromDec("a", NewDecWithPrec(22, 1)) + decCoinB1 := NewDecCoinFromDec("b", NewDecWithPrec(11, 1)) // regular add res := decCoinA1.Plus(decCoinA1) require.Equal(t, decCoinA2, res, "sum of coins is incorrect") // bad denom add - assert.Panics(t, func() { + require.Panics(t, func() { decCoinA1.Plus(decCoinB1) }, "expected panic on sum of different denoms") - } func TestPlusDecCoins(t *testing.T) { one := NewDec(1) zero := NewDec(0) - negone := NewDec(-1) two := NewDec(2) cases := []struct { @@ -34,11 +84,9 @@ func TestPlusDecCoins(t *testing.T) { inputTwo DecCoins expected DecCoins }{ - {DecCoins{{"A", one}, {"B", one}}, DecCoins{{"A", one}, {"B", one}}, DecCoins{{"A", two}, {"B", two}}}, - {DecCoins{{"A", zero}, {"B", one}}, DecCoins{{"A", zero}, {"B", zero}}, DecCoins{{"B", one}}}, - {DecCoins{{"A", zero}, {"B", zero}}, DecCoins{{"A", zero}, {"B", zero}}, DecCoins(nil)}, - {DecCoins{{"A", one}, {"B", zero}}, DecCoins{{"A", negone}, {"B", zero}}, DecCoins(nil)}, - {DecCoins{{"A", negone}, {"B", zero}}, DecCoins{{"A", zero}, {"B", zero}}, DecCoins{{"A", negone}}}, + {DecCoins{{"a", one}, {"b", one}}, DecCoins{{"a", one}, {"b", one}}, DecCoins{{"a", two}, {"b", two}}}, + {DecCoins{{"a", zero}, {"b", one}}, DecCoins{{"a", zero}, {"b", zero}}, DecCoins{{"b", one}}}, + {DecCoins{{"a", zero}, {"b", zero}}, DecCoins{{"a", zero}, {"b", zero}}, DecCoins(nil)}, } for tcIndex, tc := range cases { @@ -46,3 +94,132 @@ func TestPlusDecCoins(t *testing.T) { require.Equal(t, tc.expected, res, "sum of coins is incorrect, tc #%d", tcIndex) } } + +func TestSortDecCoins(t *testing.T) { + good := DecCoins{ + NewDecCoin("gas", 1), + NewDecCoin("mineral", 1), + NewDecCoin("tree", 1), + } + empty := DecCoins{ + NewDecCoin("gold", 0), + } + badSort1 := DecCoins{ + NewDecCoin("tree", 1), + NewDecCoin("gas", 1), + NewDecCoin("mineral", 1), + } + badSort2 := DecCoins{ // both are after the first one, but the second and third are in the wrong order + NewDecCoin("gas", 1), + NewDecCoin("tree", 1), + NewDecCoin("mineral", 1), + } + badAmt := DecCoins{ + NewDecCoin("gas", 1), + NewDecCoin("tree", 0), + NewDecCoin("mineral", 1), + } + dup := DecCoins{ + NewDecCoin("gas", 1), + NewDecCoin("gas", 1), + NewDecCoin("mineral", 1), + } + + cases := []struct { + coins DecCoins + before, after bool // valid before/after sort + }{ + {good, true, true}, + {empty, false, false}, + {badSort1, false, true}, + {badSort2, false, true}, + {badAmt, false, false}, + {dup, false, false}, + } + + for tcIndex, tc := range cases { + require.Equal(t, tc.before, tc.coins.IsValid(), "coin validity is incorrect before sorting, tc #%d", tcIndex) + tc.coins.Sort() + require.Equal(t, tc.after, tc.coins.IsValid(), "coin validity is incorrect after sorting, tc #%d", tcIndex) + } +} + +func TestDecCoinsIsValid(t *testing.T) { + testCases := []struct { + input DecCoins + expected bool + }{ + {DecCoins{}, true}, + {DecCoins{DecCoin{"a", NewDec(5)}}, true}, + {DecCoins{DecCoin{"a", NewDec(5)}, DecCoin{"b", NewDec(100000)}}, true}, + {DecCoins{DecCoin{"a", NewDec(-5)}}, false}, + {DecCoins{DecCoin{"A", NewDec(5)}}, false}, + {DecCoins{DecCoin{"a", NewDec(5)}, DecCoin{"B", NewDec(100000)}}, false}, + {DecCoins{DecCoin{"a", NewDec(5)}, DecCoin{"b", NewDec(-100000)}}, false}, + {DecCoins{DecCoin{"a", NewDec(-5)}, DecCoin{"b", NewDec(100000)}}, false}, + {DecCoins{DecCoin{"A", NewDec(5)}, DecCoin{"b", NewDec(100000)}}, false}, + } + + for i, tc := range testCases { + res := tc.input.IsValid() + require.Equal(t, tc.expected, res, "unexpected result for test case #%d, input: %v", i, tc.input) + } +} + +func TestParseDecCoins(t *testing.T) { + testCases := []struct { + input string + expectedResult DecCoins + expectedErr bool + }{ + {"", nil, false}, + {"4stake", nil, true}, + {"5.5atom,4stake", nil, true}, + {"0.0stake", nil, true}, + {"0.004STAKE", nil, true}, + { + "0.004stake", + DecCoins{NewDecCoinFromDec("stake", NewDecWithPrec(4000000000000000, Precision))}, + false, + }, + { + "5.04atom,0.004stake", + DecCoins{ + NewDecCoinFromDec("atom", NewDecWithPrec(5040000000000000000, Precision)), + NewDecCoinFromDec("stake", NewDecWithPrec(4000000000000000, Precision)), + }, + false, + }, + } + + for i, tc := range testCases { + res, err := ParseDecCoins(tc.input) + if tc.expectedErr { + require.Error(t, err, "expected error for test case #%d, input: %v", i, tc.input) + } else { + require.NoError(t, err, "unexpected error for test case #%d, input: %v", i, tc.input) + require.Equal(t, tc.expectedResult, res, "unexpected result for test case #%d, input: %v", i, tc.input) + } + } +} + +func TestDecCoinsString(t *testing.T) { + testCases := []struct { + input DecCoins + expected string + }{ + {DecCoins{}, ""}, + { + DecCoins{ + NewDecCoinFromDec("atom", NewDecWithPrec(5040000000000000000, Precision)), + NewDecCoinFromDec("stake", NewDecWithPrec(4000000000000000, Precision)), + }, + "5.040000000000000000atom,0.004000000000000000stake", + }, + } + + for i, tc := range testCases { + out := tc.input.String() + require.Equal(t, tc.expected, out, "unexpected result for test case #%d, input: %v", i, tc.input) + } +} diff --git a/types/decimal.go b/types/decimal.go index 8c9eff424cc3..0d1c54af3376 100644 --- a/types/decimal.go +++ b/types/decimal.go @@ -347,7 +347,7 @@ func chopPrecisionAndRound(d *big.Int) *big.Int { return d } - // get the trucated quotient and remainder + // get the truncated quotient and remainder quo, rem := d, big.NewInt(0) quo, rem = quo.QuoRem(d, precisionReuse, rem) @@ -419,6 +419,26 @@ func (d Dec) TruncateDec() Dec { return NewDecFromBigInt(chopPrecisionAndTruncateNonMutative(d.Int)) } +// Ceil returns the smallest interger value (as a decimal) that is greater than +// or equal to the given decimal. +func (d Dec) Ceil() Dec { + tmp := new(big.Int).Set(d.Int) + + quo, rem := tmp, big.NewInt(0) + quo, rem = quo.QuoRem(tmp, precisionReuse, rem) + + // no need to round with a zero remainder regardless of sign + if rem.Cmp(zeroInt) == 0 { + return NewDecFromBigInt(quo) + } + + if rem.Sign() == -1 { + return NewDecFromBigInt(quo) + } + + return NewDecFromBigInt(quo.Add(quo, oneInt)) +} + //___________________________________________________________________________________ // reuse nil values diff --git a/types/decimal_test.go b/types/decimal_test.go index c42cf98b5fcb..2193d841f0d6 100644 --- a/types/decimal_test.go +++ b/types/decimal_test.go @@ -384,3 +384,24 @@ func TestDecMulInt(t *testing.T) { require.Equal(t, tc.want, got, "Incorrect result on test case %d", i) } } + +func TestDecCeil(t *testing.T) { + testCases := []struct { + input Dec + expected Dec + }{ + {NewDecWithPrec(1000000000000000, Precision), NewDec(1)}, // 0.001 => 1.0 + {NewDecWithPrec(-1000000000000000, Precision), ZeroDec()}, // -0.001 => 0.0 + {ZeroDec(), ZeroDec()}, // 0.0 => 0.0 + {NewDecWithPrec(900000000000000000, Precision), NewDec(1)}, // 0.9 => 1.0 + {NewDecWithPrec(4001000000000000000, Precision), NewDec(5)}, // 4.001 => 5.0 + {NewDecWithPrec(-4001000000000000000, Precision), NewDec(-4)}, // -4.001 => -4.0 + {NewDecWithPrec(4700000000000000000, Precision), NewDec(5)}, // 4.7 => 5.0 + {NewDecWithPrec(-4700000000000000000, Precision), NewDec(-4)}, // -4.7 => -4.0 + } + + for i, tc := range testCases { + res := tc.input.Ceil() + require.Equal(t, tc.expected, res, "unexpected result for test case %d, input: %v", i, tc.input) + } +} diff --git a/x/auth/ante.go b/x/auth/ante.go index 2e84644b3441..453cd2e8b466 100644 --- a/x/auth/ante.go +++ b/x/auth/ante.go @@ -15,12 +15,6 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) -var ( - // TODO: Allow this to be configurable in the same way as minimum fees. - // ref: https://github.com/cosmos/cosmos-sdk/issues/3101 - gasPerUnitCost uint64 = 10000 // how much gas = 1 atom -) - // NewAnteHandler returns an AnteHandler that checks and increments sequence // numbers, checks signatures & account numbers, and deducts fees from the first // signer. @@ -44,7 +38,7 @@ func NewAnteHandler(ak AccountKeeper, fck FeeCollectionKeeper) sdk.AnteHandler { // if this is a CheckTx. This is only for local mempool purposes, and thus // is only ran on check tx. if ctx.IsCheckTx() && !simulate { - res := EnsureSufficientMempoolFees(ctx, stdTx) + res := EnsureSufficientMempoolFees(ctx, stdTx.Fee) if !res.IsOK() { return newCtx, res, true } @@ -262,19 +256,6 @@ func consumeMultisignatureVerificationGas(meter sdk.GasMeter, } } -func adjustFeesByGas(fees sdk.Coins, gas uint64) sdk.Coins { - gasCost := gas / gasPerUnitCost - gasFees := make(sdk.Coins, len(fees)) - - // TODO: Make this not price all coins in the same way - // TODO: Undo int64 casting once unsigned integers are supported for coins - for i := 0; i < len(fees); i++ { - gasFees[i] = sdk.NewInt64Coin(fees[i].Denom, int64(gasCost)) - } - - return fees.Plus(gasFees) -} - // DeductFees deducts fees from the given account. // // NOTE: We could use the CoinKeeper (in addition to the AccountKeeper, because @@ -313,28 +294,30 @@ func DeductFees(blockTime time.Time, acc Account, fee StdFee) (Account, sdk.Resu // enough fees to cover a proposer's minimum fees. An result object is returned // indicating success or failure. // -// NOTE: This should only be called during CheckTx as it cannot be part of +// TODO: Account for transaction size. +// +// Contract: This should only be called during CheckTx as it cannot be part of // consensus. -func EnsureSufficientMempoolFees(ctx sdk.Context, stdTx StdTx) sdk.Result { - // Currently we use a very primitive gas pricing model with a constant - // gasPrice where adjustFeesByGas handles calculating the amount of fees - // required based on the provided gas. - // - // TODO: - // - Make the gasPrice not a constant, and account for tx size. - // - Make Gas an unsigned integer and use tx basic validation - if stdTx.Fee.Gas <= 0 { - return sdk.ErrInternal(fmt.Sprintf("gas supplied must be a positive integer: %d", stdTx.Fee.Gas)).Result() - } - requiredFees := adjustFeesByGas(ctx.MinimumFees(), stdTx.Fee.Gas) +func EnsureSufficientMempoolFees(ctx sdk.Context, stdFee StdFee) sdk.Result { + minGasPrices := ctx.MinGasPrices() + if !minGasPrices.IsZero() { + requiredFees := make(sdk.Coins, len(minGasPrices)) + + // Determine the required fees by multiplying each required minimum gas + // price by the gas limit, where fee = ceil(minGasPrice * gasLimit). + glDec := sdk.NewDec(int64(stdFee.Gas)) + for i, gp := range minGasPrices { + fee := gp.Amount.Mul(glDec) + requiredFees[i] = sdk.NewInt64Coin(gp.Denom, fee.Ceil().RoundInt64()) + } - // NOTE: !A.IsAllGTE(B) is not the same as A.IsAllLT(B). - if !ctx.MinimumFees().IsZero() && !stdTx.Fee.Amount.IsAnyGTE(requiredFees) { - // validators reject any tx from the mempool with less than the minimum fee per gas * gas factor - return sdk.ErrInsufficientFee( - fmt.Sprintf( - "insufficient fee, got: %q required: %q", stdTx.Fee.Amount, requiredFees), - ).Result() + if !stdFee.Amount.IsAllGTE(requiredFees) { + return sdk.ErrInsufficientFee( + fmt.Sprintf( + "insufficient fees; got: %q required: %q", stdFee.Amount, requiredFees, + ), + ).Result() + } } return sdk.Result{} diff --git a/x/auth/ante_test.go b/x/auth/ante_test.go index e8e435ffd2f3..bbe4f62fd64d 100644 --- a/x/auth/ante_test.go +++ b/x/auth/ante_test.go @@ -635,25 +635,6 @@ func expectedGasCostByKeys(pubkeys []crypto.PubKey) uint64 { } return cost } -func TestAdjustFeesByGas(t *testing.T) { - type args struct { - fee sdk.Coins - gas uint64 - } - tests := []struct { - name string - args args - want sdk.Coins - }{ - {"nil coins", args{sdk.Coins{}, 100000}, sdk.Coins{}}, - {"nil coins", args{sdk.Coins{sdk.NewInt64Coin("a", 10), sdk.NewInt64Coin("b", 0)}, 100000}, sdk.Coins{sdk.NewInt64Coin("a", 20), sdk.NewInt64Coin("b", 10)}}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - require.True(t, tt.want.IsEqual(adjustFeesByGas(tt.args.fee, tt.args.gas))) - }) - } -} func TestCountSubkeys(t *testing.T) { genPubKeys := func(n int) []crypto.PubKey { @@ -723,3 +704,51 @@ func TestAnteHandlerSigLimitExceeded(t *testing.T) { tx = newTestTx(ctx, msgs, privs, accnums, seqs, fee) checkInvalidTx(t, anteHandler, ctx, tx, false, sdk.CodeTooManySignatures) } + +func TestEnsureSufficientMempoolFees(t *testing.T) { + // setup + input := setupTestInput() + ctx := input.ctx.WithMinGasPrices( + sdk.DecCoins{ + sdk.NewDecCoinFromDec("photino", sdk.NewDecWithPrec(1000000, sdk.Precision)), // 0.0001photino + sdk.NewDecCoinFromDec("stake", sdk.NewDecWithPrec(10000, sdk.Precision)), // 0.000001stake + }, + ) + + testCases := []struct { + input StdFee + expectedOK bool + }{ + {NewStdFee(200000, sdk.Coins{sdk.NewInt64Coin("stake", 1)}), false}, + {NewStdFee(200000, sdk.Coins{sdk.NewInt64Coin("photino", 20)}), false}, + { + NewStdFee( + 200000, + sdk.Coins{ + sdk.NewInt64Coin("photino", 20), + sdk.NewInt64Coin("stake", 1), + }, + ), + true, + }, + { + NewStdFee( + 200000, + sdk.Coins{ + sdk.NewInt64Coin("atom", 2), + sdk.NewInt64Coin("photino", 20), + sdk.NewInt64Coin("stake", 1), + }, + ), + true, + }, + } + + for i, tc := range testCases { + res := EnsureSufficientMempoolFees(ctx, tc.input) + require.Equal( + t, tc.expectedOK, res.IsOK(), + "unexpected result; tc #%d, input: %v, log: %v", i, tc.input, res.Log, + ) + } +} diff --git a/x/auth/client/rest/sign.go b/x/auth/client/rest/sign.go index a104c7fa14cf..7f7787ea4298 100644 --- a/x/auth/client/rest/sign.go +++ b/x/auth/client/rest/sign.go @@ -22,7 +22,6 @@ type SignBody struct { // nolint: unparam // sign tx REST handler func SignTxRequestHandlerFn(cdc *codec.Codec, cliCtx context.CLIContext) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { var m SignBody @@ -51,7 +50,9 @@ func SignTxRequestHandlerFn(cdc *codec.Codec, cliCtx context.CLIContext) http.Ha false, m.BaseReq.ChainID, m.Tx.GetMemo(), - m.Tx.Fee.Amount) + m.Tx.Fee.Amount, + nil, + ) signedTx, err := txBldr.SignStdTx(m.BaseReq.Name, m.BaseReq.Password, m.Tx, m.AppendSig) if keyerror.IsErrKeyNotFound(err) { diff --git a/x/auth/client/txbuilder/txbuilder.go b/x/auth/client/txbuilder/txbuilder.go index c94d0a1b1c41..ed5f4339d360 100644 --- a/x/auth/client/txbuilder/txbuilder.go +++ b/x/auth/client/txbuilder/txbuilder.go @@ -23,10 +23,15 @@ type TxBuilder struct { chainID string memo string fees sdk.Coins + gasPrices sdk.DecCoins } -// NewTxBuilder returns a new initialized TxBuilder -func NewTxBuilder(txEncoder sdk.TxEncoder, accNumber, seq, gas uint64, gasAdj float64, simulateAndExecute bool, chainID, memo string, fees sdk.Coins) TxBuilder { +// NewTxBuilder returns a new initialized TxBuilder. +func NewTxBuilder( + txEncoder sdk.TxEncoder, accNumber, seq, gas uint64, gasAdj float64, + simulateAndExecute bool, chainID, memo string, fees sdk.Coins, gasPrices sdk.DecCoins, +) TxBuilder { + return TxBuilder{ txEncoder: txEncoder, accountNumber: accNumber, @@ -37,6 +42,7 @@ func NewTxBuilder(txEncoder sdk.TxEncoder, accNumber, seq, gas uint64, gasAdj fl chainID: chainID, memo: memo, fees: fees, + gasPrices: gasPrices, } } @@ -52,7 +58,11 @@ func NewTxBuilderFromCLI() TxBuilder { chainID: viper.GetString(client.FlagChainID), memo: viper.GetString(client.FlagMemo), } - return txbldr.WithFees(viper.GetString(client.FlagFees)) + + txbldr = txbldr.WithFees(viper.GetString(client.FlagFees)) + txbldr = txbldr.WithGasPrices(viper.GetString(client.FlagGasPrices)) + + return txbldr } // GetTxEncoder returns the transaction encoder @@ -83,6 +93,9 @@ func (bldr TxBuilder) GetMemo() string { return bldr.memo } // GetFees returns the fees for the transaction func (bldr TxBuilder) GetFees() sdk.Coins { return bldr.fees } +// GetGasPrices returns the gas prices set for the transaction, if any. +func (bldr TxBuilder) GetGasPrices() sdk.DecCoins { return bldr.gasPrices } + // WithTxEncoder returns a copy of the context with an updated codec. func (bldr TxBuilder) WithTxEncoder(txEncoder sdk.TxEncoder) TxBuilder { bldr.txEncoder = txEncoder @@ -107,10 +120,22 @@ func (bldr TxBuilder) WithFees(fees string) TxBuilder { if err != nil { panic(err) } + bldr.fees = parsedFees return bldr } +// WithGasPrices returns a copy of the context with updated gas prices. +func (bldr TxBuilder) WithGasPrices(gasPrices string) TxBuilder { + parsedGasPrices, err := sdk.ParseDecCoins(gasPrices) + if err != nil { + panic(err) + } + + bldr.gasPrices = parsedGasPrices + return bldr +} + // WithSequence returns a copy of the context with an updated sequence number. func (bldr TxBuilder) WithSequence(sequence uint64) TxBuilder { bldr.sequence = sequence @@ -137,13 +162,30 @@ func (bldr TxBuilder) Build(msgs []sdk.Msg) (StdSignMsg, error) { return StdSignMsg{}, errors.Errorf("chain ID required but not specified") } + fees := bldr.fees + if !bldr.gasPrices.IsZero() { + if !fees.IsZero() { + return StdSignMsg{}, errors.New("cannot provide both fees and gas prices") + } + + glDec := sdk.NewDec(int64(bldr.gas)) + + // Derive the fees based on the provided gas prices, where + // fee = ceil(gasPrice * gasLimit). + fees = make(sdk.Coins, len(bldr.gasPrices)) + for i, gp := range bldr.gasPrices { + fee := gp.Amount.Mul(glDec) + fees[i] = sdk.NewInt64Coin(gp.Denom, fee.Ceil().RoundInt64()) + } + } + return StdSignMsg{ ChainID: bldr.chainID, AccountNumber: bldr.accountNumber, Sequence: bldr.sequence, Memo: bldr.memo, Msgs: msgs, - Fee: auth.NewStdFee(bldr.gas, bldr.fees), + Fee: auth.NewStdFee(bldr.gas, fees), }, nil } diff --git a/x/auth/client/txbuilder/txbuilder_test.go b/x/auth/client/txbuilder/txbuilder_test.go index 7d3dfc7ec7bc..fd2eeefc3491 100644 --- a/x/auth/client/txbuilder/txbuilder_test.go +++ b/x/auth/client/txbuilder/txbuilder_test.go @@ -11,7 +11,7 @@ import ( "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/auth" - stakingTypes "github.com/cosmos/cosmos-sdk/x/staking/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" ) var ( @@ -30,6 +30,7 @@ func TestTxBuilderBuild(t *testing.T) { ChainID string Memo string Fees sdk.Coins + GasPrices sdk.DecCoins } defaultMsg := []sdk.Msg{sdk.NewTestMsg(addr)} tests := []struct { @@ -43,27 +44,56 @@ func TestTxBuilderBuild(t *testing.T) { TxEncoder: auth.DefaultTxEncoder(codec.New()), AccountNumber: 1, Sequence: 1, - Gas: 100, + Gas: 200000, GasAdjustment: 1.1, SimulateGas: false, ChainID: "test-chain", - Memo: "hello from Voyager !", - Fees: sdk.Coins{sdk.NewCoin(stakingTypes.DefaultBondDenom, sdk.NewInt(1))}, + Memo: "hello from Voyager 1!", + Fees: sdk.Coins{sdk.NewCoin(stakingtypes.DefaultBondDenom, sdk.NewInt(1))}, }, defaultMsg, StdSignMsg{ ChainID: "test-chain", AccountNumber: 1, Sequence: 1, - Memo: "hello from Voyager !", + Memo: "hello from Voyager 1!", Msgs: defaultMsg, - Fee: auth.NewStdFee(100, sdk.Coins{sdk.NewCoin(stakingTypes.DefaultBondDenom, sdk.NewInt(1))}), + Fee: auth.NewStdFee(200000, sdk.Coins{sdk.NewCoin(stakingtypes.DefaultBondDenom, sdk.NewInt(1))}), + }, + false, + }, + { + fields{ + TxEncoder: auth.DefaultTxEncoder(codec.New()), + AccountNumber: 1, + Sequence: 1, + Gas: 200000, + GasAdjustment: 1.1, + SimulateGas: false, + ChainID: "test-chain", + Memo: "hello from Voyager 2!", + GasPrices: sdk.DecCoins{sdk.NewDecCoinFromDec(stakingtypes.DefaultBondDenom, sdk.NewDecWithPrec(10000, sdk.Precision))}, + }, + defaultMsg, + StdSignMsg{ + ChainID: "test-chain", + AccountNumber: 1, + Sequence: 1, + Memo: "hello from Voyager 2!", + Msgs: defaultMsg, + Fee: auth.NewStdFee(200000, sdk.Coins{sdk.NewCoin(stakingtypes.DefaultBondDenom, sdk.NewInt(1))}), }, false, }, } + for i, tc := range tests { - bldr := NewTxBuilder(tc.fields.TxEncoder, tc.fields.AccountNumber, tc.fields.Sequence, tc.fields.Gas, tc.fields.GasAdjustment, tc.fields.SimulateGas, tc.fields.ChainID, tc.fields.Memo, tc.fields.Fees) + bldr := NewTxBuilder( + tc.fields.TxEncoder, tc.fields.AccountNumber, tc.fields.Sequence, + tc.fields.Gas, tc.fields.GasAdjustment, tc.fields.SimulateGas, + tc.fields.ChainID, tc.fields.Memo, tc.fields.Fees, tc.fields.GasPrices, + ) + got, err := bldr.Build(tc.msgs) require.Equal(t, tc.wantErr, (err != nil), "TxBuilder.Build() error = %v, wantErr %v, tc %d", err, tc.wantErr, i) if !reflect.DeepEqual(got, tc.want) {