diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go
index cc5af5625c..fdeb7031c6 100644
--- a/cmd/utils/flags.go
+++ b/cmd/utils/flags.go
@@ -1302,8 +1302,7 @@ func setGPO(ctx *cli.Context, cfg *gasprice.Config, light bool) {
// If we are running the light client, apply another group
// settings for gas oracle.
if light {
- cfg.Blocks = ethconfig.LightClientGPO.Blocks
- cfg.Percentile = ethconfig.LightClientGPO.Percentile
+ *cfg = ethconfig.LightClientGPO
}
if ctx.GlobalIsSet(GpoBlocksFlag.Name) {
cfg.Blocks = ctx.GlobalInt(GpoBlocksFlag.Name)
diff --git a/eth/api_backend.go b/eth/api_backend.go
index e6810f2a9b..37c5d8a09f 100644
--- a/eth/api_backend.go
+++ b/eth/api_backend.go
@@ -133,6 +133,10 @@ func (b *EthAPIBackend) BlockByNumberOrHash(ctx context.Context, blockNrOrHash r
return nil, errors.New("invalid arguments; neither block nor hash specified")
}
+func (b *EthAPIBackend) PendingBlockAndReceipts() (*types.Block, types.Receipts) {
+ return b.eth.miner.PendingBlockAndReceipts()
+}
+
func (b *EthAPIBackend) StateAndHeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*state.StateDB, *types.Header, error) {
// Pending state is only known by the miner
if number == rpc.PendingBlockNumber {
@@ -279,6 +283,10 @@ func (b *EthAPIBackend) SuggestGasTipCap(ctx context.Context) (*big.Int, error)
return b.gpo.SuggestTipCap(ctx)
}
+func (b *EthAPIBackend) FeeHistory(ctx context.Context, blockCount int, lastBlock rpc.BlockNumber, rewardPercentiles []float64) (firstBlock rpc.BlockNumber, reward [][]*big.Int, baseFee []*big.Int, gasUsedRatio []float64, err error) {
+ return b.gpo.FeeHistory(ctx, blockCount, lastBlock, rewardPercentiles)
+}
+
func (b *EthAPIBackend) ChainDb() ethdb.Database {
return b.eth.ChainDb()
}
diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go
index 349c8da6cc..d971badafe 100644
--- a/eth/ethconfig/config.go
+++ b/eth/ethconfig/config.go
@@ -41,18 +41,22 @@ import (
// FullNodeGPO contains default gasprice oracle settings for full node.
var FullNodeGPO = gasprice.Config{
- Blocks: 20,
- Percentile: 60,
- MaxPrice: gasprice.DefaultMaxPrice,
- IgnorePrice: gasprice.DefaultIgnorePrice,
+ Blocks: 20,
+ Percentile: 60,
+ MaxHeaderHistory: 0,
+ MaxBlockHistory: 0,
+ MaxPrice: gasprice.DefaultMaxPrice,
+ IgnorePrice: gasprice.DefaultIgnorePrice,
}
// LightClientGPO contains default gasprice oracle settings for light client.
var LightClientGPO = gasprice.Config{
- Blocks: 2,
- Percentile: 60,
- MaxPrice: gasprice.DefaultMaxPrice,
- IgnorePrice: gasprice.DefaultIgnorePrice,
+ Blocks: 2,
+ Percentile: 60,
+ MaxHeaderHistory: 300,
+ MaxBlockHistory: 5,
+ MaxPrice: gasprice.DefaultMaxPrice,
+ IgnorePrice: gasprice.DefaultIgnorePrice,
}
// Defaults contains default settings for use on the Ethereum main net.
diff --git a/eth/gasprice/feehistory.go b/eth/gasprice/feehistory.go
new file mode 100644
index 0000000000..84ec8e0889
--- /dev/null
+++ b/eth/gasprice/feehistory.go
@@ -0,0 +1,293 @@
+// Copyright 2021 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package gasprice
+
+import (
+ "context"
+ "errors"
+ "math/big"
+ "sort"
+ "sync/atomic"
+
+ "github.com/ethereum/go-ethereum/consensus/misc"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/log"
+ "github.com/ethereum/go-ethereum/rpc"
+)
+
+var (
+ errInvalidPercentiles = errors.New("Invalid reward percentiles")
+ errRequestBeyondHead = errors.New("Request beyond head block")
+)
+
+const maxBlockCount = 1024 // number of blocks retrievable with a single query
+
+// blockFees represents a single block for processing
+type blockFees struct {
+ // set by the caller
+ blockNumber rpc.BlockNumber
+ header *types.Header
+ block *types.Block // only set if reward percentiles are requested
+ receipts types.Receipts
+ // filled by processBlock
+ reward []*big.Int
+ baseFee, nextBaseFee *big.Int
+ gasUsedRatio float64
+ err error
+}
+
+// txGasAndReward is sorted in ascending order based on reward
+type (
+ txGasAndReward struct {
+ gasUsed uint64
+ reward *big.Int
+ }
+ sortGasAndReward []txGasAndReward
+)
+
+func (s sortGasAndReward) Len() int { return len(s) }
+func (s sortGasAndReward) Swap(i, j int) {
+ s[i], s[j] = s[j], s[i]
+}
+func (s sortGasAndReward) Less(i, j int) bool {
+ return s[i].reward.Cmp(s[j].reward) < 0
+}
+
+// processBlock takes a blockFees structure with the blockNumber, the header and optionally
+// the block field filled in, retrieves the block from the backend if not present yet and
+// fills in the rest of the fields.
+func (oracle *Oracle) processBlock(bf *blockFees, percentiles []float64) {
+ chainconfig := oracle.backend.ChainConfig()
+ if bf.baseFee = bf.header.BaseFee; bf.baseFee == nil {
+ bf.baseFee = new(big.Int)
+ }
+ if chainconfig.IsLondon(big.NewInt(int64(bf.blockNumber + 1))) {
+ bf.nextBaseFee = misc.CalcBaseFee(chainconfig, bf.header)
+ } else {
+ bf.nextBaseFee = new(big.Int)
+ }
+ bf.gasUsedRatio = float64(bf.header.GasUsed) / float64(bf.header.GasLimit)
+ if len(percentiles) == 0 {
+ // rewards were not requested, return null
+ return
+ }
+ if bf.block == nil || (bf.receipts == nil && len(bf.block.Transactions()) != 0) {
+ log.Error("Block or receipts are missing while reward percentiles are requested")
+ return
+ }
+
+ bf.reward = make([]*big.Int, len(percentiles))
+ if len(bf.block.Transactions()) == 0 {
+ // return an all zero row if there are no transactions to gather data from
+ for i := range bf.reward {
+ bf.reward[i] = new(big.Int)
+ }
+ return
+ }
+
+ sorter := make(sortGasAndReward, len(bf.block.Transactions()))
+ for i, tx := range bf.block.Transactions() {
+ reward, _ := tx.EffectiveGasTip(bf.block.BaseFee())
+ sorter[i] = txGasAndReward{gasUsed: bf.receipts[i].GasUsed, reward: reward}
+ }
+ sort.Sort(sorter)
+
+ var txIndex int
+ sumGasUsed := sorter[0].gasUsed
+
+ for i, p := range percentiles {
+ thresholdGasUsed := uint64(float64(bf.block.GasUsed()) * p / 100)
+ for sumGasUsed < thresholdGasUsed && txIndex < len(bf.block.Transactions())-1 {
+ txIndex++
+ sumGasUsed += sorter[txIndex].gasUsed
+ }
+ bf.reward[i] = sorter[txIndex].reward
+ }
+}
+
+// resolveBlockRange resolves the specified block range to absolute block numbers while also
+// enforcing backend specific limitations. The pending block and corresponding receipts are
+// also returned if requested and available.
+// Note: an error is only returned if retrieving the head header has failed. If there are no
+// retrievable blocks in the specified range then zero block count is returned with no error.
+func (oracle *Oracle) resolveBlockRange(ctx context.Context, lastBlockNumber rpc.BlockNumber, blockCount, maxHistory int) (*types.Block, types.Receipts, rpc.BlockNumber, int, error) {
+ var (
+ headBlockNumber rpc.BlockNumber
+ pendingBlock *types.Block
+ pendingReceipts types.Receipts
+ )
+
+ // query either pending block or head header and set headBlockNumber
+ if lastBlockNumber == rpc.PendingBlockNumber {
+ if pendingBlock, pendingReceipts = oracle.backend.PendingBlockAndReceipts(); pendingBlock != nil {
+ lastBlockNumber = rpc.BlockNumber(pendingBlock.NumberU64())
+ headBlockNumber = lastBlockNumber - 1
+ } else {
+ // pending block not supported by backend, process until latest block
+ lastBlockNumber = rpc.LatestBlockNumber
+ blockCount--
+ if blockCount == 0 {
+ return nil, nil, 0, 0, nil
+ }
+ }
+ }
+ if pendingBlock == nil {
+ // if pending block is not fetched then we retrieve the head header to get the head block number
+ if latestHeader, err := oracle.backend.HeaderByNumber(ctx, rpc.LatestBlockNumber); err == nil {
+ headBlockNumber = rpc.BlockNumber(latestHeader.Number.Uint64())
+ } else {
+ return nil, nil, 0, 0, err
+ }
+ }
+ if lastBlockNumber == rpc.LatestBlockNumber {
+ lastBlockNumber = headBlockNumber
+ } else if pendingBlock == nil && lastBlockNumber > headBlockNumber {
+ return nil, nil, 0, 0, errRequestBeyondHead
+ }
+ if maxHistory != 0 {
+ // limit retrieval to the given number of latest blocks
+ if tooOldCount := int64(headBlockNumber) - int64(maxHistory) - int64(lastBlockNumber) + int64(blockCount); tooOldCount > 0 {
+ // tooOldCount is the number of requested blocks that are too old to be served
+ if int64(blockCount) > tooOldCount {
+ blockCount -= int(tooOldCount)
+ } else {
+ return nil, nil, 0, 0, nil
+ }
+ }
+ }
+ // ensure not trying to retrieve before genesis
+ if rpc.BlockNumber(blockCount) > lastBlockNumber+1 {
+ blockCount = int(lastBlockNumber + 1)
+ }
+ return pendingBlock, pendingReceipts, lastBlockNumber, blockCount, nil
+}
+
+// FeeHistory returns data relevant for fee estimation based on the specified range of blocks.
+// The range can be specified either with absolute block numbers or ending with the latest
+// or pending block. Backends may or may not support gathering data from the pending block
+// or blocks older than a certain age (specified in maxHistory). The first block of the
+// actually processed range is returned to avoid ambiguity when parts of the requested range
+// are not available or when the head has changed during processing this request.
+// Three arrays are returned based on the processed blocks:
+// - reward: the requested percentiles of effective priority fees per gas of transactions in each
+// block, sorted in ascending order and weighted by gas used.
+// - baseFee: base fee per gas in the given block
+// - gasUsedRatio: gasUsed/gasLimit in the given block
+// Note: baseFee includes the next block after the newest of the returned range, because this
+// value can be derived from the newest block.
+func (oracle *Oracle) FeeHistory(ctx context.Context, blockCount int, lastBlockNumber rpc.BlockNumber, rewardPercentiles []float64) (firstBlockNumber rpc.BlockNumber, reward [][]*big.Int, baseFee []*big.Int, gasUsedRatio []float64, err error) {
+ if blockCount < 1 {
+ // returning with no data and no error means there are no retrievable blocks
+ return
+ }
+ if blockCount > maxBlockCount {
+ blockCount = maxBlockCount
+ }
+ for i, p := range rewardPercentiles {
+ if p < 0 || p > 100 || (i > 0 && p < rewardPercentiles[i-1]) {
+ return 0, nil, nil, nil, errInvalidPercentiles
+ }
+ }
+
+ processBlocks := len(rewardPercentiles) != 0
+ // limit retrieval to maxHistory if set
+ var maxHistory int
+ if processBlocks {
+ maxHistory = oracle.maxBlockHistory
+ } else {
+ maxHistory = oracle.maxHeaderHistory
+ }
+
+ var (
+ pendingBlock *types.Block
+ pendingReceipts types.Receipts
+ )
+ if pendingBlock, pendingReceipts, lastBlockNumber, blockCount, err = oracle.resolveBlockRange(ctx, lastBlockNumber, blockCount, maxHistory); err != nil || blockCount == 0 {
+ return
+ }
+ firstBlockNumber = lastBlockNumber + 1 - rpc.BlockNumber(blockCount)
+
+ processNext := int64(firstBlockNumber)
+ resultCh := make(chan *blockFees, blockCount)
+ threadCount := 4
+ if blockCount < threadCount {
+ threadCount = blockCount
+ }
+ for i := 0; i < threadCount; i++ {
+ go func() {
+ for {
+ blockNumber := rpc.BlockNumber(atomic.AddInt64(&processNext, 1) - 1)
+ if blockNumber > lastBlockNumber {
+ return
+ }
+
+ bf := &blockFees{blockNumber: blockNumber}
+ if pendingBlock != nil && blockNumber >= rpc.BlockNumber(pendingBlock.NumberU64()) {
+ bf.block, bf.receipts = pendingBlock, pendingReceipts
+ } else {
+ if processBlocks {
+ bf.block, bf.err = oracle.backend.BlockByNumber(ctx, blockNumber)
+ if bf.block != nil {
+ bf.receipts, bf.err = oracle.backend.GetReceipts(ctx, bf.block.Hash())
+ }
+ } else {
+ bf.header, bf.err = oracle.backend.HeaderByNumber(ctx, blockNumber)
+ }
+ }
+ if bf.block != nil {
+ bf.header = bf.block.Header()
+ }
+ if bf.header != nil {
+ oracle.processBlock(bf, rewardPercentiles)
+ }
+ // send to resultCh even if empty to guarantee that blockCount items are sent in total
+ resultCh <- bf
+ }
+ }()
+ }
+
+ reward = make([][]*big.Int, blockCount)
+ baseFee = make([]*big.Int, blockCount+1)
+ gasUsedRatio = make([]float64, blockCount)
+ firstMissing := blockCount
+
+ for ; blockCount > 0; blockCount-- {
+ bf := <-resultCh
+ if bf.err != nil {
+ return 0, nil, nil, nil, bf.err
+ }
+ i := int(bf.blockNumber - firstBlockNumber)
+ if bf.header != nil {
+ reward[i], baseFee[i], baseFee[i+1], gasUsedRatio[i] = bf.reward, bf.baseFee, bf.nextBaseFee, bf.gasUsedRatio
+ } else {
+ // getting no block and no error means we are requesting into the future (might happen because of a reorg)
+ if i < firstMissing {
+ firstMissing = i
+ }
+ }
+ }
+ if firstMissing == 0 {
+ return 0, nil, nil, nil, nil
+ }
+ if processBlocks {
+ reward = reward[:firstMissing]
+ } else {
+ reward = nil
+ }
+ baseFee, gasUsedRatio = baseFee[:firstMissing+1], gasUsedRatio[:firstMissing]
+ return
+}
diff --git a/eth/gasprice/feehistory_test.go b/eth/gasprice/feehistory_test.go
new file mode 100644
index 0000000000..191e2f0ce6
--- /dev/null
+++ b/eth/gasprice/feehistory_test.go
@@ -0,0 +1,88 @@
+// Copyright 2021 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package gasprice
+
+import (
+ "context"
+ "math/big"
+ "testing"
+
+ "github.com/ethereum/go-ethereum/rpc"
+)
+
+func TestFeeHistory(t *testing.T) {
+ var cases = []struct {
+ pending bool
+ maxHeader, maxBlock int
+ count int
+ last rpc.BlockNumber
+ percent []float64
+ expFirst rpc.BlockNumber
+ expCount int
+ expErr error
+ }{
+ {false, 0, 0, 10, 30, nil, 21, 10, nil},
+ {false, 0, 0, 10, 30, []float64{0, 10}, 21, 10, nil},
+ {false, 0, 0, 10, 30, []float64{20, 10}, 0, 0, errInvalidPercentiles},
+ {false, 0, 0, 1000000000, 30, nil, 0, 31, nil},
+ {false, 0, 0, 1000000000, rpc.LatestBlockNumber, nil, 0, 33, nil},
+ {false, 0, 0, 10, 40, nil, 0, 0, errRequestBeyondHead},
+ {true, 0, 0, 10, 40, nil, 0, 0, errRequestBeyondHead},
+ {false, 20, 2, 100, rpc.LatestBlockNumber, nil, 13, 20, nil},
+ {false, 20, 2, 100, rpc.LatestBlockNumber, []float64{0, 10}, 31, 2, nil},
+ {false, 20, 2, 100, 32, []float64{0, 10}, 31, 2, nil},
+ {false, 0, 0, 1, rpc.PendingBlockNumber, nil, 0, 0, nil},
+ {false, 0, 0, 2, rpc.PendingBlockNumber, nil, 32, 1, nil},
+ {true, 0, 0, 2, rpc.PendingBlockNumber, nil, 32, 2, nil},
+ {true, 0, 0, 2, rpc.PendingBlockNumber, []float64{0, 10}, 32, 2, nil},
+ }
+ for i, c := range cases {
+ config := Config{
+ MaxHeaderHistory: c.maxHeader,
+ MaxBlockHistory: c.maxBlock,
+ }
+ backend := newTestBackend(t, big.NewInt(16), c.pending)
+ oracle := NewOracle(backend, config)
+
+ first, reward, baseFee, ratio, err := oracle.FeeHistory(context.Background(), c.count, c.last, c.percent)
+
+ expReward := c.expCount
+ if len(c.percent) == 0 {
+ expReward = 0
+ }
+ expBaseFee := c.expCount
+ if expBaseFee != 0 {
+ expBaseFee++
+ }
+
+ if first != c.expFirst {
+ t.Fatalf("Test case %d: first block mismatch, want %d, got %d", i, c.expFirst, first)
+ }
+ if len(reward) != expReward {
+ t.Fatalf("Test case %d: reward array length mismatch, want %d, got %d", i, expReward, len(reward))
+ }
+ if len(baseFee) != expBaseFee {
+ t.Fatalf("Test case %d: baseFee array length mismatch, want %d, got %d", i, expBaseFee, len(baseFee))
+ }
+ if len(ratio) != c.expCount {
+ t.Fatalf("Test case %d: gasUsedRatio array length mismatch, want %d, got %d", i, c.expCount, len(ratio))
+ }
+ if err != c.expErr {
+ t.Fatalf("Test case %d: error mismatch, want %v, got %v", i, c.expErr, err)
+ }
+ }
+}
diff --git a/eth/gasprice/gasprice.go b/eth/gasprice/gasprice.go
index f2c4def141..407eeaa289 100644
--- a/eth/gasprice/gasprice.go
+++ b/eth/gasprice/gasprice.go
@@ -37,17 +37,21 @@ var (
)
type Config struct {
- Blocks int
- Percentile 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"`
}
// OracleBackend includes all necessary background APIs for oracle.
type OracleBackend interface {
HeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Header, error)
BlockByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Block, error)
+ GetReceipts(ctx context.Context, hash common.Hash) (types.Receipts, error)
+ PendingBlockAndReceipts() (*types.Block, types.Receipts)
ChainConfig() *params.ChainConfig
}
@@ -62,8 +66,8 @@ type Oracle struct {
cacheLock sync.RWMutex
fetchLock sync.Mutex
- checkBlocks int
- percentile int
+ checkBlocks, percentile int
+ maxHeaderHistory, maxBlockHistory int
}
// NewOracle returns a new gasprice oracle which can recommend suitable
@@ -96,12 +100,14 @@ func NewOracle(backend OracleBackend, params Config) *Oracle {
log.Info("Gasprice oracle is ignoring threshold set", "threshold", ignorePrice)
}
return &Oracle{
- backend: backend,
- lastPrice: params.Default,
- maxPrice: maxPrice,
- ignorePrice: ignorePrice,
- checkBlocks: blocks,
- percentile: percent,
+ backend: backend,
+ lastPrice: params.Default,
+ maxPrice: maxPrice,
+ ignorePrice: ignorePrice,
+ checkBlocks: blocks,
+ percentile: percent,
+ maxHeaderHistory: params.MaxHeaderHistory,
+ maxBlockHistory: params.MaxBlockHistory,
}
}
@@ -111,36 +117,36 @@ func NewOracle(backend OracleBackend, params Config) *Oracle {
// Note, for legacy transactions and the legacy eth_gasPrice RPC call, it will be
// necessary to add the basefee to the returned number to fall back to the legacy
// behavior.
-func (gpo *Oracle) SuggestTipCap(ctx context.Context) (*big.Int, error) {
- head, _ := gpo.backend.HeaderByNumber(ctx, rpc.LatestBlockNumber)
+func (oracle *Oracle) SuggestTipCap(ctx context.Context) (*big.Int, error) {
+ head, _ := oracle.backend.HeaderByNumber(ctx, rpc.LatestBlockNumber)
headHash := head.Hash()
// If the latest gasprice is still available, return it.
- gpo.cacheLock.RLock()
- lastHead, lastPrice := gpo.lastHead, gpo.lastPrice
- gpo.cacheLock.RUnlock()
+ oracle.cacheLock.RLock()
+ lastHead, lastPrice := oracle.lastHead, oracle.lastPrice
+ oracle.cacheLock.RUnlock()
if headHash == lastHead {
return new(big.Int).Set(lastPrice), nil
}
- gpo.fetchLock.Lock()
- defer gpo.fetchLock.Unlock()
+ oracle.fetchLock.Lock()
+ defer oracle.fetchLock.Unlock()
// Try checking the cache again, maybe the last fetch fetched what we need
- gpo.cacheLock.RLock()
- lastHead, lastPrice = gpo.lastHead, gpo.lastPrice
- gpo.cacheLock.RUnlock()
+ oracle.cacheLock.RLock()
+ lastHead, lastPrice = oracle.lastHead, oracle.lastPrice
+ oracle.cacheLock.RUnlock()
if headHash == lastHead {
return new(big.Int).Set(lastPrice), nil
}
var (
sent, exp int
number = head.Number.Uint64()
- result = make(chan results, gpo.checkBlocks)
+ result = make(chan results, oracle.checkBlocks)
quit = make(chan struct{})
results []*big.Int
)
- for sent < gpo.checkBlocks && number > 0 {
- go gpo.getBlockValues(ctx, types.MakeSigner(gpo.backend.ChainConfig(), big.NewInt(int64(number))), number, sampleNumber, gpo.ignorePrice, result, quit)
+ for sent < oracle.checkBlocks && number > 0 {
+ go oracle.getBlockValues(ctx, types.MakeSigner(oracle.backend.ChainConfig(), big.NewInt(int64(number))), number, sampleNumber, oracle.ignorePrice, result, quit)
sent++
exp++
number--
@@ -162,8 +168,8 @@ func (gpo *Oracle) SuggestTipCap(ctx context.Context) (*big.Int, error) {
// Besides, in order to collect enough data for sampling, if nothing
// meaningful returned, try to query more blocks. But the maximum
// is 2*checkBlocks.
- if len(res.values) == 1 && len(results)+1+exp < gpo.checkBlocks*2 && number > 0 {
- go gpo.getBlockValues(ctx, types.MakeSigner(gpo.backend.ChainConfig(), big.NewInt(int64(number))), number, sampleNumber, gpo.ignorePrice, result, quit)
+ if len(res.values) == 1 && len(results)+1+exp < oracle.checkBlocks*2 && number > 0 {
+ go oracle.getBlockValues(ctx, types.MakeSigner(oracle.backend.ChainConfig(), big.NewInt(int64(number))), number, sampleNumber, oracle.ignorePrice, result, quit)
sent++
exp++
number--
@@ -173,15 +179,15 @@ func (gpo *Oracle) SuggestTipCap(ctx context.Context) (*big.Int, error) {
price := lastPrice
if len(results) > 0 {
sort.Sort(bigIntArray(results))
- price = results[(len(results)-1)*gpo.percentile/100]
+ price = results[(len(results)-1)*oracle.percentile/100]
}
- if price.Cmp(gpo.maxPrice) > 0 {
- price = new(big.Int).Set(gpo.maxPrice)
+ if price.Cmp(oracle.maxPrice) > 0 {
+ price = new(big.Int).Set(oracle.maxPrice)
}
- gpo.cacheLock.Lock()
- gpo.lastHead = headHash
- gpo.lastPrice = price
- gpo.cacheLock.Unlock()
+ oracle.cacheLock.Lock()
+ oracle.lastHead = headHash
+ oracle.lastPrice = price
+ oracle.cacheLock.Unlock()
return new(big.Int).Set(price), nil
}
@@ -219,8 +225,8 @@ func (s *txSorter) Less(i, j int) bool {
// and sends it to the result channel. If the block is empty or all transactions
// are sent by the miner itself(it doesn't make any sense to include this kind of
// transaction prices for sampling), nil gasprice is returned.
-func (gpo *Oracle) getBlockValues(ctx context.Context, signer types.Signer, blockNum uint64, limit int, ignoreUnder *big.Int, result chan results, quit chan struct{}) {
- block, err := gpo.backend.BlockByNumber(ctx, rpc.BlockNumber(blockNum))
+func (oracle *Oracle) getBlockValues(ctx context.Context, signer types.Signer, blockNum uint64, limit int, ignoreUnder *big.Int, result chan results, quit chan struct{}) {
+ block, err := oracle.backend.BlockByNumber(ctx, rpc.BlockNumber(blockNum))
if block == nil {
select {
case result <- results{nil, err}:
diff --git a/eth/gasprice/gasprice_test.go b/eth/gasprice/gasprice_test.go
index f86449c5a5..dea8fea95a 100644
--- a/eth/gasprice/gasprice_test.go
+++ b/eth/gasprice/gasprice_test.go
@@ -33,29 +33,64 @@ import (
"github.com/ethereum/go-ethereum/rpc"
)
+const testHead = 32
+
type testBackend struct {
- chain *core.BlockChain
+ chain *core.BlockChain
+ pending bool // pending block available
}
func (b *testBackend) HeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Header, error) {
+ if number > testHead {
+ return nil, nil
+ }
if number == rpc.LatestBlockNumber {
- return b.chain.CurrentBlock().Header(), nil
+ number = testHead
+ }
+ if number == rpc.PendingBlockNumber {
+ if b.pending {
+ number = testHead + 1
+ } else {
+ return nil, nil
+ }
}
return b.chain.GetHeaderByNumber(uint64(number)), nil
}
func (b *testBackend) BlockByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Block, error) {
+ if number > testHead {
+ return nil, nil
+ }
if number == rpc.LatestBlockNumber {
- return b.chain.CurrentBlock(), nil
+ number = testHead
+ }
+ if number == rpc.PendingBlockNumber {
+ if b.pending {
+ number = testHead + 1
+ } else {
+ return nil, nil
+ }
}
return b.chain.GetBlockByNumber(uint64(number)), nil
}
+func (b *testBackend) GetReceipts(ctx context.Context, hash common.Hash) (types.Receipts, error) {
+ return b.chain.GetReceiptsByHash(hash), nil
+}
+
+func (b *testBackend) PendingBlockAndReceipts() (*types.Block, types.Receipts) {
+ if b.pending {
+ block := b.chain.GetBlockByNumber(testHead + 1)
+ return block, b.chain.GetReceiptsByHash(block.Hash())
+ }
+ return nil, nil
+}
+
func (b *testBackend) ChainConfig() *params.ChainConfig {
return b.chain.Config()
}
-func newTestBackend(t *testing.T, londonBlock *big.Int) *testBackend {
+func newTestBackend(t *testing.T, londonBlock *big.Int, pending bool) *testBackend {
var (
key, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
addr = crypto.PubkeyToAddress(key.PublicKey)
@@ -76,7 +111,7 @@ func newTestBackend(t *testing.T, londonBlock *big.Int) *testBackend {
genesis, _ := gspec.Commit(db)
// Generate testing blocks
- blocks, _ := core.GenerateChain(gspec.Config, genesis, engine, db, 32, func(i int, b *core.BlockGen) {
+ blocks, _ := core.GenerateChain(gspec.Config, genesis, engine, db, testHead+1, func(i int, b *core.BlockGen) {
b.SetCoinbase(common.Address{1})
var tx *types.Transaction
@@ -116,7 +151,7 @@ func newTestBackend(t *testing.T, londonBlock *big.Int) *testBackend {
t.Fatalf("Failed to create local chain, %v", err)
}
chain.InsertChain(blocks)
- return &testBackend{chain: chain}
+ return &testBackend{chain: chain, pending: pending}
}
func (b *testBackend) CurrentHeader() *types.Header {
@@ -144,7 +179,7 @@ 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)
+ backend := newTestBackend(t, c.fork, false)
oracle := NewOracle(backend, config)
// The gas price sampled is: 32G, 31G, 30G, 29G, 28G, 27G
diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go
index 8cbb01fbba..913cc0b974 100644
--- a/internal/ethapi/api.go
+++ b/internal/ethapi/api.go
@@ -80,6 +80,40 @@ func (s *PublicEthereumAPI) MaxPriorityFeePerGas(ctx context.Context) (*hexutil.
return (*hexutil.Big)(tipcap), err
}
+type feeHistoryResults struct {
+ FirstBlock rpc.BlockNumber
+ Reward [][]*hexutil.Big
+ BaseFee []*hexutil.Big
+ GasUsedRatio []float64
+}
+
+func (s *PublicEthereumAPI) FeeHistory(ctx context.Context, blockCount int, lastBlock rpc.BlockNumber, rewardPercentiles []float64) (feeHistoryResults, error) {
+ firstBlock, reward, baseFee, gasUsedRatio, err := s.b.FeeHistory(ctx, blockCount, lastBlock, rewardPercentiles)
+ if err != nil {
+ return feeHistoryResults{}, err
+ }
+ results := feeHistoryResults{
+ FirstBlock: firstBlock,
+ GasUsedRatio: gasUsedRatio,
+ }
+ if reward != nil {
+ results.Reward = make([][]*hexutil.Big, len(reward))
+ for j, w := range reward {
+ results.Reward[j] = make([]*hexutil.Big, len(w))
+ for i, v := range w {
+ results.Reward[j][i] = (*hexutil.Big)(v)
+ }
+ }
+ }
+ if baseFee != nil {
+ results.BaseFee = make([]*hexutil.Big, len(baseFee))
+ for i, v := range baseFee {
+ results.BaseFee[i] = (*hexutil.Big)(v)
+ }
+ }
+ return results, nil
+}
+
// Syncing returns false in case the node is currently not syncing with the network. It can be up to date or has not
// yet received the latest block headers from its pears. In case it is synchronizing:
// - startingBlock: block number this node started to synchronise from
diff --git a/internal/ethapi/backend.go b/internal/ethapi/backend.go
index a0e2cf0892..90cfa852af 100644
--- a/internal/ethapi/backend.go
+++ b/internal/ethapi/backend.go
@@ -42,6 +42,7 @@ type Backend interface {
// General Ethereum API
Downloader() *downloader.Downloader
SuggestGasTipCap(ctx context.Context) (*big.Int, error)
+ FeeHistory(ctx context.Context, blockCount int, lastBlock rpc.BlockNumber, rewardPercentiles []float64) (rpc.BlockNumber, [][]*big.Int, []*big.Int, []float64, error)
ChainDb() ethdb.Database
AccountManager() *accounts.Manager
ExtRPCEnabled() bool
diff --git a/internal/web3ext/web3ext.go b/internal/web3ext/web3ext.go
index 8072a3dab9..a7b3266921 100644
--- a/internal/web3ext/web3ext.go
+++ b/internal/web3ext/web3ext.go
@@ -581,6 +581,12 @@ web3._extend({
params: 2,
inputFormatter: [null, web3._extend.formatters.inputBlockNumberFormatter],
}),
+ new web3._extend.Method({
+ name: 'feeHistory',
+ call: 'eth_feeHistory',
+ params: 3,
+ inputFormatter: [null, web3._extend.formatters.inputBlockNumberFormatter, null]
+ }),
],
properties: [
new web3._extend.Property({
diff --git a/les/api_backend.go b/les/api_backend.go
index a6ad7a38e7..2081ef64b1 100644
--- a/les/api_backend.go
+++ b/les/api_backend.go
@@ -60,7 +60,10 @@ func (b *LesApiBackend) SetHead(number uint64) {
}
func (b *LesApiBackend) HeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Header, error) {
- if number == rpc.LatestBlockNumber || number == rpc.PendingBlockNumber {
+ if number == rpc.PendingBlockNumber {
+ return nil, nil
+ }
+ if number == rpc.LatestBlockNumber {
return b.eth.blockchain.CurrentHeader(), nil
}
return b.eth.blockchain.GetHeaderByNumberOdr(ctx, uint64(number))
@@ -122,6 +125,10 @@ func (b *LesApiBackend) BlockByNumberOrHash(ctx context.Context, blockNrOrHash r
return nil, errors.New("invalid arguments; neither block nor hash specified")
}
+func (b *LesApiBackend) PendingBlockAndReceipts() (*types.Block, types.Receipts) {
+ return nil, nil
+}
+
func (b *LesApiBackend) StateAndHeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*state.StateDB, *types.Header, error) {
header, err := b.HeaderByNumber(ctx, number)
if err != nil {
@@ -255,6 +262,10 @@ func (b *LesApiBackend) SuggestGasTipCap(ctx context.Context) (*big.Int, error)
return b.gpo.SuggestTipCap(ctx)
}
+func (b *LesApiBackend) FeeHistory(ctx context.Context, blockCount int, lastBlock rpc.BlockNumber, rewardPercentiles []float64) (firstBlock rpc.BlockNumber, reward [][]*big.Int, baseFee []*big.Int, gasUsedRatio []float64, err error) {
+ return b.gpo.FeeHistory(ctx, blockCount, lastBlock, rewardPercentiles)
+}
+
func (b *LesApiBackend) ChainDb() ethdb.Database {
return b.eth.chainDb
}
diff --git a/miner/miner.go b/miner/miner.go
index 00c3d0cb5c..7143679269 100644
--- a/miner/miner.go
+++ b/miner/miner.go
@@ -194,6 +194,11 @@ func (miner *Miner) PendingBlock() *types.Block {
return miner.worker.pendingBlock()
}
+// PendingBlockAndReceipts returns the currently pending block and corresponding receipts.
+func (miner *Miner) PendingBlockAndReceipts() (*types.Block, types.Receipts) {
+ return miner.worker.pendingBlockAndReceipts()
+}
+
func (miner *Miner) SetEtherbase(addr common.Address) {
miner.coinbase = addr
miner.worker.setEtherbase(addr)
diff --git a/miner/worker.go b/miner/worker.go
index b0b676ad02..c88963827e 100644
--- a/miner/worker.go
+++ b/miner/worker.go
@@ -162,9 +162,10 @@ type worker struct {
pendingMu sync.RWMutex
pendingTasks map[common.Hash]*task
- snapshotMu sync.RWMutex // The lock used to protect the block snapshot and state snapshot
- snapshotBlock *types.Block
- snapshotState *state.StateDB
+ snapshotMu sync.RWMutex // The lock used to protect the block snapshot and state snapshot
+ snapshotBlock *types.Block
+ snapshotReceipts types.Receipts
+ snapshotState *state.StateDB
// atomic status counters
running int32 // The indicator whether the consensus engine is running or not.
@@ -284,6 +285,14 @@ func (w *worker) pendingBlock() *types.Block {
return w.snapshotBlock
}
+// pendingBlockAndReceipts returns pending block and corresponding receipts.
+func (w *worker) pendingBlockAndReceipts() (*types.Block, types.Receipts) {
+ // return a snapshot to avoid contention on currentMu mutex
+ w.snapshotMu.RLock()
+ defer w.snapshotMu.RUnlock()
+ return w.snapshotBlock, w.snapshotReceipts
+}
+
// start sets the running status as 1 and triggers new work submitting.
func (w *worker) start() {
atomic.StoreInt32(&w.running, 1)
@@ -730,6 +739,7 @@ func (w *worker) updateSnapshot() {
w.current.receipts,
trie.NewStackTrie(nil),
)
+ w.snapshotReceipts = w.current.receipts
w.snapshotState = w.current.state.Copy()
}