diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index cc5af5625..fdeb7031c 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 e6810f2a9..37c5d8a09 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 349c8da6c..d971badaf 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 000000000..84ec8e088 --- /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 000000000..191e2f0ce --- /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 f2c4def14..407eeaa28 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 f86449c5a..dea8fea95 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 8cbb01fbb..913cc0b97 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 a0e2cf089..90cfa852a 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 8072a3dab..a7b326692 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 a6ad7a38e..2081ef64b 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 00c3d0cb5..714367926 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 b0b676ad0..c88963827 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() }