@@ -21,15 +21,20 @@ package gasprice
2121
2222import (
2323 "context"
24+ "encoding/binary"
2425 "errors"
2526 "fmt"
27+ "math"
2628 "math/big"
2729 "sort"
2830 "sync/atomic"
2931
3032 "github.com/holiman/uint256"
33+ "golang.org/x/sync/errgroup"
3134
3235 "github.com/erigontech/erigon/common"
36+ commonlru "github.com/erigontech/erigon/common/lru"
37+ "github.com/erigontech/erigon/execution/chain"
3338 "github.com/erigontech/erigon/execution/protocol/misc"
3439 "github.com/erigontech/erigon/execution/types"
3540 "github.com/erigontech/erigon/rpc"
@@ -46,8 +51,59 @@ const (
4651 maxFeeHistory = 1024
4752 // maxQueryLimit is the max number of requested percentiles.
4853 maxQueryLimit = 100
54+ // feeHistoryCacheSize is the number of processed block results to keep in LRU cache.
55+ feeHistoryCacheSize = 2048
56+ // maxBlockFetchers is the number of goroutines used in the parallel block fetch loop.
57+ maxBlockFetchers = 4
4958)
5059
60+ // cacheKey identifies a processed block in the fee history cache.
61+ // The percentiles string is a binary encoding of the requested percentile slice,
62+ // so identical percentile arrays produce the same key.
63+ type cacheKey struct {
64+ number uint64
65+ percentiles string
66+ }
67+
68+ // processedFees holds the computed fee data for a single block.
69+ // This is what gets stored in the LRU cache.
70+ type processedFees struct {
71+ reward []* big.Int
72+ baseFee , nextBaseFee * uint256.Int
73+ blobBaseFee , nextBlobBaseFee * uint256.Int
74+ gasUsedRatio float64
75+ blobGasUsedRatio float64
76+ }
77+
78+ // FeeHistoryCache is an opaque LRU cache for fee history block data.
79+ // It is safe for concurrent use.
80+ type FeeHistoryCache struct {
81+ c * commonlru.Cache [cacheKey , processedFees ]
82+ }
83+
84+ // NewFeeHistoryCache creates a new fee history cache.
85+ func NewFeeHistoryCache () * FeeHistoryCache {
86+ return & FeeHistoryCache {c : commonlru.NewCache [cacheKey , processedFees ](feeHistoryCacheSize )}
87+ }
88+
89+ func (fc * FeeHistoryCache ) get (k cacheKey ) (processedFees , bool ) {
90+ return fc .c .Get (k )
91+ }
92+
93+ func (fc * FeeHistoryCache ) add (k cacheKey , v processedFees ) {
94+ fc .c .Add (k , v )
95+ }
96+
97+ // encodePercentiles serializes a slice of float64 percentile values into a
98+ // binary string suitable for use as a cache key component.
99+ func encodePercentiles (percentiles []float64 ) string {
100+ b := make ([]byte , 8 * len (percentiles ))
101+ for i , p := range percentiles {
102+ binary .LittleEndian .PutUint64 (b [i * 8 :], math .Float64bits (p ))
103+ }
104+ return string (b )
105+ }
106+
51107// blockFees represents a single block for processing
52108type blockFees struct {
53109 // set by the caller
@@ -56,12 +112,8 @@ type blockFees struct {
56112 block * types.Block // only set if reward percentiles are requested
57113 receipts types.Receipts
58114 // filled by processBlock
59- reward []* big.Int
60- baseFee , nextBaseFee * uint256.Int
61- blobBaseFee , nextBlobBaseFee * uint256.Int
62- gasUsedRatio float64
63- blobGasUsedRatio float64
64- err error
115+ results processedFees
116+ err error
65117}
66118
67119// txGasAndReward is sorted in ascending order based on reward
@@ -84,15 +136,14 @@ func (s sortGasAndReward) Less(i, j int) bool {
84136// processBlock takes a blockFees structure with the blockNumber, the header and optionally
85137// the block field filled in, retrieves the block from the backend if not present yet and
86138// fills in the rest of the fields.
87- func (oracle * Oracle ) processBlock (bf * blockFees , percentiles []float64 ) {
88- chainconfig := oracle .backend .ChainConfig ()
89- if bf .baseFee = bf .header .BaseFee ; bf .baseFee == nil {
90- bf .baseFee = new (uint256.Int )
139+ func (oracle * Oracle ) processBlock (bf * blockFees , percentiles []float64 , chainconfig * chain.Config ) {
140+ if bf .results .baseFee = bf .header .BaseFee ; bf .results .baseFee == nil {
141+ bf .results .baseFee = new (uint256.Int )
91142 }
92143 if chainconfig .IsLondon (bf .blockNumber + 1 ) {
93- bf .nextBaseFee = misc .CalcBaseFee (chainconfig , bf .header )
144+ bf .results . nextBaseFee = misc .CalcBaseFee (chainconfig , bf .header )
94145 } else {
95- bf .nextBaseFee = new (uint256.Int )
146+ bf .results . nextBaseFee = new (uint256.Int )
96147 }
97148
98149 // Fill in blob base fee and next blob base fee.
@@ -108,17 +159,17 @@ func (oracle *Oracle) processBlock(bf *blockFees, percentiles []float64) {
108159 bf .err = err
109160 return
110161 }
111- bf .blobBaseFee = & blobBaseFee256
112- bf .nextBlobBaseFee = & nextBlobBaseFee256
162+ bf .results . blobBaseFee = & blobBaseFee256
163+ bf .results . nextBlobBaseFee = & nextBlobBaseFee256
113164
114165 } else {
115- bf .blobBaseFee = new (uint256.Int )
116- bf .nextBlobBaseFee = new (uint256.Int )
166+ bf .results . blobBaseFee = new (uint256.Int )
167+ bf .results . nextBlobBaseFee = new (uint256.Int )
117168 }
118- bf .gasUsedRatio = float64 (bf .header .GasUsed ) / float64 (bf .header .GasLimit )
169+ bf .results . gasUsedRatio = float64 (bf .header .GasUsed ) / float64 (bf .header .GasLimit )
119170
120171 if blobGasUsed := bf .header .BlobGasUsed ; blobGasUsed != nil && chainconfig .GetMaxBlobGasPerBlock (bf .header .Time ) != 0 {
121- bf .blobGasUsedRatio = float64 (* blobGasUsed ) / float64 (chainconfig .GetMaxBlobGasPerBlock (bf .header .Time ))
172+ bf .results . blobGasUsedRatio = float64 (* blobGasUsed ) / float64 (chainconfig .GetMaxBlobGasPerBlock (bf .header .Time ))
122173 }
123174
124175 if len (percentiles ) == 0 {
@@ -131,11 +182,11 @@ func (oracle *Oracle) processBlock(bf *blockFees, percentiles []float64) {
131182 return
132183 }
133184
134- bf .reward = make ([]* big.Int , len (percentiles ))
185+ bf .results . reward = make ([]* big.Int , len (percentiles ))
135186 if len (bf .block .Transactions ()) == 0 {
136187 // return an all zero row if there are no transactions to gather data from
137- for i := range bf .reward {
138- bf .reward [i ] = new (big.Int )
188+ for i := range bf .results . reward {
189+ bf .results . reward [i ] = new (big.Int )
139190 }
140191 return
141192 }
@@ -160,7 +211,7 @@ func (oracle *Oracle) processBlock(bf *blockFees, percentiles []float64) {
160211 txIndex ++
161212 sumGasUsed += sorter [txIndex ].gasUsed
162213 }
163- bf .reward [i ] = sorter [txIndex ].reward
214+ bf .results . reward [i ] = sorter [txIndex ].reward
164215 }
165216}
166217
@@ -273,61 +324,126 @@ func (oracle *Oracle) FeeHistory(ctx context.Context, blocks int, unresolvedLast
273324 }
274325 oldestBlock := lastBlock + 1 - uint64 (blocks )
275326
327+ // percentileKey is the binary-encoded percentile slice used as part of the cache key.
328+ percentileKey := encodePercentiles (rewardPercentiles )
329+
330+ // blockResult holds the computed data for a single block slot.
331+ type blockResult struct {
332+ processed processedFees
333+ hasResult bool
334+ missing bool
335+ }
336+
276337 var (
277- next = oldestBlock
278- )
279- var (
280- reward = make ([][]* big.Int , blocks )
281- baseFee = make ([]* uint256.Int , blocks + 1 )
282- gasUsedRatio = make ([]float64 , blocks )
283- blobGasUsedRatio = make ([]float64 , blocks )
284- blobBaseFee = make ([]* uint256.Int , blocks + 1 )
285- firstMissing = blocks
338+ next = oldestBlock
339+ blockResults = make ([]blockResult , blocks )
340+ reward = make ([][]* big.Int , blocks )
341+ baseFee = make ([]* uint256.Int , blocks + 1 )
342+ gasUsedRatio = make ([]float64 , blocks )
343+ blobBaseFee = make ([]* uint256.Int , blocks + 1 )
286344 )
287- for ; blocks > 0 ; blocks -- {
288- if err = common .Stopped (ctx .Done ()); err != nil {
289- return common .Big0 , nil , nil , nil , nil , nil , err
290- }
291- // Retrieve the next block number to fetch with this goroutine
292- blockNumber := atomic .AddUint64 (& next , 1 ) - 1
293- if blockNumber > lastBlock {
294- continue
295- }
296345
297- fees := & blockFees {blockNumber : blockNumber }
298- if pendingBlock != nil && blockNumber >= pendingBlock .NumberU64 () {
299- fees .block , fees .receipts = pendingBlock , pendingReceipts
300- } else {
301- if len (rewardPercentiles ) != 0 {
302- fees .block , fees .err = oracle .backend .BlockByNumber (ctx , rpc .BlockNumber (blockNumber ))
303- if fees .block != nil && fees .err == nil {
304- fees .receipts , fees .err = oracle .backend .GetReceiptsGasUsed (ctx , fees .block )
346+ // Pre-fetch chain config once using the main backend (safe: single goroutine).
347+ chainconfig := oracle .backend .ChainConfig ()
348+
349+ // Launch up to maxBlockFetchers goroutines. Each goroutine opens its own
350+ // TemporalTx via Fork so MDBX transactions are never shared across goroutines.
351+ // If Fork returns nil (not supported), we fall back to using the main backend
352+ // sequentially (only one goroutine will do real work in that case because they
353+ // all share the same backend, but correctness is preserved).
354+ g , fetchCtx := errgroup .WithContext (ctx )
355+ for range maxBlockFetchers {
356+ g .Go (func () error {
357+ localBackend , cleanup , forkErr := oracle .backend .Fork (fetchCtx )
358+ if forkErr != nil {
359+ return forkErr
360+ }
361+ if cleanup != nil {
362+ defer cleanup ()
363+ }
364+ if localBackend == nil {
365+ localBackend = oracle .backend
366+ }
367+
368+ for {
369+ if fetchCtx .Err () != nil {
370+ return nil
371+ }
372+ blockNumber := atomic .AddUint64 (& next , 1 ) - 1
373+ if blockNumber > lastBlock {
374+ return nil
375+ }
376+ idx := int (blockNumber - oldestBlock )
377+
378+ // Try the LRU cache first (skip for pending blocks — they are ephemeral).
379+ isPending := pendingBlock != nil && blockNumber >= pendingBlock .NumberU64 ()
380+ if ! isPending && oracle .historyCache != nil {
381+ if cached , ok := oracle .historyCache .get (cacheKey {blockNumber , percentileKey }); ok {
382+ blockResults [idx ] = blockResult {processed : cached , hasResult : true }
383+ continue
384+ }
385+ }
386+
387+ fees := & blockFees {blockNumber : blockNumber }
388+ if isPending {
389+ fees .block , fees .receipts = pendingBlock , pendingReceipts
390+ } else if len (rewardPercentiles ) != 0 {
391+ fees .block , fees .err = localBackend .BlockByNumber (fetchCtx , rpc .BlockNumber (blockNumber ))
392+ if fees .block != nil && fees .err == nil {
393+ fees .receipts , fees .err = localBackend .GetReceiptsGasUsed (fetchCtx , fees .block )
394+ }
395+ } else {
396+ fees .header , fees .err = localBackend .HeaderByNumber (fetchCtx , rpc .BlockNumber (blockNumber ))
397+ }
398+ if fees .block != nil {
399+ fees .header = fees .block .Header ()
400+ }
401+ if fees .err != nil {
402+ return fees .err
403+ }
404+
405+ if fees .header == nil {
406+ // No block and no error: requesting into the future (possible reorg).
407+ blockResults [idx ].missing = true
408+ continue
409+ }
410+
411+ oracle .processBlock (fees , rewardPercentiles , chainconfig )
412+ if fees .err != nil {
413+ return fees .err
414+ }
415+
416+ blockResults [idx ] = blockResult {processed : fees .results , hasResult : true }
417+ if ! isPending && oracle .historyCache != nil {
418+ oracle .historyCache .add (cacheKey {blockNumber , percentileKey }, fees .results )
305419 }
306- } else {
307- fees .header , fees .err = oracle .backend .HeaderByNumber (ctx , rpc .BlockNumber (blockNumber ))
308420 }
309- }
310- if fees .block != nil {
311- fees .header = fees .block .Header ()
312- }
313- if fees .header != nil {
314- oracle .processBlock (fees , rewardPercentiles )
315- }
421+ })
422+ }
423+ if err = g .Wait (); err != nil {
424+ return common .Big0 , nil , nil , nil , nil , nil , err
425+ }
316426
317- if fees .err != nil {
318- return common .Big0 , nil , nil , nil , nil , nil , fees .err
319- }
320- i := int (fees .blockNumber - oldestBlock )
321- if fees .header != nil {
322- reward [i ], baseFee [i ], baseFee [i + 1 ], gasUsedRatio [i ] = fees .reward , fees .baseFee , fees .nextBaseFee , fees .gasUsedRatio
323- blobGasUsedRatio [i ], blobBaseFee [i ], blobBaseFee [i + 1 ] = fees .blobGasUsedRatio , fees .blobBaseFee , fees .nextBlobBaseFee
324- } else {
325- // getting no block and no error means we are requesting into the future (might happen because of a reorg)
427+ // Post-processing is serial: all goroutines have finished, no races.
428+ firstMissing := len (blockResults )
429+ blobGasUsedRatio := make ([]float64 , len (blockResults ))
430+ for i , r := range blockResults {
431+ if r .missing || ! r .hasResult {
326432 if i < firstMissing {
327433 firstMissing = i
328434 }
435+ continue
329436 }
437+ p := & r .processed
438+ reward [i ] = p .reward
439+ baseFee [i ] = p .baseFee
440+ baseFee [i + 1 ] = p .nextBaseFee
441+ gasUsedRatio [i ] = p .gasUsedRatio
442+ blobGasUsedRatio [i ] = p .blobGasUsedRatio
443+ blobBaseFee [i ] = p .blobBaseFee
444+ blobBaseFee [i + 1 ] = p .nextBlobBaseFee
330445 }
446+
331447 if firstMissing == 0 {
332448 return common .Big0 , nil , nil , nil , nil , nil , nil
333449 }
0 commit comments