Skip to content

Commit

Permalink
Merge pull request #88 from bobanetwork/add-minimum-gas-price
Browse files Browse the repository at this point in the history
Add minimum gas price
  • Loading branch information
boyuan-chen authored Mar 12, 2024
2 parents 604bba1 + 56f1d99 commit 8da622a
Show file tree
Hide file tree
Showing 7 changed files with 323 additions and 8 deletions.
16 changes: 16 additions & 0 deletions cmd/utils/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,16 @@ var (
Usage: "Maximum gas price will be recommended by gpo",
Value: ethconfig.Defaults.GPO.MaxPrice.Int64(),
}
GpoIgnoreGasPriceFlag = cli.Int64Flag{
Name: "gpo.ignoreprice",
Usage: "Gas price below which gpo will ignore transactions",
Value: ethconfig.Defaults.GPO.IgnorePrice.Int64(),
}
GpoMinSuggestedPriorityFeeFlag = cli.Int64Flag{
Name: "gpo.minsuggestedpriorityfee",
Usage: "Minimum transaction priority fee to suggest. Used on OP chains when blocks are not full.",
Value: ethconfig.Defaults.GPO.MinSuggestedPriorityFee.Int64(),
}

// Rollup Flags
RollupSequencerHTTPFlag = cli.StringFlag{
Expand Down Expand Up @@ -1357,6 +1367,12 @@ func setGPO(ctx *cli.Context, cfg *gaspricecfg.Config) {
if ctx.IsSet(GpoMaxGasPriceFlag.Name) {
cfg.MaxPrice = big.NewInt(ctx.Int64(GpoMaxGasPriceFlag.Name))
}
if ctx.IsSet(GpoIgnoreGasPriceFlag.Name) {
cfg.IgnorePrice = big.NewInt(ctx.Int64(GpoIgnoreGasPriceFlag.Name))
}
if ctx.IsSet(GpoMinSuggestedPriorityFeeFlag.Name) {
cfg.MinSuggestedPriorityFee = big.NewInt(ctx.Int64(GpoMinSuggestedPriorityFeeFlag.Name))
}
}

// nolint
Expand Down
15 changes: 8 additions & 7 deletions eth/ethconfig/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,14 @@ const HistoryV3AggregationStep = 3_125_000 // 100M / 32

// FullNodeGPO contains default gasprice oracle settings for full node.
var FullNodeGPO = gaspricecfg.Config{
Blocks: 20,
Default: big.NewInt(0),
Percentile: 60,
MaxHeaderHistory: 0,
MaxBlockHistory: 0,
MaxPrice: gaspricecfg.DefaultMaxPrice,
IgnorePrice: gaspricecfg.DefaultIgnorePrice,
Blocks: 20,
Default: big.NewInt(0),
Percentile: 60,
MaxHeaderHistory: 0,
MaxBlockHistory: 0,
MaxPrice: gaspricecfg.DefaultMaxPrice,
IgnorePrice: gaspricecfg.DefaultIgnorePrice,
MinSuggestedPriorityFee: gaspricecfg.DefaultMinSuggestedPriorityFee,
}

// LightClientGPO contains default gasprice oracle settings for light client.
Expand Down
25 changes: 24 additions & 1 deletion eth/gasprice/gasprice.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ type Oracle struct {
checkBlocks int
percentile int
maxHeaderHistory, maxBlockHistory int

minSuggestedPriorityFee *big.Int // for Optimism fee suggestion
}

// NewOracle returns a new gasprice oracle which can recommend suitable
Expand Down Expand Up @@ -91,7 +93,7 @@ func NewOracle(backend OracleBackend, params gaspricecfg.Config, cache Cache) *O
ignorePrice = gaspricecfg.DefaultIgnorePrice
log.Warn("Sanitizing invalid gasprice oracle ignore price", "provided", params.IgnorePrice, "updated", ignorePrice)
}
return &Oracle{
r := &Oracle{
backend: backend,
lastPrice: params.Default,
maxPrice: maxPrice,
Expand All @@ -102,6 +104,17 @@ func NewOracle(backend OracleBackend, params gaspricecfg.Config, cache Cache) *O
maxHeaderHistory: params.MaxHeaderHistory,
maxBlockHistory: params.MaxBlockHistory,
}

if backend.ChainConfig().IsOptimism() {
r.minSuggestedPriorityFee = params.MinSuggestedPriorityFee
if r.minSuggestedPriorityFee == nil || r.minSuggestedPriorityFee.Int64() <= 0 {
r.minSuggestedPriorityFee = gaspricecfg.DefaultMinSuggestedPriorityFee
log.Warn("Sanitizing invalid optimism gasprice oracle min priority fee suggestion",
"provided", params.MinSuggestedPriorityFee,
"updated", r.minSuggestedPriorityFee)
}
}
return r
}

