Skip to content

Commit

Permalink
Dev/mempool 1559 (#6811)
Browse files Browse the repository at this point in the history
* initial commit

* More skeleton code

* Is this good for testing?

* sync

* better comment

* fix

* Update

* add cli

* fix code

* ensure it doesn't go below min

* export eip struct

* Update smth

* update

* moire prints

* change target to 40M to see what happens

* reparam + config

* Nicolas/1559 persist (#6812)

* persisting to disk

* better prints after testing

* add max base fee

* cloning everywhere to be safe

* chore: add unit tests to eip code

* Add a recheck bound

* Cfg + adjust constants

* Make a GlobalMempool

* Ok testing

* remove log

* lint

* Apply @Pipello 's code suggestions

* chore: add comments to eip-1559 code (#6818)

* chore: add comments to eip-1559 code

* Update x/txfees/keeper/mempool-1559/code.go

Co-authored-by: Roman <roman@osmosis.team>

* Update x/txfees/keeper/mempool-1559/code.go

Co-authored-by: Roman <roman@osmosis.team>

* chore: add logger, use const instead of var

* chore: remove sleep in test

---------

Co-authored-by: Roman <roman@osmosis.team>

* add app.toml options for all vars

* Revert "add app.toml options for all vars"

This reverts commit 6fa54ba.

---------

Co-authored-by: Adam Tucker <adamleetucker@outlook.com>
Co-authored-by: Nicolas Lara <nicolaslara@gmail.com>
Co-authored-by: PaddyMc <paddymchale@hotmail.com>
Co-authored-by: Roman <roman@osmosis.team>
  • Loading branch information
5 people authored Nov 3, 2023
1 parent 95a560c commit a8af889
Show file tree
Hide file tree
Showing 13 changed files with 813 additions and 42 deletions.
3 changes: 3 additions & 0 deletions cmd/osmosisd/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,9 @@ arbitrage-min-gas-fee = ".005"
# This is the minimum gas fee any tx with high gas demand should have, denominated in uosmo per gas
# Default value of ".0025" then means that a tx with 1 million gas costs (.0025 uosmo/gas) * 1_000_000 gas = .0025 osmo
min-gas-price-for-high-gas-tx = ".0025"
# This parameter enables EIP-1559 like fee market logic in the mempool
adaptive-fee-enabled = "false"
`

return OsmosisAppTemplate, OsmosisAppCfg
Expand Down
15 changes: 15 additions & 0 deletions proto/osmosis/txfees/v1beta1/query.proto
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ service Query {
rpc BaseDenom(QueryBaseDenomRequest) returns (QueryBaseDenomResponse) {
option (google.api.http).get = "/osmosis/txfees/v1beta1/base_denom";
}

// Returns a list of all base denom tokens and their corresponding pools.
rpc GetEipBaseFee(QueryEipBaseFeeRequest) returns (QueryEipBaseFeeResponse) {
option (google.api.http).get = "/osmosis/txfees/v1beta1/cur_eip_base_fee";
}
}

message QueryFeeTokensRequest {}
Expand Down Expand Up @@ -73,3 +78,13 @@ message QueryBaseDenomRequest {}
message QueryBaseDenomResponse {
string base_denom = 1 [ (gogoproto.moretags) = "yaml:\"base_denom\"" ];
}

message QueryEipBaseFeeRequest {}
message QueryEipBaseFeeResponse {
string base_fee = 1 [
(gogoproto.moretags) = "yaml:\"base_fee\"",

(gogoproto.customtype) = "cosmossdk.io/math.LegacyDec",
(gogoproto.nullable) = false
];
}
12 changes: 12 additions & 0 deletions x/txfees/client/cli/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ func GetQueryCmd() *cobra.Command {
GetCmdFeeTokens(),
GetCmdDenomPoolID(),
GetCmdBaseDenom(),
GetCmdQueryBaseFee(),
)

return cmd
Expand Down Expand Up @@ -52,3 +53,14 @@ func GetCmdBaseDenom() *cobra.Command {
types.ModuleName, types.NewQueryClient,
)
}

func GetCmdQueryBaseFee() *cobra.Command {
return osmocli.SimpleQueryCmd[*types.QueryEipBaseFeeRequest](
"base-fee",
"Query the eip base fee",
`{{.Short}}{{.ExampleHeader}}
{{.CommandPrefix}} base-fee
`,
types.ModuleName, types.NewQueryClient,
)
}
17 changes: 17 additions & 0 deletions x/txfees/keeper/feedecorator.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"

"github.com/osmosis-labs/osmosis/osmomath"
mempool1559 "github.com/osmosis-labs/osmosis/v20/x/txfees/keeper/mempool-1559"
"github.com/osmosis-labs/osmosis/v20/x/txfees/keeper/txfee_filters"
"github.com/osmosis-labs/osmosis/v20/x/txfees/types"

Expand Down Expand Up @@ -57,6 +58,12 @@ func (mfd MempoolFeeDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate b
return ctx, types.ErrTooManyFeeCoins
}

// TODO: Is there a better way to do this?
// I want ctx.IsDeliverTx() but that doesn't exist.
if !ctx.IsCheckTx() && !ctx.IsReCheckTx() {
mempool1559.DeliverTxCode(ctx, feeTx)
}

baseDenom, err := mfd.TxFeesKeeper.GetBaseDenom(ctx)
if err != nil {
return ctx, err
Expand Down Expand Up @@ -137,6 +144,8 @@ func (k Keeper) IsSufficientFee(ctx sdk.Context, minBaseGasPrice osmomath.Dec, g
}

func (mfd MempoolFeeDecorator) GetMinBaseGasPriceForTx(ctx sdk.Context, baseDenom string, tx sdk.FeeTx) osmomath.Dec {
var is1559enabled = mfd.Opts.Mempool1559Enabled

cfgMinGasPrice := ctx.MinGasPrices().AmountOf(baseDenom)
// the check below prevents tx gas from getting over HighGasTxThreshold which is default to 1_000_000
if tx.GetGas() >= mfd.Opts.HighGasTxThreshold {
Expand All @@ -145,6 +154,14 @@ func (mfd MempoolFeeDecorator) GetMinBaseGasPriceForTx(ctx sdk.Context, baseDeno
if txfee_filters.IsArbTxLoose(tx) {
cfgMinGasPrice = sdk.MaxDec(cfgMinGasPrice, mfd.Opts.MinGasPriceForArbitrageTx)
}
// Initial tx only, no recheck
if is1559enabled && ctx.IsCheckTx() && !ctx.IsReCheckTx() {
cfgMinGasPrice = sdk.MaxDec(cfgMinGasPrice, mempool1559.CurEipState.GetCurBaseFee())
}
// RecheckTx only
if is1559enabled && ctx.IsReCheckTx() {
cfgMinGasPrice = sdk.MaxDec(cfgMinGasPrice, mempool1559.CurEipState.GetCurRecheckBaseFee())
}
return cfgMinGasPrice
}

Expand Down
7 changes: 7 additions & 0 deletions x/txfees/keeper/grpc_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"

mempool1559 "github.com/osmosis-labs/osmosis/v20/x/txfees/keeper/mempool-1559"
"github.com/osmosis-labs/osmosis/v20/x/txfees/types"
)

Expand All @@ -18,6 +19,7 @@ var _ types.QueryServer = Querier{}
// handlers.
type Querier struct {
Keeper
mempool1559.EipState
}

func NewQuerier(k Keeper) Querier {
Expand Down Expand Up @@ -83,3 +85,8 @@ func (q Querier) BaseDenom(ctx context.Context, _ *types.QueryBaseDenomRequest)

return &types.QueryBaseDenomResponse{BaseDenom: baseDenom}, nil
}

func (q Querier) GetEipBaseFee(_ context.Context, _ *types.QueryEipBaseFeeRequest) (*types.QueryEipBaseFeeResponse, error) {
response := mempool1559.CurEipState.GetCurBaseFee()
return &types.QueryEipBaseFeeResponse{BaseFee: response}, nil
}
1 change: 1 addition & 0 deletions x/txfees/keeper/mempool-1559/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
eip1559state.json
181 changes: 181 additions & 0 deletions x/txfees/keeper/mempool-1559/code.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package mempool1559

import (
"encoding/json"
"fmt"
"os"

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

osmomath "github.com/osmosis-labs/osmosis/osmomath"
)

/*
This is the logic for the Osmosis implementation for EIP-1559 fee market,
the goal of this code is to prevent spam by charging more for transactions when the network is busy.
This logic does two things:
- Maintaining data parsed from chain transaction execution and updating eipState accordingly.
- Resetting eipState to default every ResetInterval (1000) block height intervals to maintain consistency.
Additionally:
- Periodically evaluating CheckTx and RecheckTx for compliance with these parameters.
Note: The reset interval is set to 1000 blocks, which is approximately 2 hours. Consider adjusting for a smaller time interval (e.g., 500 blocks = 1 hour) if necessary.
Challenges:
- Transactions falling under their gas bounds are currently discarded by nodes. This behavior can be modified for CheckTx, rather than RecheckTx.
Global variables stored in memory:
- DefaultBaseFee: Default base fee, initialized to 0.0025.
- MinBaseFee: Minimum base fee, initialized to 0.0025.
- MaxBaseFee: Maximum base fee, initialized to 10.
- MaxBlockChangeRate: The maximum block change rate, initialized to 1/16.
Global constants:
- TargetGas: Gas wanted per block, initialized to 60,000,000.
- ResetInterval: The interval at which eipState is reset, initialized to 1000 blocks.
- BackupFile: File for backup, set to "eip1559state.json".
- RecheckFeeConstant: A constant value for rechecking fees, initialized to 4.
*/

var (
DefaultBaseFee = sdk.MustNewDecFromStr("0.0025")
MinBaseFee = sdk.MustNewDecFromStr("0.0025")
MaxBaseFee = sdk.MustNewDecFromStr("10")
MaxBlockChangeRate = sdk.NewDec(1).Quo(sdk.NewDec(16))
)

const (
TargetGas = int64(60_000_000)
ResetInterval = int64(1000)
BackupFile = "eip1559state.json"
RecheckFeeConstant = int64(4)
)

// EipState tracks the current base fee and totalGasWantedThisBlock
// this structure is never written to state
type EipState struct {
lastBlockHeight int64
totalGasWantedThisBlock int64

CurBaseFee osmomath.Dec `json:"cur_base_fee"`
}

// CurEipState is a global variable used in the BeginBlock, EndBlock and
// DeliverTx (fee decorator AnteHandler) functions, it's also using when determining
// if a transaction has enough gas to successfully execute
var CurEipState = EipState{
lastBlockHeight: 0,
totalGasWantedThisBlock: 0,
CurBaseFee: sdk.NewDec(0),
}

// startBlock is executed at the start of each block and is responsible for reseting the state
// of the CurBaseFee when the node reaches the reset interval
func (e *EipState) startBlock(height int64) {
e.lastBlockHeight = height
e.totalGasWantedThisBlock = 0

if e.CurBaseFee.Equal(sdk.NewDec(0)) {
// CurBaseFee has not been initialized yet. This only happens when the node has just started.
// Try to read the previous value from the backup file and if not available, set it to the default.
e.CurBaseFee = e.tryLoad()
}

// we reset the CurBaseFee every ResetInterval
if height%ResetInterval == 0 {
e.CurBaseFee = DefaultBaseFee.Clone()
}
}

// deliverTxCode runs on every transaction in the feedecorator ante handler and sums the gas of each transaction
func (e *EipState) deliverTxCode(ctx sdk.Context, tx sdk.FeeTx) {
if ctx.BlockHeight() != e.lastBlockHeight {
ctx.Logger().Error("Something is off here? ctx.BlockHeight() != e.lastBlockHeight", ctx.BlockHeight(), e.lastBlockHeight)
}
e.totalGasWantedThisBlock += int64(tx.GetGas())
}

// updateBaseFee updates of a base fee in Osmosis.
// It employs the following equation to calculate the new base fee:
//
// baseFeeMultiplier = 1 + (gasUsed - targetGas) / targetGas * maxChangeRate
// newBaseFee = baseFee * baseFeeMultiplier
//
// updateBaseFee runs at the end of every block
func (e *EipState) updateBaseFee(height int64) {
if height != e.lastBlockHeight {
fmt.Println("Something is off here? height != e.lastBlockHeight", height, e.lastBlockHeight)
}
e.lastBlockHeight = height

gasUsed := e.totalGasWantedThisBlock
gasDiff := gasUsed - TargetGas
// (gasUsed - targetGas) / targetGas * maxChangeRate
baseFeeIncrement := sdk.NewDec(gasDiff).Quo(sdk.NewDec(TargetGas)).Mul(MaxBlockChangeRate)
baseFeeMultiplier := sdk.NewDec(1).Add(baseFeeIncrement)
e.CurBaseFee.MulMut(baseFeeMultiplier)

// Enforce the minimum base fee by resetting the CurBaseFee is it drops below the MinBaseFee
if e.CurBaseFee.LT(MinBaseFee) {
e.CurBaseFee = MinBaseFee.Clone()
}

// Enforce the maximum base fee by resetting the CurBaseFee is it goes above the MaxBaseFee
if e.CurBaseFee.GT(MaxBaseFee) {
e.CurBaseFee = MaxBaseFee.Clone()
}

go e.tryPersist()
}

// GetCurBaseFee returns a clone of the CurBaseFee to avoid overwriting the initial value in
// the EipState, we use this in the AnteHandler to Check transactions
func (e *EipState) GetCurBaseFee() osmomath.Dec {
return e.CurBaseFee.Clone()
}

// GetCurRecheckBaseFee returns a clone of the CurBaseFee / RecheckFeeConstant to account for
// rechecked transactions in the feedecorator ante handler
func (e *EipState) GetCurRecheckBaseFee() osmomath.Dec {
return e.CurBaseFee.Clone().Quo(sdk.NewDec(RecheckFeeConstant))
}

// tryPersist persists the eip1559 state to disk in the form of a json file
// we do this in case a node stops and it can continue functioning as normal
func (e *EipState) tryPersist() {
bz, err := json.Marshal(e)
if err != nil {
fmt.Println("Error marshalling eip1559 state", err)
return
}

err = os.WriteFile(BackupFile, bz, 0644)
if err != nil {
fmt.Println("Error writing eip1559 state", err)
return
}
}

// tryLoad reads eip1559 state from disk and initializes the CurEipState to
// the previous state when a node is restarted
func (e *EipState) tryLoad() osmomath.Dec {
bz, err := os.ReadFile(BackupFile)
if err != nil {
fmt.Println("Error reading eip1559 state", err)
fmt.Println("Setting eip1559 state to default value", MinBaseFee)
return MinBaseFee.Clone()
}

var loaded EipState
err = json.Unmarshal(bz, &loaded)
if err != nil {
fmt.Println("Error unmarshalling eip1559 state", err)
fmt.Println("Setting eip1559 state to default value", MinBaseFee)
return MinBaseFee.Clone()
}

fmt.Println("Loaded eip1559 state. CurBaseFee=", loaded.CurBaseFee)
return loaded.CurBaseFee.Clone()
}
84 changes: 84 additions & 0 deletions x/txfees/keeper/mempool-1559/code_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package mempool1559

import (
"testing"

sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/tendermint/tendermint/libs/log"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
"gotest.tools/assert"

"github.com/osmosis-labs/osmosis/osmoutils/noapptest"
)

// TestUpdateBaseFee simulates the update of a base fee in Osmosis.
// It employs the following equation to calculate the new base fee:
//
// baseFeeMultiplier = 1 + (gasUsed - targetGas) / targetGas * maxChangeRate
// newBaseFee = baseFee * baseFeeMultiplier
//
// The function iterates through a series of simulated blocks and transactions,
// updating and validating the base fee at each step to ensure it follows the equation.
func TestUpdateBaseFee(t *testing.T) {
// Create an instance of eipState
eip := &EipState{
lastBlockHeight: 0,
totalGasWantedThisBlock: 0,
CurBaseFee: DefaultBaseFee.Clone(),
}

// we iterate over 1000 blocks as the reset happens after 1000 blocks
for i := 1; i <= 1002; i++ {
// create a new block
ctx := sdk.NewContext(nil, tmproto.Header{Height: int64(i)}, false, log.NewNopLogger())

// start the new block
eip.startBlock(int64(i))

// generate transactions
if i%10 == 0 {
for j := 1; j <= 3; j++ {
tx := GenTx(uint64(500000000 + i))
eip.deliverTxCode(ctx, tx.(sdk.FeeTx))
}
}
baseFeeBeforeUpdate := eip.GetCurBaseFee()

// update base fee
eip.updateBaseFee(int64(i))

// calcualte the base fees
expectedBaseFee := calculateBaseFee(eip.totalGasWantedThisBlock, baseFeeBeforeUpdate)

// Assert that the actual result matches the expected result
assert.DeepEqual(t, expectedBaseFee, eip.CurBaseFee)
}
}

// calculateBaseFee is the same as in is defined on the eip1559 code
func calculateBaseFee(totalGasWantedThisBlock int64, eipStateCurBaseFee sdk.Dec) (expectedBaseFee sdk.Dec) {
gasUsed := totalGasWantedThisBlock
gasDiff := gasUsed - TargetGas

baseFeeIncrement := sdk.NewDec(gasDiff).Quo(sdk.NewDec(TargetGas)).Mul(MaxBlockChangeRate)
expectedBaseFeeMultiplier := sdk.NewDec(1).Add(baseFeeIncrement)
expectedBaseFee = eipStateCurBaseFee.MulMut(expectedBaseFeeMultiplier)

if expectedBaseFee.LT(MinBaseFee) {
expectedBaseFee = MinBaseFee
}

if expectedBaseFee.GT(MaxBaseFee) {
expectedBaseFee = MaxBaseFee.Clone()
}

return expectedBaseFee
}

// GenTx generates a mock gas transaction.
func GenTx(gas uint64) sdk.Tx {
gen := noapptest.MakeTestEncodingConfig().TxConfig
txBuilder := gen.NewTxBuilder()
txBuilder.SetGasLimit(gas)
return txBuilder.GetTx()
}
Loading

0 comments on commit a8af889

Please sign in to comment.