Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: congestion-aware gas price oracle #790

Merged
merged 5 commits into from
Jun 4, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
colinlyguo marked this conversation as resolved.
Show resolved Hide resolved
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
colinlyguo marked this conversation as resolved.
Show resolved Hide resolved
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
Loading