// SuggestTipCap returns a TipCap so that newly created transaction can
Expand Down Expand Up @@ -129,6 +142,10 @@ func (oracle *Oracle) SuggestTipCap(ctx context.Context) (*big.Int, error) {
return latestPrice, nil
}

if oracle.backend.ChainConfig().IsOptimism() {
return oracle.SuggestOptimismPriorityFee(ctx, head, headHash), nil
}

number := head.Number.Uint64()
txPrices := make(sortingHeap, 0, sampleNumber*oracle.checkBlocks)
for txPrices.Len() < sampleNumber*oracle.checkBlocks && number > 0 {
Expand Down Expand Up @@ -280,3 +297,9 @@ func (s *sortingHeap) Pop() interface{} {
*s = old[0 : n-1]
return x
}

type bigIntArray []*big.Int

func (s bigIntArray) Len() int { return len(s) }
func (s bigIntArray) Less(i, j int) bool { return s[i].Cmp(s[j]) < 0 }
func (s bigIntArray) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
4 changes: 4 additions & 0 deletions eth/gasprice/gaspricecfg/gaspricecfg.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ var DefaultIgnorePrice = big.NewInt(2 * params.Wei)

var (
DefaultMaxPrice = big.NewInt(500 * params.GWei)

DefaultMinSuggestedPriorityFee = big.NewInt(1e6 * params.Wei) // 0.001 gwei, for Optimism fee suggestion
)

type Config struct {
Expand All @@ -20,4 +22,6 @@ type Config struct {
Default *big.Int `toml:",omitempty"`
MaxPrice *big.Int `toml:",omitempty"`
IgnorePrice *big.Int `toml:",omitempty"`

MinSuggestedPriorityFee *big.Int `toml:",omitempty"` // for Optimism fee suggestion
}
112 changes: 112 additions & 0 deletions eth/gasprice/optimism-gasprice.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package gasprice

import (
"context"
"math/big"
"sort"

"github.com/holiman/uint256"
"github.com/ledgerwatch/erigon-lib/common"
"github.com/ledgerwatch/erigon/core/types"
"github.com/ledgerwatch/erigon/rpc"
"github.com/ledgerwatch/log/v3"
)

// SuggestOptimismPriorityFee returns a max priority fee value that can be used such that newly
// created transactions have a very high chance to be included in the following blocks, using a
// simplified and more predictable algorithm appropriate for chains like Optimism with a single
// known block builder.
//
// In the typical case, which results whenever the last block had room for more transactions, this
// function returns a minimum suggested priority fee value. Otherwise it returns the higher of this
// minimum suggestion or 10% over the median effective priority fee from the last block.
//
// Rationale: For a chain such as Optimism where there is a single block builder whose behavior is
// known, we know priority fee (as long as it is non-zero) has no impact on the probability for tx
// inclusion as long as there is capacity for it in the block. In this case then, there's no reason
// to return any value higher than some fixed minimum. Blocks typically reach capacity only under
// extreme events such as airdrops, meaning predicting whether the next block is going to be at
// capacity is difficult *except* in the case where we're already experiencing the increased demand
// from such an event. We therefore expect whether the last known block is at capacity to be one of
// the best predictors of whether the next block is likely to be at capacity. (An even better
// predictor is to look at the state of the transaction pool, but we want an algorithm that works
// even if the txpool is private or unavailable.)
//
// In the event the next block may be at capacity, the algorithm should allow for average fees to
// rise in order to reach a market price that appropriately reflects demand. We accomplish this by
// returning a suggestion that is a significant amount (10%) higher than the median effective
// priority fee from the previous block.
func (oracle *Oracle) SuggestOptimismPriorityFee(ctx context.Context, h *types.Header, headHash common.Hash) *big.Int {
suggestion := new(big.Int).Set(oracle.minSuggestedPriorityFee)

// find the maximum gas used by any of the transactions in the block to use as the capacity
// margin
block, err := oracle.backend.BlockByNumber(ctx, rpc.BlockNumber(h.Number.Int64()))
if err != nil {
log.Error("failed to get block", "err", err)
return suggestion
}
receipts, err := oracle.backend.GetReceipts(ctx, block)
if receipts == nil || err != nil {
log.Error("failed to get block receipts", "err", err)
return suggestion
}
var maxTxGasUsed uint64
for i := range receipts {
gu := receipts[i].GasUsed
if gu > maxTxGasUsed {
maxTxGasUsed = gu
}
}
// sanity check the max gas used value
if maxTxGasUsed > h.GasLimit {
log.Error("found tx consuming more gas than the block limit", "gas", maxTxGasUsed)
return suggestion
}

if h.GasUsed+maxTxGasUsed > h.GasLimit {
// A block is "at capacity" if, when it is built, there is a pending tx in the txpool that
// could not be included because the block's gas limit would be exceeded. Since we don't
// have access to the txpool, we instead adopt the following heuristic: consider a block as
// at capacity if the total gas consumed by its transactions is within max-tx-gas-used of
// the block limit, where max-tx-gas-used is the most gas used by any one transaction
// within the block. This heuristic is almost perfectly accurate when transactions always
// consume the same amount of gas, but becomes less accurate as tx gas consumption begins
// to vary. The typical error is we assume a block is at capacity when it was not because
// max-tx-gas-used will in most cases over-estimate the "capacity margin". But it's better
// to err on the side of returning a higher-than-needed suggestion than a lower-than-needed
// one in order to satisfy our desire for high chance of inclusion and rising fees under
// high demand.
block, err := oracle.backend.BlockByNumber(ctx, rpc.BlockNumber(h.Number.Int64()))
if block == nil || err != nil {
log.Error("failed to get last block", "err", err)
return suggestion
}
baseFee := block.BaseFee()
txs := block.Transactions()
if len(txs) == 0 {
log.Error("block was at capacity but doesn't have transactions")
return suggestion
}
tips := bigIntArray(make([]*big.Int, len(txs)))
for i := range txs {
tips[i] = txs[i].GetEffectiveGasTip(uint256.MustFromBig(baseFee)).ToBig()
}
sort.Sort(tips)
median := tips[len(tips)/2]
newSuggestion := new(big.Int).Add(median, new(big.Int).Div(median, big.NewInt(10)))
// use the new suggestion only if it's bigger than the minimum
if newSuggestion.Cmp(suggestion) > 0 {
suggestion = newSuggestion
}
}

// the suggestion should be capped by oracle.maxPrice
if suggestion.Cmp(oracle.maxPrice) > 0 {
suggestion.Set(oracle.maxPrice)
}

oracle.cache.SetLatest(headHash, suggestion)

return new(big.Int).Set(suggestion)
}
157 changes: 157 additions & 0 deletions eth/gasprice/optimism-gasprice_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
// Copyright 2020 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.

package gasprice

import (
"context"
"math/big"
"testing"

"github.com/holiman/uint256"
"github.com/ledgerwatch/erigon-lib/chain"
libcommon "github.com/ledgerwatch/erigon-lib/common"
"github.com/ledgerwatch/erigon/core"
"github.com/ledgerwatch/erigon/core/types"
"github.com/ledgerwatch/erigon/crypto"
"github.com/ledgerwatch/erigon/eth/gasprice/gaspricecfg"
"github.com/ledgerwatch/erigon/event"
"github.com/ledgerwatch/erigon/params"
"github.com/ledgerwatch/erigon/rpc"
)

const (
blockGasLimit = params.TxGas * 3
)

type testTxData struct {
priorityFee int64
gasLimit uint64
}

type testCache struct {
latestHash libcommon.Hash
latestPrice *big.Int
}

// GetLatest implements Cache.
func (c *testCache) GetLatest() (libcommon.Hash, *big.Int) {
return c.latestHash, c.latestPrice
}

// SetLatest implements Cache.
func (c *testCache) SetLatest(hash libcommon.Hash, price *big.Int) {
c.latestHash = hash
c.latestPrice = price
}

type opTestBackend struct {
block *types.Block
receipts []*types.Receipt
}

func (b *opTestBackend) HeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Header, error) {
panic("not implemented")
}

func (b *opTestBackend) BlockByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Block, error) {
return b.block, nil
}

