Files

363 lines
13 KiB
Go

// (c) 2020-2020, Lux Industries, Inc.
//
// This file is a derived work, based on the go-ethereum library whose original
// notices appear below.
//
// It is distributed under a license compatible with the licensing terms of the
// original code from which it is derived.
//
// Much love to the original authors for their work.
// **********
// 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/luxfi/node/v2/utils/timer/mockable"
"github.com/luxfi/evm/core"
"github.com/luxfi/evm/core/types"
"github.com/luxfi/evm/params"
gethparams "github.com/luxfi/geth/params"
customheader "github.com/luxfi/evm/plugin/evm/header"
"github.com/luxfi/evm/upgrade/lp176"
"github.com/luxfi/geth/rpc"
"github.com/luxfi/geth/common"
"github.com/luxfi/geth/common/lru"
"github.com/luxfi/geth/event"
"github.com/luxfi/log"
"golang.org/x/exp/slices"
)
const (
// DefaultMaxCallBlockHistory is the number of blocks that can be fetched in
// a single call to eth_feeHistory.
DefaultMaxCallBlockHistory = 2048
// DefaultMaxBlockHistory is the number of blocks from the last accepted
// block that can be fetched in eth_feeHistory.
//
// DefaultMaxBlockHistory is chosen to be a value larger than the required
// fee lookback window that MetaMask uses (20k blocks).
DefaultMaxBlockHistory = 25_000
// DefaultFeeHistoryCacheSize is chosen to be some value larger than
// [DefaultMaxBlockHistory] to ensure all block lookups can be cached when
// serving a fee history query.
DefaultFeeHistoryCacheSize = 30_000
)
var (
DefaultMaxPrice = big.NewInt(150 * gethparams.GWei)
DefaultMinPrice = big.NewInt(lp176.MinGasPrice)
DefaultMinGasUsed = big.NewInt(lp176.MinTargetPerSecond)
DefaultMaxLookbackSeconds = uint64(80)
)
type Config struct {
// Blocks specifies the number of blocks to fetch during gas price estimation.
Blocks int
// Percentile is a value between 0 and 100 that we use during gas price estimation to choose
// the gas price estimate in which Percentile% of the gas estimate values in the array fall below it
Percentile int
// MaxLookbackSeconds specifies the maximum number of seconds that current timestamp
// can differ from block timestamp in order to be included in gas price estimation
MaxLookbackSeconds uint64
// MaxCallBlockHistory specifies the maximum number of blocks that can be
// fetched in a single eth_feeHistory call.
MaxCallBlockHistory uint64
// MaxBlockHistory specifies the furthest back behind the last accepted block that can
// be requested by fee history.
MaxBlockHistory uint64
MaxPrice *big.Int `toml:",omitempty"`
MinPrice *big.Int `toml:",omitempty"`
MinGasUsed *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)
ChainConfig() *params.ChainConfig
SubscribeChainHeadEvent(ch chan<- core.ChainHeadEvent) event.Subscription
SubscribeChainAcceptedEvent(ch chan<- core.ChainEvent) event.Subscription
MinRequiredTip(ctx context.Context, header *types.Header) (*big.Int, error)
LastAcceptedBlock() *types.Block
}
// 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
// [minPrice] ensures we don't get into a positive feedback loop where tips
// sink to 0 during a period of slow block production, such that nobody's
// transactions will be included until the full block fee duration has
// elapsed.
minPrice *big.Int
maxPrice *big.Int
cacheLock sync.RWMutex
fetchLock sync.Mutex
// clock to decide what set of rules to use when recommending a gas price
clock mockable.Clock
checkBlocks, percentile int
maxLookbackSeconds uint64
maxCallBlockHistory uint64
maxBlockHistory uint64
historyCache *lru.Cache[uint64, *slimBlock]
feeInfoProvider *feeInfoProvider
}
// NewOracle returns a new gasprice oracle which can recommend suitable
// gasprice for newly created transaction.
func NewOracle(backend OracleBackend, config Config) (*Oracle, error) {
blocks := config.Blocks
if blocks < 1 {
blocks = 1
log.Warn("Sanitizing invalid gasprice oracle sample blocks", "provided", config.Blocks, "updated", blocks)
}
percent := config.Percentile
if percent < 0 {
percent = 0
log.Warn("Sanitizing invalid gasprice oracle sample percentile", "provided", config.Percentile, "updated", percent)
} else if percent > 100 {
percent = 100
log.Warn("Sanitizing invalid gasprice oracle sample percentile", "provided", config.Percentile, "updated", percent)
}
maxLookbackSeconds := config.MaxLookbackSeconds
if maxLookbackSeconds <= 0 {
maxLookbackSeconds = DefaultMaxLookbackSeconds
log.Warn("Sanitizing invalid gasprice oracle max block seconds", "provided", config.MaxLookbackSeconds, "updated", maxLookbackSeconds)
}
maxPrice := config.MaxPrice
if maxPrice == nil || maxPrice.Int64() <= 0 {
maxPrice = DefaultMaxPrice
log.Warn("Sanitizing invalid gasprice oracle max price", "provided", config.MaxPrice, "updated", maxPrice)
}
minPrice := config.MinPrice
if minPrice == nil || minPrice.Int64() < 0 {
minPrice = DefaultMinPrice
log.Warn("Sanitizing invalid gasprice oracle min price", "provided", config.MinPrice, "updated", minPrice)
}
minGasUsed := config.MinGasUsed
if minGasUsed == nil || minGasUsed.Int64() < 0 {
minGasUsed = DefaultMinGasUsed
log.Warn("Sanitizing invalid gasprice oracle min gas used", "provided", config.MinGasUsed, "updated", minGasUsed)
}
maxCallBlockHistory := config.MaxCallBlockHistory
if maxCallBlockHistory < 1 {
maxCallBlockHistory = DefaultMaxCallBlockHistory
log.Warn("Sanitizing invalid gasprice oracle max call block history", "provided", config.MaxCallBlockHistory, "updated", maxCallBlockHistory)
}
maxBlockHistory := config.MaxBlockHistory
if maxBlockHistory < 1 {
maxBlockHistory = DefaultMaxBlockHistory
log.Warn("Sanitizing invalid gasprice oracle max block history", "provided", config.MaxBlockHistory, "updated", maxBlockHistory)
}
cache := lru.NewCache[uint64, *slimBlock](DefaultFeeHistoryCacheSize)
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()
}
}()
feeInfoProvider, err := newFeeInfoProvider(backend, minGasUsed.Uint64(), config.Blocks)
if err != nil {
return nil, err
}
return &Oracle{
backend: backend,
lastPrice: minPrice,
minPrice: minPrice,
maxPrice: maxPrice,
checkBlocks: blocks,
percentile: percent,
maxLookbackSeconds: maxLookbackSeconds,
maxCallBlockHistory: maxCallBlockHistory,
maxBlockHistory: maxBlockHistory,
historyCache: cache,
feeInfoProvider: feeInfoProvider,
}, nil
}
// EstimateBaseFee returns an estimate of what the base fee will be on a block
// produced at the current time. If ApricotPhase3 has not been activated, it may
// return a nil value and a nil error.
func (oracle *Oracle) EstimateBaseFee(ctx context.Context) (*big.Int, error) {
return oracle.estimateNextBaseFee(ctx)
}
// estimateNextBaseFee calculates what the base fee should be on the next block if it
// were produced immediately. If the current time is less than the timestamp of the latest
// block, this esimtate uses the timestamp of the latest block instead.
// If the latest block has a nil base fee, this function will return nil as the base fee
// of the next block.
func (oracle *Oracle) estimateNextBaseFee(ctx context.Context) (*big.Int, error) {
// Fetch the most recent header by number
header, err := oracle.backend.HeaderByNumber(ctx, rpc.LatestBlockNumber)
if err != nil {
return nil, err
}
// If the fetched block does not have a base fee, return nil as the base fee
if header.BaseFee == nil {
return nil, nil
}
// If the block does have a baseFee, calculate the next base fee
// based on the current time and add it to the tip to estimate the
// total gas price estimate.
config := oracle.backend.ChainConfig()
extrasConfig := params.GetExtra(config)
// Use the default fee config from the chain config
feeConfig := extrasConfig.FeeConfig
return customheader.EstimateNextBaseFee(extrasConfig, feeConfig, header, oracle.clock.Unix())
}
// SuggestPrice returns an estimated price for legacy transactions.
func (oracle *Oracle) SuggestPrice(ctx context.Context) (*big.Int, error) {
// Estimate the effective tip based on recent blocks.
tip, err := oracle.suggestTip(ctx)
if err != nil {
return nil, err
}
// We calculate the `nextBaseFee` if a block were to be produced immediately.
nextBaseFee, err := oracle.estimateNextBaseFee(ctx)
if err != nil {
log.Warn("failed to estimate next base fee", "err", err)
return nil, err
}
if nextBaseFee == nil {
// This occurs if AP3 has not been scheduled yet
return tip, nil
}
return new(big.Int).Add(tip, nextBaseFee), nil
}
// 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) {
return oracle.suggestTip(ctx)
}
// suggestTip estimates the gas tip based on a simple sampling method
func (oracle *Oracle) suggestTip(ctx context.Context) (*big.Int, error) {
head, err := oracle.backend.HeaderByNumber(ctx, rpc.LatestBlockNumber)
if err != nil {
return nil, err
}
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 (
latestBlockNumber = head.Number.Uint64()
lowerBlockNumberLimit = uint64(0)
currentTime = oracle.clock.Unix()
tipResults []*big.Int
)
if uint64(oracle.checkBlocks) <= latestBlockNumber {
lowerBlockNumberLimit = latestBlockNumber - uint64(oracle.checkBlocks)
}
// Process block headers in the range calculated for this gas price estimation.
for i := latestBlockNumber; i > lowerBlockNumberLimit; i-- {
feeInfo, err := oracle.getFeeInfo(ctx, i)
if err != nil {
return new(big.Int).Set(lastPrice), err
}
if feeInfo.timestamp+oracle.maxLookbackSeconds < currentTime {
break
}
if feeInfo.tip != nil {
tipResults = append(tipResults, feeInfo.tip)
} else {
tipResults = append(tipResults, new(big.Int).Set(common.Big0))
}
}
price := lastPrice
if len(tipResults) > 0 {
slices.SortFunc(tipResults, func(a, b *big.Int) int { return a.Cmp(b) })
price = tipResults[(len(tipResults)-1)*oracle.percentile/100]
}
if price.Cmp(oracle.maxPrice) > 0 {
price = new(big.Int).Set(oracle.maxPrice)
}
if price.Cmp(oracle.minPrice) < 0 {
price = new(big.Int).Set(oracle.minPrice)
}
oracle.cacheLock.Lock()
oracle.lastHead = headHash
oracle.lastPrice = price
oracle.cacheLock.Unlock()
return new(big.Int).Set(price), nil
}
// getFeeInfo calculates the minimum required tip to be included in a given
// block and returns the value as a feeInfo struct.
func (oracle *Oracle) getFeeInfo(ctx context.Context, number uint64) (*feeInfo, error) {
feeInfo, ok := oracle.feeInfoProvider.get(number)
if ok {
return feeInfo, nil
}
// on cache miss, read from database
header, err := oracle.backend.HeaderByNumber(ctx, rpc.BlockNumber(number))
if err != nil {
return nil, err
}
return oracle.feeInfoProvider.addHeader(ctx, header)
}