Refactor Gas/Fee Model (#3258)

This commit is contained in:
Alexander Bezobchuk 2019-01-18 11:45:20 -05:00 committed by Jack Zampolin
parent 8f7a222308
commit 36d1736a08
30 changed files with 785 additions and 249 deletions

View File

@ -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.

View File

@ -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

View File

@ -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) {

View File

@ -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", "<host>:<port> 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 ")

View File

@ -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)

View File

@ -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

View File

@ -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,

View File

@ -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 {

View File

@ -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()

View File

@ -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),
}

View File

@ -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)),
)
}

View File

@ -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

View File

@ -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

View File

@ -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,
},
}
}

View File

@ -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)
}

View File

@ -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

View File

@ -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)

View File

@ -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.

View File

@ -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)

View File

@ -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 {

View File

@ -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())
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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

View File

@ -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)
}
}

View File

@ -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))
// 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()
// 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())
}
if !stdFee.Amount.IsAllGTE(requiredFees) {
return sdk.ErrInsufficientFee(
fmt.Sprintf(
"insufficient fees; got: %q required: %q", stdFee.Amount, requiredFees,
),
).Result()
}
}
return sdk.Result{}

View File

@ -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,
)
}
}

View File

@ -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) {

View File

@ -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
}

View File

@ -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) {