From b9d3c72e0d0e1f8892bacca726a4fc1fdbc7db6c Mon Sep 17 00:00:00 2001 From: Jonas Theis <4181434+jonastheis@users.noreply.github.com> Date: Tue, 4 Jun 2024 17:05:58 +0800 Subject: [PATCH] feat: congestion-aware gas price oracle (#790) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Implement functionality to reset gas price / suggested tip to minimal value when there's no congestion * Add flags to configure congestion value and initialize gas price oracle accordingly * Fix and add tests to make sure GPO works as expected depending on pre- or post-Curie (EIP 1559) upgrade * Apply review suggestions * chore: auto version bump [bot] --------- Co-authored-by: omerfirmak --- cmd/geth/main.go | 2 + cmd/geth/usage.go | 1 + cmd/utils/flags.go | 8 ++++ eth/backend.go | 1 + eth/gasprice/feehistory_test.go | 2 +- eth/gasprice/gasprice.go | 72 ++++++++++++++++++++++++------- eth/gasprice/gasprice_test.go | 76 ++++++++++++++++++++++++++++++--- params/version.go | 2 +- 8 files changed, 141 insertions(+), 23 deletions(-) diff --git a/cmd/geth/main.go b/cmd/geth/main.go index ac0c8df1fe32..e9c9bf68906f 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -159,6 +159,8 @@ var ( utils.GpoPercentileFlag, utils.GpoMaxGasPriceFlag, utils.GpoIgnoreGasPriceFlag, + utils.GpoCongestionThresholdFlag, + utils.MinerNotifyFullFlag, configFileFlag, utils.CatalystFlag, diff --git a/cmd/geth/usage.go b/cmd/geth/usage.go index 8c2e17218efd..1fa00a384e2b 100644 --- a/cmd/geth/usage.go +++ b/cmd/geth/usage.go @@ -204,6 +204,7 @@ var AppHelpFlagGroups = []flags.FlagGroup{ utils.GpoPercentileFlag, utils.GpoMaxGasPriceFlag, utils.GpoIgnoreGasPriceFlag, + utils.GpoCongestionThresholdFlag, }, }, { diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 4d2224d6522a..7ad9915815fb 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -735,6 +735,11 @@ var ( Usage: "Gas price below which gpo will ignore transactions", Value: ethconfig.Defaults.GPO.IgnorePrice.Int64(), } + GpoCongestionThresholdFlag = cli.IntFlag{ + Name: "gpo.congestionthreshold", + Usage: "Number of pending transactions to consider the network congested and suggest a minimum tip cap", + Value: ethconfig.Defaults.GPO.CongestedThreshold, + } // Metrics flags MetricsEnabledFlag = cli.BoolFlag{ @@ -1429,6 +1434,9 @@ func setGPO(ctx *cli.Context, cfg *gasprice.Config, light bool) { if ctx.GlobalIsSet(GpoIgnoreGasPriceFlag.Name) { cfg.IgnorePrice = big.NewInt(ctx.GlobalInt64(GpoIgnoreGasPriceFlag.Name)) } + if ctx.GlobalIsSet(GpoCongestionThresholdFlag.Name) { + cfg.CongestedThreshold = ctx.GlobalInt(GpoCongestionThresholdFlag.Name) + } } func setTxPool(ctx *cli.Context, cfg *core.TxPoolConfig) { diff --git a/eth/backend.go b/eth/backend.go index 91bbc47e4ae8..497ec9ee2d95 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -264,6 +264,7 @@ func New(stack *node.Node, config *ethconfig.Config, l1Client sync_service.EthCl if gpoParams.Default == nil { gpoParams.Default = config.Miner.GasPrice } + gpoParams.DefaultBasePrice = new(big.Int).SetUint64(config.TxPool.PriceLimit) eth.APIBackend.gpo = gasprice.NewOracle(eth.APIBackend, gpoParams) // Setup DNS discovery iterators. diff --git a/eth/gasprice/feehistory_test.go b/eth/gasprice/feehistory_test.go index 14354a3d5c2a..c5cb5af2d339 100644 --- a/eth/gasprice/feehistory_test.go +++ b/eth/gasprice/feehistory_test.go @@ -56,7 +56,7 @@ func TestFeeHistory(t *testing.T) { MaxHeaderHistory: c.maxHeader, MaxBlockHistory: c.maxBlock, } - backend := newTestBackend(t, big.NewInt(16), c.pending) + backend := newTestBackend(t, big.NewInt(16), c.pending, 0) oracle := NewOracle(backend, config) first, reward, baseFee, ratio, err := oracle.FeeHistory(context.Background(), c.count, c.last, c.percent) diff --git a/eth/gasprice/gasprice.go b/eth/gasprice/gasprice.go index 96ee73901d87..29ffcc60512b 100644 --- a/eth/gasprice/gasprice.go +++ b/eth/gasprice/gasprice.go @@ -39,16 +39,19 @@ const sampleNumber = 3 // Number of transactions sampled in a block var ( DefaultMaxPrice = big.NewInt(500 * params.GWei) DefaultIgnorePrice = big.NewInt(2 * params.Wei) + DefaultBasePrice = big.NewInt(0) ) type Config struct { - Blocks int - Percentile int - MaxHeaderHistory int - MaxBlockHistory int - Default *big.Int `toml:",omitempty"` - MaxPrice *big.Int `toml:",omitempty"` - IgnorePrice *big.Int `toml:",omitempty"` + Blocks int + Percentile int + MaxHeaderHistory int + MaxBlockHistory int + Default *big.Int `toml:",omitempty"` + MaxPrice *big.Int `toml:",omitempty"` + IgnorePrice *big.Int `toml:",omitempty"` + CongestedThreshold int // Number of pending transactions to consider the network congested and suggest a minimum tip cap. + DefaultBasePrice *big.Int `toml:",omitempty"` // Base price to set when CongestedThreshold is reached before Curie (EIP 1559). } // OracleBackend includes all necessary background APIs for oracle. @@ -60,6 +63,7 @@ type OracleBackend interface { ChainConfig() *params.ChainConfig SubscribeChainHeadEvent(ch chan<- core.ChainHeadEvent) event.Subscription StateAt(root common.Hash) (*state.StateDB, error) + Stats() (pending int, queued int) } // Oracle recommends gas prices based on the content of recent @@ -75,6 +79,8 @@ type Oracle struct { checkBlocks, percentile int maxHeaderHistory, maxBlockHistory int + congestedThreshold int // Number of pending transactions to consider the network congested and suggest a minimum tip cap. + defaultBasePrice *big.Int // Base price to set when CongestedThreshold is reached before Curie (EIP 1559). historyCache *lru.Cache } @@ -116,6 +122,16 @@ func NewOracle(backend OracleBackend, params Config) *Oracle { maxBlockHistory = 1 log.Warn("Sanitizing invalid gasprice oracle max block history", "provided", params.MaxBlockHistory, "updated", maxBlockHistory) } + congestedThreshold := params.CongestedThreshold + if congestedThreshold < 0 { + congestedThreshold = 0 + log.Warn("Sanitizing invalid gasprice oracle congested threshold", "provided", params.CongestedThreshold, "updated", congestedThreshold) + } + defaultBasePrice := params.DefaultBasePrice + if defaultBasePrice == nil || defaultBasePrice.Int64() < 0 { + defaultBasePrice = DefaultBasePrice + log.Warn("Sanitizing invalid gasprice oracle default base price", "provided", params.DefaultBasePrice, "updated", defaultBasePrice) + } cache, _ := lru.New(2048) headEvent := make(chan core.ChainHeadEvent, 1) @@ -131,15 +147,17 @@ func NewOracle(backend OracleBackend, params Config) *Oracle { }() return &Oracle{ - backend: backend, - lastPrice: params.Default, - maxPrice: maxPrice, - ignorePrice: ignorePrice, - checkBlocks: blocks, - percentile: percent, - maxHeaderHistory: maxHeaderHistory, - maxBlockHistory: maxBlockHistory, - historyCache: cache, + backend: backend, + lastPrice: params.Default, + maxPrice: maxPrice, + ignorePrice: ignorePrice, + checkBlocks: blocks, + percentile: percent, + maxHeaderHistory: maxHeaderHistory, + maxBlockHistory: maxBlockHistory, + congestedThreshold: congestedThreshold, + defaultBasePrice: defaultBasePrice, + historyCache: cache, } } @@ -170,6 +188,28 @@ func (oracle *Oracle) SuggestTipCap(ctx context.Context) (*big.Int, error) { if headHash == lastHead { return new(big.Int).Set(lastPrice), nil } + + // If pending txs are less than oracle.congestedThreshold, we consider the network to be non-congested and suggest + // a minimal tip cap. This is to prevent users from overpaying for gas when the network is not congested and a few + // high-priced txs are causing the suggested tip cap to be high. + pendingTxCount, _ := oracle.backend.Stats() + if pendingTxCount < oracle.congestedThreshold { + // Before Curie (EIP-1559), we need to return the total suggested gas price. After Curie we return 1 wei as the tip cap, + // as the base fee is set separately or added manually for legacy transactions. + // Set price to 1 as otherwise tx with a 0 tip might be filtered out by the default mempool config. + price := big.NewInt(1) + if !oracle.backend.ChainConfig().IsCurie(head.Number) { + price = oracle.defaultBasePrice + } + + oracle.cacheLock.Lock() + oracle.lastHead = headHash + oracle.lastPrice = price + oracle.cacheLock.Unlock() + + return new(big.Int).Set(price), nil + } + var ( sent, exp int number = head.Number.Uint64() diff --git a/eth/gasprice/gasprice_test.go b/eth/gasprice/gasprice_test.go index 24822c4c2a96..b27046546cfe 100644 --- a/eth/gasprice/gasprice_test.go +++ b/eth/gasprice/gasprice_test.go @@ -38,8 +38,9 @@ import ( const testHead = 32 type testBackend struct { - chain *core.BlockChain - pending bool // pending block available + chain *core.BlockChain + pending bool // pending block available + pendingTxCount int } func (b *testBackend) HeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Header, error) { @@ -96,7 +97,11 @@ func (b *testBackend) SubscribeChainHeadEvent(ch chan<- core.ChainHeadEvent) eve return nil } -func newTestBackend(t *testing.T, londonBlock *big.Int, pending bool) *testBackend { +func (b *testBackend) Stats() (int, int) { + return b.pendingTxCount, 0 +} + +func newTestBackend(t *testing.T, londonBlock *big.Int, pending bool, pendingTxCount int) *testBackend { var ( key, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") addr = crypto.PubkeyToAddress(key.PublicKey) @@ -113,6 +118,7 @@ func newTestBackend(t *testing.T, londonBlock *big.Int, pending bool) *testBacke config.ShanghaiBlock = londonBlock config.BernoulliBlock = londonBlock config.CurieBlock = londonBlock + config.DescartesBlock = londonBlock engine := ethash.NewFaker() db := rawdb.NewMemoryDatabase() genesis, err := gspec.Commit(db) @@ -154,7 +160,7 @@ func newTestBackend(t *testing.T, londonBlock *big.Int, pending bool) *testBacke t.Fatalf("Failed to create local chain, %v", err) } chain.InsertChain(blocks) - return &testBackend{chain: chain, pending: pending} + return &testBackend{chain: chain, pending: pending, pendingTxCount: pendingTxCount} } func (b *testBackend) CurrentHeader() *types.Header { @@ -186,7 +192,67 @@ func TestSuggestTipCap(t *testing.T) { {big.NewInt(33), big.NewInt(params.GWei * int64(30))}, // Fork point in the future } for _, c := range cases { - backend := newTestBackend(t, c.fork, false) + backend := newTestBackend(t, c.fork, false, 0) + oracle := NewOracle(backend, config) + + // The gas price sampled is: 32G, 31G, 30G, 29G, 28G, 27G + got, err := oracle.SuggestTipCap(context.Background()) + if err != nil { + t.Fatalf("Failed to retrieve recommended gas price: %v", err) + } + if got.Cmp(c.expect) != 0 { + t.Fatalf("Gas price mismatch, want %d, got %d", c.expect, got) + } + } +} + +func TestSuggestTipCapCongestedThreshold(t *testing.T) { + expectedDefaultBasePricePreCurie := big.NewInt(2000) + expectedDefaultBasePricePostCurie := big.NewInt(1) + + config := Config{ + Blocks: 3, + Percentile: 60, + Default: big.NewInt(params.GWei), + CongestedThreshold: 50, + DefaultBasePrice: expectedDefaultBasePricePreCurie, + } + var cases = []struct { + fork *big.Int // London fork number + pendingTx int // Number of pending transactions in the mempool + expect *big.Int // Expected gasprice suggestion + }{ + {nil, 0, expectedDefaultBasePricePreCurie}, // No congestion - default base price + {nil, 49, expectedDefaultBasePricePreCurie}, // No congestion - default base price + {nil, 50, big.NewInt(params.GWei * int64(30))}, // Congestion - normal behavior + {nil, 100, big.NewInt(params.GWei * int64(30))}, // Congestion - normal behavior + + // Fork point in genesis + {big.NewInt(0), 0, expectedDefaultBasePricePostCurie}, // No congestion - default base price + {big.NewInt(0), 49, expectedDefaultBasePricePostCurie}, // No congestion - default base price + {big.NewInt(0), 50, big.NewInt(params.GWei * int64(30))}, // Congestion - normal behavior + {big.NewInt(0), 100, big.NewInt(params.GWei * int64(30))}, // Congestion - normal behavior + + // Fork point in first block + {big.NewInt(1), 0, expectedDefaultBasePricePostCurie}, // No congestion - default base price + {big.NewInt(1), 49, expectedDefaultBasePricePostCurie}, // No congestion - default base price + {big.NewInt(1), 50, big.NewInt(params.GWei * int64(30))}, // Congestion - normal behavior + {big.NewInt(1), 100, big.NewInt(params.GWei * int64(30))}, // Congestion - normal behavior + + // Fork point in last block + {big.NewInt(32), 0, expectedDefaultBasePricePostCurie}, // No congestion - default base price + {big.NewInt(32), 49, expectedDefaultBasePricePostCurie}, // No congestion - default base price + {big.NewInt(32), 50, big.NewInt(params.GWei * int64(30))}, // Congestion - normal behavior + {big.NewInt(32), 100, big.NewInt(params.GWei * int64(30))}, // Congestion - normal behavior + + // Fork point in the future + {big.NewInt(33), 0, expectedDefaultBasePricePreCurie}, // No congestion - default base price + {big.NewInt(33), 49, expectedDefaultBasePricePreCurie}, // No congestion - default base price + {big.NewInt(33), 50, big.NewInt(params.GWei * int64(30))}, // Congestion - normal behavior + {big.NewInt(33), 100, big.NewInt(params.GWei * int64(30))}, // Congestion - normal behavior + } + for _, c := range cases { + backend := newTestBackend(t, c.fork, false, c.pendingTx) oracle := NewOracle(backend, config) // The gas price sampled is: 32G, 31G, 30G, 29G, 28G, 27G diff --git a/params/version.go b/params/version.go index 766652cc48c3..a8ddf9139005 100644 --- a/params/version.go +++ b/params/version.go @@ -24,7 +24,7 @@ import ( const ( VersionMajor = 5 // Major version component of the current release VersionMinor = 3 // Minor version component of the current release - VersionPatch = 34 // Patch version component of the current release + VersionPatch = 35 // Patch version component of the current release VersionMeta = "mainnet" // Version metadata to append to the version string )