Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: tx prioritization #101

Merged
merged 8 commits into from
Jun 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions tests/e2e/consumer.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,17 @@ func CCVChainConstructor(t *testing.T, spec *interchaintest.ChainSpec) []*cosmos
Version: providerVersion,
NumValidators: &providerNumValidators,
ChainConfig: ibc.ChainConfig{
GasPrices: "1uatom",
GasAdjustment: 1.5,
GasPrices: "0uatom",
GasAdjustment: 0.0,
ChainID: providerChainID,
TrustingPeriod: "336h",
ModifyGenesis: cosmos.ModifyGenesis(
[]cosmos.GenesisKV{
cosmos.NewGenesisKV("app_state.provider.params.blocks_per_epoch", "1"),
},
),
}},
},
},
},
)

Expand Down
89 changes: 62 additions & 27 deletions x/feemarket/ante/fee.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,23 +113,50 @@ func (dfd feeMarketCheckDecorator) anteHandle(ctx sdk.Context, tx sdk.Tx, simula
ctx = ctx.WithMinGasPrices(sdk.NewDecCoins(minGasPrice))

if !simulate {
_, tip, err := CheckTxFee(ctx, minGasPrice, feeCoin, feeGas, true)
_, _, err := CheckTxFee(ctx, minGasPrice, feeCoin, feeGas, true)
if err != nil {
return ctx, errorsmod.Wrapf(err, "error checking fee")
}
ctx = ctx.WithPriority(getTxPriority(tip, int64(gas)))

priorityFee, err := dfd.resolveTxPriorityCoins(ctx, feeCoin, params.FeeDenom)
if err != nil {
return ctx, errorsmod.Wrapf(err, "error resolving fee priority")
}

baseGasPrice, err := dfd.feemarketKeeper.GetMinGasPrice(ctx, params.FeeDenom)
if err != nil {
return ctx, err
}

ctx = ctx.WithPriority(GetTxPriority(priorityFee, int64(gas), baseGasPrice))
return next(ctx, tx, simulate)
}
return next(ctx, tx, simulate)
}

// resolveTxPriorityCoins converts the coins to the proper denom used for tx prioritization calculation.
func (dfd feeMarketCheckDecorator) resolveTxPriorityCoins(ctx sdk.Context, fee sdk.Coin, baseDenom string) (sdk.Coin, error) {
if fee.Denom == baseDenom {
return fee, nil
}

feeDec := sdk.NewDecCoinFromCoin(fee)
convertedDec, err := dfd.feemarketKeeper.GetDenomResolver().ConvertToDenom(ctx, feeDec, baseDenom)
if err != nil {
return sdk.Coin{}, err
}

// truncate down
return sdk.NewCoin(baseDenom, convertedDec.Amount.TruncateInt()), nil
}

