Skip to content

Commit

Permalink
feat: congestion-aware gas price oracle (#790)
Browse files Browse the repository at this point in the history
* 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 <omerfirmak@users.noreply.github.com>
  • Loading branch information
jonastheis and omerfirmak authored Jun 4, 2024
1 parent 9ec83a5 commit b9d3c72
Show file tree
Hide file tree
Showing 8 changed files with 141 additions and 23 deletions.
2 changes: 2 additions & 0 deletions cmd/geth/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,8 @@ var (
utils.GpoPercentileFlag,
utils.GpoMaxGasPriceFlag,
utils.GpoIgnoreGasPriceFlag,
utils.GpoCongestionThresholdFlag,

utils.MinerNotifyFullFlag,
configFileFlag,
utils.CatalystFlag,
Expand Down
1 change: 1 addition & 0 deletions cmd/geth/usage.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ var AppHelpFlagGroups = []flags.FlagGroup{
utils.GpoPercentileFlag,
utils.GpoMaxGasPriceFlag,
utils.GpoIgnoreGasPriceFlag,
utils.GpoCongestionThresholdFlag,
},
},
{
Expand Down
8 changes: 8 additions & 0 deletions cmd/utils/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions eth/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion eth/gasprice/feehistory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
72 changes: 56 additions & 16 deletions eth/gasprice/gasprice.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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
}

Expand Down Expand Up @@ -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)
Expand All @@ -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,
}
}

Expand Down Expand Up @@ -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()
Expand Down
76 changes: 71 additions & 5 deletions eth/gasprice/gasprice_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion params/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
)

Expand Down

0 comments on commit b9d3c72

Please sign in to comment.