Skip to content

Commit

Permalink
feat: in-protocol minimum gas price (#107)
Browse files Browse the repository at this point in the history
Resolves babylonlabs-io/pm#49

This PR introduces the in-protocol minimum gas price mechanism, in which
the consensus (more specifically the AnteHandler) enforces every tx has
to set a gas price at least 0.002 ubbn. The PR also provides relevant
tests. The impl closely follows
celestiaorg/celestia-app#2985.

In addition, this PR creates a new package `app/ante` to abstract out
the construction of the AnteHandler for Babylon, following the practice
at Celestia.

- RFC: babylonlabs-io/pm#56
- ADR: babylonlabs-io/pm#61
  • Loading branch information
SebastianElvis authored Oct 1, 2024
1 parent c11bbb3 commit ae2e53e
Show file tree
Hide file tree
Showing 18 changed files with 380 additions and 93 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)

### State Machine Breaking

* [#107](https://github.com/babylonlabs-io/babylon/pull/107) Implement ADR-027 and
enable in-protocol minimum gas price
* [#103](https://github.com/babylonlabs-io/babylon/pull/103) Add token distribution
to upgrade handler and rename `signet-launch` to `v1`
* [#55](https://github.com/babylonlabs-io/babylon/pull/55) Remove `x/zoneconcierge`
module

### Bug fixes

### Misc Improvements

* [#106](https://github.com/babylonlabs-io/babylon/pull/106) Add CLI command for
Expand Down
24 changes: 0 additions & 24 deletions app/ante.go

This file was deleted.

91 changes: 91 additions & 0 deletions app/ante/ante.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package ante

import (
"cosmossdk.io/core/store"
circuitkeeper "cosmossdk.io/x/circuit/keeper"
txsigning "cosmossdk.io/x/tx/signing"
wasmapp "github.com/CosmWasm/wasmd/app"
wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper"
wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types"
bbn "github.com/babylonlabs-io/babylon/types"
btcckeeper "github.com/babylonlabs-io/babylon/x/btccheckpoint/keeper"
epochingkeeper "github.com/babylonlabs-io/babylon/x/epoching/keeper"
sdk "github.com/cosmos/cosmos-sdk/types"
authante "github.com/cosmos/cosmos-sdk/x/auth/ante"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
ibckeeper "github.com/cosmos/ibc-go/v8/modules/core/keeper"
)

// NewAnteHandler creates a new AnteHandler for the Babylon chain.
func NewAnteHandler(
accountKeeper authante.AccountKeeper,
bankKeeper authtypes.BankKeeper,
feegrantKeeper authante.FeegrantKeeper,
signModeHandler *txsigning.HandlerMap,
ibcKeeper *ibckeeper.Keeper,
wasmConfig *wasmtypes.WasmConfig,
wasmKeeper *wasmkeeper.Keeper,
circuitKeeper *circuitkeeper.Keeper,
epochingKeeper *epochingkeeper.Keeper,
btcConfig *bbn.BtcConfig,
btccKeeper *btcckeeper.Keeper,
txCounterStoreService store.KVStoreService,
) sdk.AnteHandler {
// initialize AnteHandler, which includes
// - authAnteHandler
// - custom wasm ante handler NewLimitSimulationGasDecorator and NewCountTXDecorator
// - Extra decorators introduced in Babylon, such as DropValidatorMsgDecorator that delays validator-related messages
//
// We are using constructor from wasmapp as it introduces custom wasm ante handle decorators
// early in chain of ante handlers.
authAnteHandler, err := wasmapp.NewAnteHandler(
wasmapp.HandlerOptions{
HandlerOptions: authante.HandlerOptions{
AccountKeeper: accountKeeper,
BankKeeper: bankKeeper,
SignModeHandler: signModeHandler,
FeegrantKeeper: feegrantKeeper,
SigGasConsumer: authante.DefaultSigVerificationGasConsumer,
// CheckTxFeeWithGlobalMinGasPrices will enforce the global minimum
// gas price for all transactions.
TxFeeChecker: CheckTxFeeWithGlobalMinGasPrices,
},
IBCKeeper: ibcKeeper,
WasmConfig: wasmConfig,
TXCounterStoreService: txCounterStoreService,
WasmKeeper: wasmKeeper,
CircuitKeeper: circuitKeeper,
},
)

if err != nil {
panic(err)
}

anteHandler := sdk.ChainAnteDecorators(
NewWrappedAnteHandler(authAnteHandler),
epochingkeeper.NewDropValidatorMsgDecorator(epochingKeeper),
NewBtcValidationDecorator(btcConfig, btccKeeper),
)

return anteHandler
}

// WrappedAnteHandler is the wrapped AnteHandler that implements the `AnteDecorator` interface, which has a single function `AnteHandle`.
// It allows us to chain an existing AnteHandler with other decorators by using `sdk.ChainAnteDecorators`.
type WrappedAnteHandler struct {
ah sdk.AnteHandler
}

// NewWrappedAnteHandler creates a new WrappedAnteHandler for a given AnteHandler.
func NewWrappedAnteHandler(ah sdk.AnteHandler) WrappedAnteHandler {
return WrappedAnteHandler{ah}
}

func (wah WrappedAnteHandler) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (newCtx sdk.Context, err error) {
newCtx, err = wah.ah(ctx, tx, simulate)
if err != nil {
return newCtx, err
}
return next(newCtx, tx, simulate)
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package app
package ante

import (
bbn "github.com/babylonlabs-io/babylon/types"
Expand All @@ -9,12 +9,12 @@ import (
)

type BtcValidationDecorator struct {
BtcCfg bbn.BtcConfig
BtcCfg *bbn.BtcConfig
btccheckpointKeeper *btccheckpointkeeper.Keeper
}

func NewBtcValidationDecorator(
cfg bbn.BtcConfig,
cfg *bbn.BtcConfig,
k *btccheckpointkeeper.Keeper,
) BtcValidationDecorator {
return BtcValidationDecorator{
Expand Down
66 changes: 66 additions & 0 deletions app/ante/fee_checker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package ante

import (
"fmt"

errors "cosmossdk.io/errors"
sdkmath "cosmossdk.io/math"
appparams "github.com/babylonlabs-io/babylon/app/params"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerror "github.com/cosmos/cosmos-sdk/types/errors"
)

const (
// priorityScalingFactor is a scaling factor to convert the gas price to a priority.
priorityScalingFactor = 1_000_000
)

// CheckTxFeeWithGlobalMinGasPrices implements the default fee logic, where the minimum price per
// unit of gas is fixed and set globally, and the tx priority is computed from the gas price.
// adapted from https://github.com/celestiaorg/celestia-app/pull/2985
func CheckTxFeeWithGlobalMinGasPrices(ctx sdk.Context, tx sdk.Tx) (sdk.Coins, int64, error) {
feeTx, ok := tx.(sdk.FeeTx)
if !ok {
return nil, 0, errors.Wrap(sdkerror.ErrTxDecode, "Tx must be a FeeTx")
}

denom := appparams.DefaultBondDenom

fee := feeTx.GetFee().AmountOf(denom)
gas := feeTx.GetGas()

// convert the global minimum gas price to a big.Int
globalMinGasPrice, err := sdkmath.LegacyNewDecFromStr(fmt.Sprintf("%f", appparams.GlobalMinGasPrice))
if err != nil {
return nil, 0, errors.Wrap(err, "invalid GlobalMinGasPrice")
}

gasInt := sdkmath.NewIntFromUint64(gas)
minFee := globalMinGasPrice.MulInt(gasInt).RoundInt()

if !fee.GTE(minFee) {
return nil, 0, errors.Wrapf(sdkerror.ErrInsufficientFee, "insufficient fees; got: %s required: %s", fee, minFee)
}

priority := getTxPriority(feeTx.GetFee(), int64(gas))
return feeTx.GetFee(), priority, 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 not be used for txs with multiple coins.
func getTxPriority(fee sdk.Coins, gas int64) int64 {
var priority int64
for _, c := range fee {
p := c.Amount.Mul(sdkmath.NewInt(priorityScalingFactor)).QuoRaw(gas)
if !p.IsInt64() {
continue
}
// take the lowest priority as the tx priority
if priority == 0 || p.Int64() < priority {
priority = p.Int64()
}
}

return priority
}
100 changes: 100 additions & 0 deletions app/ante/fee_checker_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package ante_test

import (
"math"
"testing"

bbnapp "github.com/babylonlabs-io/babylon/app"
"github.com/babylonlabs-io/babylon/app/ante"
appparams "github.com/babylonlabs-io/babylon/app/params"
"github.com/babylonlabs-io/babylon/testutil/datagen"
sdk "github.com/cosmos/cosmos-sdk/types"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
"github.com/stretchr/testify/require"
)

// TestCheckTxFeeWithGlobalMinGasPrices tests the CheckTxFeeWithGlobalMinGasPrices
// function
// adapted from https://github.com/celestiaorg/celestia-app/pull/2985
func TestCheckTxFeeWithGlobalMinGasPrices(t *testing.T) {
encCfg := bbnapp.GetEncodingConfig()

builder := encCfg.TxConfig.NewTxBuilder()
err := builder.SetMsgs(
banktypes.NewMsgSend(
datagen.GenRandomAccount().GetAddress(),
datagen.GenRandomAccount().GetAddress(),
sdk.NewCoins(sdk.NewInt64Coin(appparams.DefaultBondDenom, 10)),
),
)
require.NoError(t, err)

feeAmount := int64(1000)
ctx := sdk.Context{}

testCases := []struct {
name string
fee sdk.Coins
gasLimit uint64
appVersion uint64
expErr bool
}{
{
name: "bad tx; fee below required minimum",
fee: sdk.NewCoins(sdk.NewInt64Coin(appparams.DefaultBondDenom, feeAmount-1)),
gasLimit: uint64(float64(feeAmount) / appparams.GlobalMinGasPrice),
appVersion: uint64(2),
expErr: true,
},
{
name: "good tx; fee equal to required minimum",
fee: sdk.NewCoins(sdk.NewInt64Coin(appparams.DefaultBondDenom, feeAmount)),
gasLimit: uint64(float64(feeAmount) / appparams.GlobalMinGasPrice),
appVersion: uint64(2),
expErr: false,
},
{
name: "good tx; fee above required minimum",
fee: sdk.NewCoins(sdk.NewInt64Coin(appparams.DefaultBondDenom, feeAmount+1)),
gasLimit: uint64(float64(feeAmount) / appparams.GlobalMinGasPrice),
appVersion: uint64(2),
expErr: false,
},
{
name: "good tx; gas limit and fee are maximum values",
fee: sdk.NewCoins(sdk.NewInt64Coin(appparams.DefaultBondDenom, math.MaxInt64)),
gasLimit: math.MaxUint64,
appVersion: uint64(2),
expErr: false,
},
{
name: "bad tx; gas limit and fee are 0",
fee: sdk.NewCoins(sdk.NewInt64Coin(appparams.DefaultBondDenom, 0)),
gasLimit: 0,
appVersion: uint64(2),
expErr: false,
},
{
name: "good tx; minFee = 0.8, rounds up to 1",
fee: sdk.NewCoins(sdk.NewInt64Coin(appparams.DefaultBondDenom, feeAmount)),
gasLimit: 400,
appVersion: uint64(2),
expErr: false,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
builder.SetGasLimit(tc.gasLimit)
builder.SetFeeAmount(tc.fee)
tx := builder.GetTx()

_, _, err := ante.CheckTxFeeWithGlobalMinGasPrices(ctx, tx)
if tc.expErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}
60 changes: 60 additions & 0 deletions app/ante/get_tx_priority_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package ante

import (
"testing"

appparams "github.com/babylonlabs-io/babylon/app/params"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/assert"
)

// TestGetTxPriority tests the getTxPriority function
// adapted from https://github.com/celestiaorg/celestia-app/pull/2985
func TestGetTxPriority(t *testing.T) {
denom := appparams.DefaultBondDenom

cases := []struct {
name string
fee sdk.Coins
gas int64
expectedPri int64
}{
{
name: "1 bbn fee large gas",
fee: sdk.NewCoins(sdk.NewInt64Coin(denom, 1_000_000)),
gas: 1000000,
expectedPri: 1000000,
},
{
name: "1 ubbn fee small gas",
fee: sdk.NewCoins(sdk.NewInt64Coin(denom, 1)),
gas: 1,
expectedPri: 1000000,
},
{
name: "2 ubbn fee small gas",
fee: sdk.NewCoins(sdk.NewInt64Coin(denom, 2)),
gas: 1,
expectedPri: 2000000,
},
{
name: "1_000_000 bbn fee normal gas tx",
fee: sdk.NewCoins(sdk.NewInt64Coin(denom, 1_000_000_000_000)),
gas: 75000,
expectedPri: 13333333333333,
},
{
name: "0.001 ubbn gas price",
fee: sdk.NewCoins(sdk.NewInt64Coin(denom, 1_000)),
gas: 1_000_000,
expectedPri: 1000,
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
pri := getTxPriority(tc.fee, tc.gas)
assert.Equal(t, tc.expectedPri, pri)
})
}
}
Loading

0 comments on commit ae2e53e

Please sign in to comment.