Skip to content

Commit f38abc5

Browse files
authored
eth/gasprice: feeHistory improvements (#23422)
* eth/gasprice: cache feeHistory results * eth/gasprice: changed feeHistory block count limitation * eth/gasprice: do not use embedded struct in blockFees * eth/gasprice: fee processing logic cleanup * eth/gasprice: purge feeHistory cache at chain reorgs
1 parent dfeb2f7 commit f38abc5

File tree

5 files changed

+95
-61
lines changed

5 files changed

+95
-61
lines changed

eth/ethconfig/config.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ import (
4343
var FullNodeGPO = gasprice.Config{
4444
Blocks: 20,
4545
Percentile: 60,
46-
MaxHeaderHistory: 0,
47-
MaxBlockHistory: 0,
46+
MaxHeaderHistory: 1024,
47+
MaxBlockHistory: 1024,
4848
MaxPrice: gasprice.DefaultMaxPrice,
4949
IgnorePrice: gasprice.DefaultIgnorePrice,
5050
}

eth/gasprice/feehistory.go

Lines changed: 57 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@ package gasprice
1818

1919
import (
2020
"context"
21+
"encoding/binary"
2122
"errors"
2223
"fmt"
24+
"math"
2325
"math/big"
2426
"sort"
2527
"sync/atomic"
@@ -37,10 +39,6 @@ var (
3739
)
3840

3941
const (
40-
// maxFeeHistory is the maximum number of blocks that can be retrieved for a
41-
// fee history request.
42-
maxFeeHistory = 1024
43-
4442
// maxBlockFetchers is the max number of goroutines to spin up to pull blocks
4543
// for the fee history calculation (mostly relevant for LES).
4644
maxBlockFetchers = 4
@@ -54,10 +52,15 @@ type blockFees struct {
5452
block *types.Block // only set if reward percentiles are requested
5553
receipts types.Receipts
5654
// filled by processBlock
55+
results processedFees
56+
err error
57+
}
58+
59+
// processedFees contains the results of a processed block and is also used for caching
60+
type processedFees struct {
5761
reward []*big.Int
5862
baseFee, nextBaseFee *big.Int
5963
gasUsedRatio float64
60-
err error
6164
}
6265

6366
// txGasAndReward is sorted in ascending order based on reward
@@ -82,15 +85,15 @@ func (s sortGasAndReward) Less(i, j int) bool {
8285
// fills in the rest of the fields.
8386
func (oracle *Oracle) processBlock(bf *blockFees, percentiles []float64) {
8487
chainconfig := oracle.backend.ChainConfig()
85-
if bf.baseFee = bf.header.BaseFee; bf.baseFee == nil {
86-
bf.baseFee = new(big.Int)
88+
if bf.results.baseFee = bf.header.BaseFee; bf.results.baseFee == nil {
89+
bf.results.baseFee = new(big.Int)
8790
}
8891
if chainconfig.IsLondon(big.NewInt(int64(bf.blockNumber + 1))) {
89-
bf.nextBaseFee = misc.CalcBaseFee(chainconfig, bf.header)
92+
bf.results.nextBaseFee = misc.CalcBaseFee(chainconfig, bf.header)
9093
} else {
91-
bf.nextBaseFee = new(big.Int)
94+
bf.results.nextBaseFee = new(big.Int)
9295
}
93-
bf.gasUsedRatio = float64(bf.header.GasUsed) / float64(bf.header.GasLimit)
96+
bf.results.gasUsedRatio = float64(bf.header.GasUsed) / float64(bf.header.GasLimit)
9497
if len(percentiles) == 0 {
9598
// rewards were not requested, return null
9699
return
@@ -100,11 +103,11 @@ func (oracle *Oracle) processBlock(bf *blockFees, percentiles []float64) {
100103
return
101104
}
102105

103-
bf.reward = make([]*big.Int, len(percentiles))
106+
bf.results.reward = make([]*big.Int, len(percentiles))
104107
if len(bf.block.Transactions()) == 0 {
105108
// return an all zero row if there are no transactions to gather data from
106-
for i := range bf.reward {
107-
bf.reward[i] = new(big.Int)
109+
for i := range bf.results.reward {
110+
bf.results.reward[i] = new(big.Int)
108111
}
109112
return
110113
}
@@ -125,7 +128,7 @@ func (oracle *Oracle) processBlock(bf *blockFees, percentiles []float64) {
125128
txIndex++
126129
sumGasUsed += sorter[txIndex].gasUsed
127130
}
128-
bf.reward[i] = sorter[txIndex].reward
131+
bf.results.reward[i] = sorter[txIndex].reward
129132
}
130133
}
131134

@@ -134,7 +137,7 @@ func (oracle *Oracle) processBlock(bf *blockFees, percentiles []float64) {
134137
// also returned if requested and available.
135138
// Note: an error is only returned if retrieving the head header has failed. If there are no
136139
// retrievable blocks in the specified range then zero block count is returned with no error.
137-
func (oracle *Oracle) resolveBlockRange(ctx context.Context, lastBlock rpc.BlockNumber, blocks, maxHistory int) (*types.Block, []*types.Receipt, uint64, int, error) {
140+
func (oracle *Oracle) resolveBlockRange(ctx context.Context, lastBlock rpc.BlockNumber, blocks int) (*types.Block, []*types.Receipt, uint64, int, error) {
138141
var (
139142
headBlock rpc.BlockNumber
140143
pendingBlock *types.Block
@@ -167,17 +170,6 @@ func (oracle *Oracle) resolveBlockRange(ctx context.Context, lastBlock rpc.Block
167170
} else if pendingBlock == nil && lastBlock > headBlock {
168171
return nil, nil, 0, 0, fmt.Errorf("%w: requested %d, head %d", errRequestBeyondHead, lastBlock, headBlock)
169172
}
170-
if maxHistory != 0 {
171-
// limit retrieval to the given number of latest blocks
172-
if tooOldCount := int64(headBlock) - int64(maxHistory) - int64(lastBlock) + int64(blocks); tooOldCount > 0 {
173-
// tooOldCount is the number of requested blocks that are too old to be served
174-
if int64(blocks) > tooOldCount {
175-
blocks -= int(tooOldCount)
176-
} else {
177-
return nil, nil, 0, 0, nil
178-
}
179-
}
180-
}
181173
// ensure not trying to retrieve before genesis
182174
if rpc.BlockNumber(blocks) > lastBlock+1 {
183175
blocks = int(lastBlock + 1)
@@ -202,6 +194,10 @@ func (oracle *Oracle) FeeHistory(ctx context.Context, blocks int, unresolvedLast
202194
if blocks < 1 {
203195
return common.Big0, nil, nil, nil, nil // returning with no data and no error means there are no retrievable blocks
204196
}
197+
maxFeeHistory := oracle.maxHeaderHistory
198+
if len(rewardPercentiles) != 0 {
199+
maxFeeHistory = oracle.maxBlockHistory
200+
}
205201
if blocks > maxFeeHistory {
206202
log.Warn("Sanitizing fee history length", "requested", blocks, "truncated", maxFeeHistory)
207203
blocks = maxFeeHistory
@@ -214,17 +210,12 @@ func (oracle *Oracle) FeeHistory(ctx context.Context, blocks int, unresolvedLast
214210
return common.Big0, nil, nil, nil, fmt.Errorf("%w: #%d:%f > #%d:%f", errInvalidPercentile, i-1, rewardPercentiles[i-1], i, p)
215211
}
216212
}
217-
// Only process blocks if reward percentiles were requested
218-
maxHistory := oracle.maxHeaderHistory
219-
if len(rewardPercentiles) != 0 {
220-
maxHistory = oracle.maxBlockHistory
221-
}
222213
var (
223214
pendingBlock *types.Block
224215
pendingReceipts []*types.Receipt
225216
err error
226217
)
227-
pendingBlock, pendingReceipts, lastBlock, blocks, err := oracle.resolveBlockRange(ctx, unresolvedLastBlock, blocks, maxHistory)
218+
pendingBlock, pendingReceipts, lastBlock, blocks, err := oracle.resolveBlockRange(ctx, unresolvedLastBlock, blocks)
228219
if err != nil || blocks == 0 {
229220
return common.Big0, nil, nil, nil, err
230221
}
@@ -234,6 +225,10 @@ func (oracle *Oracle) FeeHistory(ctx context.Context, blocks int, unresolvedLast
234225
next = oldestBlock
235226
results = make(chan *blockFees, blocks)
236227
)
228+
percentileKey := make([]byte, 8*len(rewardPercentiles))
229+
for i, p := range rewardPercentiles {
230+
binary.LittleEndian.PutUint64(percentileKey[i*8:(i+1)*8], math.Float64bits(p))
231+
}
237232
for i := 0; i < maxBlockFetchers && i < blocks; i++ {
238233
go func() {
239234
for {
@@ -246,24 +241,38 @@ func (oracle *Oracle) FeeHistory(ctx context.Context, blocks int, unresolvedLast
246241
fees := &blockFees{blockNumber: blockNumber}
247242
if pendingBlock != nil && blockNumber >= pendingBlock.NumberU64() {
248243
fees.block, fees.receipts = pendingBlock, pendingReceipts
244+
fees.header = fees.block.Header()
245+
oracle.processBlock(fees, rewardPercentiles)
246+
results <- fees
249247
} else {
250-
if len(rewardPercentiles) != 0 {
251-
fees.block, fees.err = oracle.backend.BlockByNumber(ctx, rpc.BlockNumber(blockNumber))
252-
if fees.block != nil && fees.err == nil {
253-
fees.receipts, fees.err = oracle.backend.GetReceipts(ctx, fees.block.Hash())
254-
}
248+
cacheKey := struct {
249+
number uint64
250+
percentiles string
251+
}{blockNumber, string(percentileKey)}
252+
253+
if p, ok := oracle.historyCache.Get(cacheKey); ok {
254+
fees.results = p.(processedFees)
255+
results <- fees
255256
} else {
256-
fees.header, fees.err = oracle.backend.HeaderByNumber(ctx, rpc.BlockNumber(blockNumber))
257+
if len(rewardPercentiles) != 0 {
258+
fees.block, fees.err = oracle.backend.BlockByNumber(ctx, rpc.BlockNumber(blockNumber))
259+
if fees.block != nil && fees.err == nil {
260+
fees.receipts, fees.err = oracle.backend.GetReceipts(ctx, fees.block.Hash())
261+
fees.header = fees.block.Header()
262+
}
263+
} else {
264+
fees.header, fees.err = oracle.backend.HeaderByNumber(ctx, rpc.BlockNumber(blockNumber))
265+
}
266+
if fees.header != nil && fees.err == nil {
267+
oracle.processBlock(fees, rewardPercentiles)
268+
if fees.err == nil {
269+
oracle.historyCache.Add(cacheKey, fees.results)
270+
}
271+
}
272+
// send to results even if empty to guarantee that blocks items are sent in total
273+
results <- fees
257274
}
258275
}
259-
if fees.block != nil {
260-
fees.header = fees.block.Header()
261-
}
262-
if fees.header != nil {
263-
oracle.processBlock(fees, rewardPercentiles)
264-
}
265-
// send to results even if empty to guarantee that blocks items are sent in total
266-
results <- fees
267276
}
268277
}()
269278
}
@@ -279,8 +288,8 @@ func (oracle *Oracle) FeeHistory(ctx context.Context, blocks int, unresolvedLast
279288
return common.Big0, nil, nil, nil, fees.err
280289
}
281290
i := int(fees.blockNumber - oldestBlock)
282-
if fees.header != nil {
283-
reward[i], baseFee[i], baseFee[i+1], gasUsedRatio[i] = fees.reward, fees.baseFee, fees.nextBaseFee, fees.gasUsedRatio
291+
if fees.results.baseFee != nil {
292+
reward[i], baseFee[i], baseFee[i+1], gasUsedRatio[i] = fees.results.reward, fees.results.baseFee, fees.results.nextBaseFee, fees.results.gasUsedRatio
284293
} else {
285294
// getting no block and no error means we are requesting into the future (might happen because of a reorg)
286295
if i < firstMissing {

eth/gasprice/feehistory_test.go

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,20 +36,20 @@ func TestFeeHistory(t *testing.T) {
3636
expCount int
3737
expErr error
3838
}{
39-
{false, 0, 0, 10, 30, nil, 21, 10, nil},
40-
{false, 0, 0, 10, 30, []float64{0, 10}, 21, 10, nil},
41-
{false, 0, 0, 10, 30, []float64{20, 10}, 0, 0, errInvalidPercentile},
42-
{false, 0, 0, 1000000000, 30, nil, 0, 31, nil},
43-
{false, 0, 0, 1000000000, rpc.LatestBlockNumber, nil, 0, 33, nil},
44-
{false, 0, 0, 10, 40, nil, 0, 0, errRequestBeyondHead},
45-
{true, 0, 0, 10, 40, nil, 0, 0, errRequestBeyondHead},
39+
{false, 1000, 1000, 10, 30, nil, 21, 10, nil},
40+
{false, 1000, 1000, 10, 30, []float64{0, 10}, 21, 10, nil},
41+
{false, 1000, 1000, 10, 30, []float64{20, 10}, 0, 0, errInvalidPercentile},
42+
{false, 1000, 1000, 1000000000, 30, nil, 0, 31, nil},
43+
{false, 1000, 1000, 1000000000, rpc.LatestBlockNumber, nil, 0, 33, nil},
44+
{false, 1000, 1000, 10, 40, nil, 0, 0, errRequestBeyondHead},
45+
{true, 1000, 1000, 10, 40, nil, 0, 0, errRequestBeyondHead},
4646
{false, 20, 2, 100, rpc.LatestBlockNumber, nil, 13, 20, nil},
4747
{false, 20, 2, 100, rpc.LatestBlockNumber, []float64{0, 10}, 31, 2, nil},
4848
{false, 20, 2, 100, 32, []float64{0, 10}, 31, 2, nil},
49-
{false, 0, 0, 1, rpc.PendingBlockNumber, nil, 0, 0, nil},
50-
{false, 0, 0, 2, rpc.PendingBlockNumber, nil, 32, 1, nil},
51-
{true, 0, 0, 2, rpc.PendingBlockNumber, nil, 32, 2, nil},
52-
{true, 0, 0, 2, rpc.PendingBlockNumber, []float64{0, 10}, 32, 2, nil},
49+
{false, 1000, 1000, 1, rpc.PendingBlockNumber, nil, 0, 0, nil},
50+
{false, 1000, 1000, 2, rpc.PendingBlockNumber, nil, 32, 1, nil},
51+
{true, 1000, 1000, 2, rpc.PendingBlockNumber, nil, 32, 2, nil},
52+
{true, 1000, 1000, 2, rpc.PendingBlockNumber, []float64{0, 10}, 32, 2, nil},
5353
}
5454
for i, c := range cases {
5555
config := Config{

eth/gasprice/gasprice.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,13 @@ import (
2323
"sync"
2424

2525
"github.com/ethereum/go-ethereum/common"
26+
"github.com/ethereum/go-ethereum/core"
2627
"github.com/ethereum/go-ethereum/core/types"
28+
"github.com/ethereum/go-ethereum/event"
2729
"github.com/ethereum/go-ethereum/log"
2830
"github.com/ethereum/go-ethereum/params"
2931
"github.com/ethereum/go-ethereum/rpc"
32+
lru "github.com/hashicorp/golang-lru"
3033
)
3134

3235
const sampleNumber = 3 // Number of transactions sampled in a block
@@ -53,6 +56,7 @@ type OracleBackend interface {
5356
GetReceipts(ctx context.Context, hash common.Hash) (types.Receipts, error)
5457
PendingBlockAndReceipts() (*types.Block, types.Receipts)
5558
ChainConfig() *params.ChainConfig
59+
SubscribeChainHeadEvent(ch chan<- core.ChainHeadEvent) event.Subscription
5660
}
5761

5862
// Oracle recommends gas prices based on the content of recent
@@ -68,6 +72,7 @@ type Oracle struct {
6872

6973
checkBlocks, percentile int
7074
maxHeaderHistory, maxBlockHistory int
75+
historyCache *lru.Cache
7176
}
7277

7378
// NewOracle returns a new gasprice oracle which can recommend suitable
@@ -99,6 +104,20 @@ func NewOracle(backend OracleBackend, params Config) *Oracle {
99104
} else if ignorePrice.Int64() > 0 {
100105
log.Info("Gasprice oracle is ignoring threshold set", "threshold", ignorePrice)
101106
}
107+
108+
cache, _ := lru.New(2048)
109+
headEvent := make(chan core.ChainHeadEvent, 1)
110+
backend.SubscribeChainHeadEvent(headEvent)
111+
go func() {
112+
var lastHead common.Hash
113+
for ev := range headEvent {
114+
if ev.Block.ParentHash() != lastHead {
115+
cache.Purge()
116+
}
117+
lastHead = ev.Block.Hash()
118+
}
119+
}()
120+
102121
return &Oracle{
103122
backend: backend,
104123
lastPrice: params.Default,
@@ -108,6 +127,7 @@ func NewOracle(backend OracleBackend, params Config) *Oracle {
108127
percentile: percent,
109128
maxHeaderHistory: params.MaxHeaderHistory,
110129
maxBlockHistory: params.MaxBlockHistory,
130+
historyCache: cache,
111131
}
112132
}
113133

eth/gasprice/gasprice_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"github.com/ethereum/go-ethereum/core/types"
3030
"github.com/ethereum/go-ethereum/core/vm"
3131
"github.com/ethereum/go-ethereum/crypto"
32+
"github.com/ethereum/go-ethereum/event"
3233
"github.com/ethereum/go-ethereum/params"
3334
"github.com/ethereum/go-ethereum/rpc"
3435
)
@@ -90,6 +91,10 @@ func (b *testBackend) ChainConfig() *params.ChainConfig {
9091
return b.chain.Config()
9192
}
9293

94+
func (b *testBackend) SubscribeChainHeadEvent(ch chan<- core.ChainHeadEvent) event.Subscription {
95+
return nil
96+
}
97+
9398
func newTestBackend(t *testing.T, londonBlock *big.Int, pending bool) *testBackend {
9499
var (
95100
key, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")

0 commit comments

Comments
 (0)