func (b *opTestBackend) GetReceipts(ctx context.Context, block *types.Block) (types.Receipts, error) {
return b.receipts, nil
}

func (b *opTestBackend) PendingBlockAndReceipts() (*types.Block, types.Receipts) {
panic("not implemented")
}

func (b *opTestBackend) ChainConfig() *chain.Config {
return params.OptimismTestConfig
}

func (b *opTestBackend) SubscribeChainHeadEvent(ch chan<- core.ChainHeadEvent) event.Subscription {
return nil
}

func newOpTestBackend(t *testing.T, txs []testTxData) *opTestBackend {
var (
key, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
signer = types.LatestSigner(params.TestChainConfig)
)
// only the most recent block is considered for optimism priority fee suggestions, so this is
// where we add the test transactions
ts := []types.Transaction{}
rs := []*types.Receipt{}
header := types.Header{}
header.GasLimit = blockGasLimit
var nonce uint64
for _, tx := range txs {
txdata := &types.DynamicFeeTransaction{
ChainID: uint256.MustFromBig(params.TestChainConfig.ChainID),
FeeCap: uint256.MustFromBig(big.NewInt(100 * params.GWei)),
Tip: uint256.MustFromBig(big.NewInt(tx.priorityFee)),
CommonTx: types.CommonTx{},
}
t := types.MustSignNewTx(key, *signer, txdata)
ts = append(ts, t)
r := types.Receipt{}
r.GasUsed = tx.gasLimit
header.GasUsed += r.GasUsed
rs = append(rs, &r)
nonce++
}
// hasher := trie.NewStackTrie(nil)
b := types.NewBlock(&header, ts, nil, nil, nil)
return &opTestBackend{block: b, receipts: rs}
}

