be65b47645
The Go authors updated golang/x/ext to change the function signature of the slices sort method. It's an entire shitshow now because x/ext is not tagged, so everyone's codebase just picked a new version that some other dep depends on, causing our code to fail building. This PR updates the dep on our code too and does all the refactorings to follow upstream...
277 lines
9.0 KiB
Go
277 lines
9.0 KiB
Go
// Copyright 2015 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 <http://www.gnu.org/licenses/>.
|
|
|
|
package gasprice
|
|
|
|
import (
|
|
"context"
|
|
"math/big"
|
|
"sync"
|
|
|
|
"github.com/ethereum/go-ethereum/common"
|
|
"github.com/ethereum/go-ethereum/common/lru"
|
|
"github.com/ethereum/go-ethereum/core"
|
|
"github.com/ethereum/go-ethereum/core/types"
|
|
"github.com/ethereum/go-ethereum/event"
|
|
"github.com/ethereum/go-ethereum/log"
|
|
"github.com/ethereum/go-ethereum/params"
|
|
"github.com/ethereum/go-ethereum/rpc"
|
|
"golang.org/x/exp/slices"
|
|
)
|
|
|
|
const sampleNumber = 3 // Number of transactions sampled in a block
|
|
|
|
var (
|
|
DefaultMaxPrice = big.NewInt(500 * params.GWei)
|
|
DefaultIgnorePrice = big.NewInt(2 * params.Wei)
|
|
)
|
|
|
|
type Config struct {
|
|
Blocks int
|
|
Percentile int
|
|
MaxHeaderHistory uint64
|
|
MaxBlockHistory uint64
|
|
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
|
|
SubscribeChainHeadEvent(ch chan<- core.ChainHeadEvent) event.Subscription
|
|
}
|
|
|
|
// Oracle recommends gas prices based on the content of recent
|
|
// blocks. Suitable for both light and full clients.
|
|
type Oracle struct {
|
|
backend OracleBackend
|
|
lastHead common.Hash
|
|
lastPrice *big.Int
|
|
maxPrice *big.Int
|
|
ignorePrice *big.Int
|
|
cacheLock sync.RWMutex
|
|
fetchLock sync.Mutex
|
|
|
|
checkBlocks, percentile int
|
|
maxHeaderHistory, maxBlockHistory uint64
|
|
|
|
historyCache *lru.Cache[cacheKey, processedFees]
|
|
}
|
|
|
|
// NewOracle returns a new gasprice oracle which can recommend suitable
|
|
// gasprice for newly created transaction.
|
|
func NewOracle(backend OracleBackend, params Config) *Oracle {
|
|
blocks := params.Blocks
|
|
if blocks < 1 {
|
|
blocks = 1
|
|
log.Warn("Sanitizing invalid gasprice oracle sample blocks", "provided", params.Blocks, "updated", blocks)
|
|
}
|
|
percent := params.Percentile
|
|
if percent < 0 {
|
|
percent = 0
|
|
log.Warn("Sanitizing invalid gasprice oracle sample percentile", "provided", params.Percentile, "updated", percent)
|
|
} else if percent > 100 {
|
|
percent = 100
|
|
log.Warn("Sanitizing invalid gasprice oracle sample percentile", "provided", params.Percentile, "updated", percent)
|
|
}
|
|
maxPrice := params.MaxPrice
|
|
if maxPrice == nil || maxPrice.Int64() <= 0 {
|
|
maxPrice = DefaultMaxPrice
|
|
log.Warn("Sanitizing invalid gasprice oracle price cap", "provided", params.MaxPrice, "updated", maxPrice)
|
|
}
|
|
ignorePrice := params.IgnorePrice
|
|
if ignorePrice == nil || ignorePrice.Int64() <= 0 {
|
|
ignorePrice = DefaultIgnorePrice
|
|
log.Warn("Sanitizing invalid gasprice oracle ignore price", "provided", params.IgnorePrice, "updated", ignorePrice)
|
|
} else if ignorePrice.Int64() > 0 {
|
|
log.Info("Gasprice oracle is ignoring threshold set", "threshold", ignorePrice)
|
|
}
|
|
maxHeaderHistory := params.MaxHeaderHistory
|
|
if maxHeaderHistory < 1 {
|
|
maxHeaderHistory = 1
|
|
log.Warn("Sanitizing invalid gasprice oracle max header history", "provided", params.MaxHeaderHistory, "updated", maxHeaderHistory)
|
|
}
|
|
maxBlockHistory := params.MaxBlockHistory
|
|
if maxBlockHistory < 1 {
|
|
maxBlockHistory = 1
|
|
log.Warn("Sanitizing invalid gasprice oracle max block history", "provided", params.MaxBlockHistory, "updated", maxBlockHistory)
|
|
}
|
|
|
|
cache := lru.NewCache[cacheKey, processedFees](2048)
|
|
headEvent := make(chan core.ChainHeadEvent, 1)
|
|
backend.SubscribeChainHeadEvent(headEvent)
|
|
go func() {
|
|
var lastHead common.Hash
|
|
for ev := range headEvent {
|
|
if ev.Block.ParentHash() != lastHead {
|
|
cache.Purge()
|
|
}
|
|
lastHead = ev.Block.Hash()
|
|
}
|
|
}()
|
|
|
|
return &Oracle{
|
|
backend: backend,
|
|
lastPrice: params.Default,
|
|
maxPrice: maxPrice,
|
|
ignorePrice: ignorePrice,
|
|
checkBlocks: blocks,
|
|
percentile: percent,
|
|
maxHeaderHistory: maxHeaderHistory,
|
|
maxBlockHistory: maxBlockHistory,
|
|
historyCache: cache,
|
|
}
|
|
}
|
|
|
|
// SuggestTipCap returns a tip cap so that newly created transaction can have a
|
|
// very high chance to be included in the following blocks.
|
|
//
|
|
// 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 (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.
|
|
oracle.cacheLock.RLock()
|
|
lastHead, lastPrice := oracle.lastHead, oracle.lastPrice
|
|
oracle.cacheLock.RUnlock()
|
|
if headHash == lastHead {
|
|
return new(big.Int).Set(lastPrice), nil
|
|
}
|
|
oracle.fetchLock.Lock()
|
|
defer oracle.fetchLock.Unlock()
|
|
|
|
// Try checking the cache again, maybe the last fetch fetched what we need
|
|
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, oracle.checkBlocks)
|
|
quit = make(chan struct{})
|
|
results []*big.Int
|
|
)
|
|
for sent < oracle.checkBlocks && number > 0 {
|
|
go oracle.getBlockValues(ctx, number, sampleNumber, oracle.ignorePrice, result, quit)
|
|
sent++
|
|
exp++
|
|
number--
|
|
}
|
|
for exp > 0 {
|
|
res := <-result
|
|
if res.err != nil {
|
|
close(quit)
|
|
return new(big.Int).Set(lastPrice), res.err
|
|
}
|
|
exp--
|
|
// Nothing returned. There are two special cases here:
|
|
// - The block is empty
|
|
// - All the transactions included are sent by the miner itself.
|
|
// In these cases, use the latest calculated price for sampling.
|
|
if len(res.values) == 0 {
|
|
res.values = []*big.Int{lastPrice}
|
|
}
|
|
// 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 < oracle.checkBlocks*2 && number > 0 {
|
|
go oracle.getBlockValues(ctx, number, sampleNumber, oracle.ignorePrice, result, quit)
|
|
sent++
|
|
exp++
|
|
number--
|
|
}
|
|
results = append(results, res.values...)
|
|
}
|
|
price := lastPrice
|
|
if len(results) > 0 {
|
|
slices.SortFunc(results, func(a, b *big.Int) int { return a.Cmp(b) })
|
|
price = results[(len(results)-1)*oracle.percentile/100]
|
|
}
|
|
if price.Cmp(oracle.maxPrice) > 0 {
|
|
price = new(big.Int).Set(oracle.maxPrice)
|
|
}
|
|
oracle.cacheLock.Lock()
|
|
oracle.lastHead = headHash
|
|
oracle.lastPrice = price
|
|
oracle.cacheLock.Unlock()
|
|
|
|
return new(big.Int).Set(price), nil
|
|
}
|
|
|
|
type results struct {
|
|
values []*big.Int
|
|
err error
|
|
}
|
|
|
|
// getBlockValues calculates the lowest transaction gas price in a given block
|
|
// 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 (oracle *Oracle) getBlockValues(ctx context.Context, 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}:
|
|
case <-quit:
|
|
}
|
|
return
|
|
}
|
|
signer := types.MakeSigner(oracle.backend.ChainConfig(), block.Number(), block.Time())
|
|
|
|
// Sort the transaction by effective tip in ascending sort.
|
|
txs := block.Transactions()
|
|
sortedTxs := make([]*types.Transaction, len(txs))
|
|
copy(sortedTxs, txs)
|
|
baseFee := block.BaseFee()
|
|
slices.SortFunc(sortedTxs, func(a, b *types.Transaction) int {
|
|
// It's okay to discard the error because a tx would never be
|
|
// accepted into a block with an invalid effective tip.
|
|
tip1, _ := a.EffectiveGasTip(baseFee)
|
|
tip2, _ := b.EffectiveGasTip(baseFee)
|
|
return tip1.Cmp(tip2)
|
|
})
|
|
|
|
var prices []*big.Int
|
|
for _, tx := range sortedTxs {
|
|
tip, _ := tx.EffectiveGasTip(baseFee)
|
|
if ignoreUnder != nil && tip.Cmp(ignoreUnder) == -1 {
|
|
continue
|
|
}
|
|
sender, err := types.Sender(signer, tx)
|
|
if err == nil && sender != block.Coinbase() {
|
|
prices = append(prices, tip)
|
|
if len(prices) >= limit {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
select {
|
|
case result <- results{prices, nil}:
|
|
case <-quit:
|
|
}
|
|
}
|