// CheckTxFee implements the logic for the fee market to check if a Tx has provided sufficient
// fees given the current state of the fee market. Returns an error if insufficient fees.
func CheckTxFee(ctx sdk.Context, minGasPrice sdk.DecCoin, feeCoin sdk.Coin, feeGas int64, isAnte bool) (payCoin sdk.Coin, tip sdk.Coin, err error) {
func CheckTxFee(ctx sdk.Context, gasPrice sdk.DecCoin, feeCoin sdk.Coin, feeGas int64, isAnte bool) (payCoin sdk.Coin, tip sdk.Coin, err error) {
payCoin = feeCoin

// Ensure that the provided fees meet the minimum
if !minGasPrice.IsZero() {
if !gasPrice.IsZero() {
var (
requiredFee sdk.Coin
consumedFee sdk.Coin
Expand All @@ -141,50 +168,58 @@ func CheckTxFee(ctx sdk.Context, minGasPrice sdk.DecCoin, feeCoin sdk.Coin, feeG
gcDec := sdkmath.LegacyNewDec(gasConsumed)
glDec := sdkmath.LegacyNewDec(feeGas)

consumedFeeAmount := minGasPrice.Amount.Mul(gcDec)
limitFee := minGasPrice.Amount.Mul(glDec)
consumedFee = sdk.NewCoin(minGasPrice.Denom, consumedFeeAmount.Ceil().RoundInt())
requiredFee = sdk.NewCoin(minGasPrice.Denom, limitFee.Ceil().RoundInt())
consumedFeeAmount := gasPrice.Amount.Mul(gcDec)
limitFee := gasPrice.Amount.Mul(glDec)

consumedFee = sdk.NewCoin(gasPrice.Denom, consumedFeeAmount.Ceil().RoundInt())
requiredFee = sdk.NewCoin(gasPrice.Denom, limitFee.Ceil().RoundInt())

if !payCoin.IsGTE(requiredFee) {
return sdk.Coin{}, sdk.Coin{}, sdkerrors.ErrInsufficientFee.Wrapf(
"got: %s required: %s, minGasPrice: %s, gas: %d",
payCoin,
requiredFee,
minGasPrice,
gasPrice,
gasConsumed,
)
}

if isAnte {
tip = payCoin.Sub(requiredFee)
// set fee coins to be required amount if checking
payCoin = requiredFee
} else {
// tip is the difference between payCoin and the required fee
tip = payCoin.Sub(requiredFee)
// set fee coin to be ONLY the consumed amount if we are calculated consumed fee to deduct
tip = payCoin.Sub(consumedFee)
payCoin = consumedFee
}
}

return payCoin, tip, nil
}

// getTxPriority returns a naive tx priority based on the amount of the smallest denomination of the gas price
// provided in a transaction.
// NOTE: This implementation should be used with a great consideration as it opens potential attack vectors
// where txs with multiple coins could not be prioritized as expected.
func getTxPriority(fee sdk.Coin, gas int64) int64 {
var priority int64
p := int64(math.MaxInt64)
gasPrice := fee.Amount.QuoRaw(gas)
if gasPrice.IsInt64() {
p = gasPrice.Int64()
}
if p < priority {
priority = p
const (
// gasPricePrecision is the amount of digit precision to scale the gas prices to.
gasPricePrecision = 6
)

// GetTxPriority returns a naive tx priority based on the amount of gas price provided in a transaction.
//
// The fee amount is divided by the gasLimit to calculate "Effective Gas Price".
// This value is then normalized and scaled into an integer, so it can be used as a priority.
//
// effectiveGasPrice = feeAmount / gas limit (denominated in fee per gas)
// normalizedGasPrice = effectiveGasPrice / currentGasPrice (floor is 1. The minimum effective gas price can ever be is current gas price)
// scaledGasPrice = normalizedGasPrice * 10 ^ gasPricePrecision (amount of decimal places in the normalized gas price to consider when converting to int64).
func GetTxPriority(fee sdk.Coin, gasLimit int64, currentGasPrice sdk.DecCoin) int64 {
effectiveGasPrice := fee.Amount.ToLegacyDec().QuoInt64(gasLimit)
normalizedGasPrice := effectiveGasPrice.Quo(currentGasPrice.Amount)
scaledGasPrice := normalizedGasPrice.MulInt64(int64(math.Pow10(gasPricePrecision)))

// overflow panic protection
if scaledGasPrice.GTE(sdkmath.LegacyNewDec(math.MaxInt64)) {
return math.MaxInt64
} else if scaledGasPrice.LTE(sdkmath.LegacyOneDec()) {
return 0
}

return priority
return scaledGasPrice.TruncateInt64()
}
52 changes: 52 additions & 0 deletions x/feemarket/fuzz/tx_priority_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package fuzz_test

import (
"math"
"testing"

sdkmath "cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/require"
"pgregory.net/rapid"

"github.com/skip-mev/feemarket/x/feemarket/ante"
)

type input struct {
payFee sdk.Coin
gasLimit int64
currentGasPrice sdk.DecCoin
}

// TestGetTxPriority ensures that tx priority is properly bounded
func TestGetTxPriority(t *testing.T) {
rapid.Check(t, func(t *rapid.T) {
inputs := createRandomInput(t)

priority := ante.GetTxPriority(inputs.payFee, inputs.gasLimit, inputs.currentGasPrice)
require.GreaterOrEqual(t, priority, int64(0))
require.LessOrEqual(t, priority, int64(math.MaxInt64))
})
}

// CreateRandomInput returns a random inputs to the priority function.
func createRandomInput(t *rapid.T) input {
denom := "skip"

price := rapid.Int64Range(1, 1_000_000_000).Draw(t, "gas price")
priceDec := sdkmath.LegacyNewDecWithPrec(price, 6)

gasLimit := rapid.Int64Range(1_000_000, 1_000_000_000_000).Draw(t, "gas limit")

if priceDec.MulInt64(gasLimit).GTE(sdkmath.LegacyNewDec(math.MaxInt64)) {
t.Fatalf("not supposed to happen")
}

payFeeAmt := rapid.Int64Range(priceDec.MulInt64(gasLimit).TruncateInt64(), math.MaxInt64).Draw(t, "fee amount")

return input{
payFee: sdk.NewCoin(denom, sdkmath.NewInt(payFeeAmt)),
gasLimit: gasLimit,
currentGasPrice: sdk.NewDecCoinFromDec(denom, priceDec),
}
}
7 changes: 3 additions & 4 deletions x/feemarket/post/fee_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,12 @@ import (
"testing"

"cosmossdk.io/math"

"github.com/cosmos/cosmos-sdk/testutil/testdata"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
"github.com/stretchr/testify/mock"

sdk "github.com/cosmos/cosmos-sdk/types"

antesuite "github.com/skip-mev/feemarket/x/feemarket/ante/suite"
"github.com/skip-mev/feemarket/x/feemarket/post"
"github.com/skip-mev/feemarket/x/feemarket/types"
Expand Down Expand Up @@ -154,7 +152,8 @@ func TestPostHandle(t *testing.T) {
resolvableDenom = "atom"
)

gasLimit := antesuite.NewTestGasLimit()
// exact cost of transaction
gasLimit := uint64(27284)
validFeeAmount := types.DefaultMinBaseGasPrice.MulInt64(int64(gasLimit))
validFeeAmountWithTip := validFeeAmount.Add(math.LegacyNewDec(100))
validFee := sdk.NewCoins(sdk.NewCoin(baseDenom, validFeeAmount.TruncateInt()))
Expand Down
Loading