func TestSuggestOptimismPriorityFee(t *testing.T) {
minSuggestion := new(big.Int).SetUint64(1e8 * params.Wei)
var cases = []struct {
txdata []testTxData
want *big.Int
}{
{
// block well under capacity, expect min priority fee suggestion
txdata: []testTxData{{params.GWei, 21000}},
want: minSuggestion,
},
{
// 2 txs, still under capacity, expect min priority fee suggestion
txdata: []testTxData{{params.GWei, 21000}, {params.GWei, 21000}},
want: minSuggestion,
},
{
// 2 txs w same priority fee (1 gwei), but second tx puts it right over capacity
txdata: []testTxData{{params.GWei, 21000}, {params.GWei, 21001}},
want: big.NewInt(1100000000), // 10 percent over 1 gwei, the median
},
{
// 3 txs, full block. return 10% over the median tx (10 gwei * 10% == 11 gwei)
txdata: []testTxData{{10 * params.GWei, 21000}, {1 * params.GWei, 21000}, {100 * params.GWei, 21000}},
want: big.NewInt(11 * params.GWei),
},
}
for i, c := range cases {
backend := newOpTestBackend(t, c.txdata)
oracle := NewOracle(backend, gaspricecfg.Config{MinSuggestedPriorityFee: minSuggestion}, &testCache{})
got := oracle.SuggestOptimismPriorityFee(context.Background(), backend.block.Header(), backend.block.Hash())
if got.Cmp(c.want) != 0 {
t.Errorf("Gas price mismatch for test case %d: want %d, got %d", i, c.want, got)
}
}
}
Loading

0 comments on commit 8da622a

Please sign in to comment.