From 896456c9d06a3dfb77680a15999fd29dad4b8a1f Mon Sep 17 00:00:00 2001 From: cby3149 Date: Thu, 14 Mar 2024 14:17:26 -0700 Subject: [PATCH] Add minimum suggested priority gas price --- cmd/utils/flags.go | 16 +++ eth/ethconfig/config.go | 15 +-- eth/gasprice/gasprice.go | 24 +++- eth/gasprice/gaspricecfg/gaspricecfg.go | 4 + eth/gasprice/optimism-gasprice.go | 112 +++++++++++++++++ eth/gasprice/optimism-gasprice_test.go | 157 ++++++++++++++++++++++++ turbo/cli/default_flags.go | 2 + 7 files changed, 322 insertions(+), 8 deletions(-) create mode 100644 eth/gasprice/optimism-gasprice.go create mode 100644 eth/gasprice/optimism-gasprice_test.go diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index f2c92488acf..32f41c968da 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -641,6 +641,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{ @@ -1369,6 +1379,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 diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go index c10abf71152..dfb8eeda415 100644 --- a/eth/ethconfig/config.go +++ b/eth/ethconfig/config.go @@ -50,13 +50,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. diff --git a/eth/gasprice/gasprice.go b/eth/gasprice/gasprice.go index 81c47fc161d..ce700fe1600 100644 --- a/eth/gasprice/gasprice.go +++ b/eth/gasprice/gasprice.go @@ -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 @@ -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, @@ -102,6 +104,16 @@ 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 @@ -129,6 +141,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 { @@ -280,3 +296,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] } diff --git a/eth/gasprice/gaspricecfg/gaspricecfg.go b/eth/gasprice/gaspricecfg/gaspricecfg.go index af364b0a220..0242e915585 100644 --- a/eth/gasprice/gaspricecfg/gaspricecfg.go +++ b/eth/gasprice/gaspricecfg/gaspricecfg.go @@ -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 { @@ -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 } diff --git a/eth/gasprice/optimism-gasprice.go b/eth/gasprice/optimism-gasprice.go new file mode 100644 index 00000000000..0bcbb9e48c9 --- /dev/null +++ b/eth/gasprice/optimism-gasprice.go @@ -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) +} diff --git a/eth/gasprice/optimism-gasprice_test.go b/eth/gasprice/optimism-gasprice_test.go new file mode 100644 index 00000000000..fac5a1b3210 --- /dev/null +++ b/eth/gasprice/optimism-gasprice_test.go @@ -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 . + +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) + } + } +} diff --git a/turbo/cli/default_flags.go b/turbo/cli/default_flags.go index 221b007254d..fd3395a9891 100644 --- a/turbo/cli/default_flags.go +++ b/turbo/cli/default_flags.go @@ -122,6 +122,8 @@ var DefaultFlags = []cli.Flag{ &utils.FakePoWFlag, &utils.GpoBlocksFlag, &utils.GpoPercentileFlag, + &utils.GpoIgnoreGasPriceFlag, + &utils.GpoMinSuggestedPriorityFeeFlag, &utils.InsecureUnlockAllowedFlag, &utils.HistoryV3Flag, &utils.IdentityFlag,