mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-23 13:16:42 +00:00
feat: suggest gas price based on latest block's transactions (#1220)
* feat: suggest gas price based on latest block's transactions * remove debug logs * rename values * fix: typo * lint * fix: ci test * update comments * chore: auto version bump [bot] * fix: feehistory logic * Update eth/gasprice/feehistory.go Co-authored-by: Jonas Theis <4181434+jonastheis@users.noreply.github.com> * address comments * address comments * chore: remove used flag * fix: ci * fix: test * fix: lint * chore: auto version bump [bot] * test: add scroll gasprice unit test * address comments * address comments * add benchmark * perf: fee_history also use cache * perf: fee_history also use cache * fix: lastIsCongested lock --------- Co-authored-by: Jonas Theis <4181434+jonastheis@users.noreply.github.com>
This commit is contained in:
parent
daff298710
commit
f641f00a02
10 changed files with 583 additions and 139 deletions
|
|
@ -163,7 +163,6 @@ var (
|
|||
utils.GpoPercentileFlag,
|
||||
utils.GpoMaxGasPriceFlag,
|
||||
utils.GpoIgnoreGasPriceFlag,
|
||||
utils.GpoCongestionThresholdFlag,
|
||||
|
||||
utils.MinerNotifyFullFlag,
|
||||
configFileFlag,
|
||||
|
|
|
|||
|
|
@ -208,7 +208,6 @@ var AppHelpFlagGroups = []flags.FlagGroup{
|
|||
utils.GpoPercentileFlag,
|
||||
utils.GpoMaxGasPriceFlag,
|
||||
utils.GpoIgnoreGasPriceFlag,
|
||||
utils.GpoCongestionThresholdFlag,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -749,11 +749,6 @@ var (
|
|||
Usage: "Gas price below which gpo will ignore transactions",
|
||||
Value: ethconfig.Defaults.GPO.IgnorePrice.Int64(),
|
||||
}
|
||||
GpoCongestionThresholdFlag = cli.IntFlag{
|
||||
Name: "gpo.congestionthreshold",
|
||||
Usage: "Number of pending transactions to consider the network congested and suggest a minimum tip cap",
|
||||
Value: ethconfig.Defaults.GPO.CongestedThreshold,
|
||||
}
|
||||
GpoDefaultGasTipCapFlag = cli.Int64Flag{
|
||||
Name: "gpo.defaultgastipcap",
|
||||
Usage: "Default minimum gas tip cap (in wei) to be used after Curie fork (EIP-1559) (default: 100)",
|
||||
|
|
@ -1552,9 +1547,6 @@ func setGPO(ctx *cli.Context, cfg *gasprice.Config, light bool) {
|
|||
if ctx.GlobalIsSet(GpoIgnoreGasPriceFlag.Name) {
|
||||
cfg.IgnorePrice = big.NewInt(ctx.GlobalInt64(GpoIgnoreGasPriceFlag.Name))
|
||||
}
|
||||
if ctx.GlobalIsSet(GpoCongestionThresholdFlag.Name) {
|
||||
cfg.CongestedThreshold = ctx.GlobalInt(GpoCongestionThresholdFlag.Name)
|
||||
}
|
||||
if ctx.GlobalIsSet(GpoDefaultGasTipCapFlag.Name) {
|
||||
cfg.DefaultGasTipCap = big.NewInt(ctx.GlobalInt64(GpoDefaultGasTipCapFlag.Name))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -245,18 +245,14 @@ func (oracle *Oracle) FeeHistory(ctx context.Context, blocks int, unresolvedLast
|
|||
}
|
||||
oldestBlock := lastBlock + 1 - uint64(blocks)
|
||||
|
||||
// If pending txs are less than oracle.congestedThreshold, we consider the network to be non-congested and suggest
|
||||
// a minimal tip cap. This is to prevent users from overpaying for gas when the network is not congested and a few
|
||||
// high-priced txs are causing the suggested tip cap to be high.
|
||||
// If the suggestedGasPrice equals to oracle.defaultGasTipCap or oracle.defaultBasePrice (before Curie (EIP-1559)),
|
||||
// it means the latest block is NOT out of capacity. We consider the network to be non-congested and suggest a minimal
|
||||
// tip cap. This is to prevent users from overpaying for gas when the network is not congested and a few high-priced
|
||||
// transactions are causing the suggested tip cap to be high.
|
||||
var nonCongestedPrice *big.Int
|
||||
pendingTxCount, _ := oracle.backend.StatsWithMinBaseFee(headHeader.BaseFee)
|
||||
if pendingTxCount < oracle.congestedThreshold {
|
||||
// Before Curie (EIP-1559), we need to return the total suggested gas price. After Curie we return defaultGasTipCap wei as the tip cap,
|
||||
// as the base fee is set separately or added manually for legacy transactions.
|
||||
nonCongestedPrice = oracle.defaultGasTipCap
|
||||
if !oracle.backend.ChainConfig().IsCurie(headHeader.Number) {
|
||||
nonCongestedPrice = oracle.defaultBasePrice
|
||||
}
|
||||
suggestedGasPrice, isCongested := oracle.calculateSuggestPriorityFee(ctx, headHeader)
|
||||
if !isCongested {
|
||||
nonCongestedPrice = suggestedGasPrice
|
||||
}
|
||||
|
||||
var (
|
||||
|
|
|
|||
|
|
@ -49,16 +49,15 @@ var (
|
|||
)
|
||||
|
||||
type Config struct {
|
||||
Blocks int
|
||||
Percentile int
|
||||
MaxHeaderHistory int
|
||||
MaxBlockHistory int
|
||||
Default *big.Int `toml:",omitempty"`
|
||||
MaxPrice *big.Int `toml:",omitempty"`
|
||||
IgnorePrice *big.Int `toml:",omitempty"`
|
||||
CongestedThreshold int // Number of pending transactions to consider the network congested and suggest a minimum tip cap.
|
||||
DefaultBasePrice *big.Int `toml:",omitempty"` // Base price to set when CongestedThreshold is reached before Curie (EIP 1559).
|
||||
DefaultGasTipCap *big.Int `toml:",omitempty"` // Default minimum gas tip cap to use after Curie (EIP 1559).
|
||||
Blocks int
|
||||
Percentile int
|
||||
MaxHeaderHistory int
|
||||
MaxBlockHistory int
|
||||
Default *big.Int `toml:",omitempty"`
|
||||
MaxPrice *big.Int `toml:",omitempty"`
|
||||
IgnorePrice *big.Int `toml:",omitempty"`
|
||||
DefaultBasePrice *big.Int `toml:",omitempty"` // Base price to set when CongestedThreshold is reached before Curie (EIP 1559).
|
||||
DefaultGasTipCap *big.Int `toml:",omitempty"` // Default minimum gas tip cap to use after Curie (EIP 1559).
|
||||
}
|
||||
|
||||
// OracleBackend includes all necessary background APIs for oracle.
|
||||
|
|
@ -70,24 +69,22 @@ type OracleBackend interface {
|
|||
ChainConfig() *params.ChainConfig
|
||||
SubscribeChainHeadEvent(ch chan<- core.ChainHeadEvent) event.Subscription
|
||||
StateAt(root common.Hash) (*state.StateDB, error)
|
||||
Stats() (pending int, queued int)
|
||||
StatsWithMinBaseFee(minBaseFee *big.Int) (pending int, queued int)
|
||||
}
|
||||
|
||||
// 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
|
||||
backend OracleBackend
|
||||
lastHead common.Hash
|
||||
lastPrice *big.Int
|
||||
lastIsCongested bool
|
||||
maxPrice *big.Int
|
||||
ignorePrice *big.Int
|
||||
cacheLock sync.RWMutex
|
||||
fetchLock sync.Mutex
|
||||
|
||||
checkBlocks, percentile int
|
||||
maxHeaderHistory, maxBlockHistory int
|
||||
congestedThreshold int // Number of pending transactions to consider the network congested and suggest a minimum tip cap.
|
||||
defaultBasePrice *big.Int // Base price to set when CongestedThreshold is reached before Curie (EIP 1559).
|
||||
defaultGasTipCap *big.Int // Default gas tip cap to suggest after Curie (EIP 1559) when the network is not congested.
|
||||
historyCache *lru.Cache
|
||||
|
|
@ -131,11 +128,6 @@ func NewOracle(backend OracleBackend, params Config) *Oracle {
|
|||
maxBlockHistory = 1
|
||||
log.Warn("Sanitizing invalid gasprice oracle max block history", "provided", params.MaxBlockHistory, "updated", maxBlockHistory)
|
||||
}
|
||||
congestedThreshold := params.CongestedThreshold
|
||||
if congestedThreshold < 0 {
|
||||
congestedThreshold = 0
|
||||
log.Warn("Sanitizing invalid gasprice oracle congested threshold", "provided", params.CongestedThreshold, "updated", congestedThreshold)
|
||||
}
|
||||
defaultBasePrice := params.DefaultBasePrice
|
||||
if defaultBasePrice == nil || defaultBasePrice.Int64() < 0 {
|
||||
defaultBasePrice = DefaultBasePrice
|
||||
|
|
@ -161,18 +153,17 @@ func NewOracle(backend OracleBackend, params Config) *Oracle {
|
|||
}()
|
||||
|
||||
return &Oracle{
|
||||
backend: backend,
|
||||
lastPrice: params.Default,
|
||||
maxPrice: maxPrice,
|
||||
ignorePrice: ignorePrice,
|
||||
checkBlocks: blocks,
|
||||
percentile: percent,
|
||||
maxHeaderHistory: maxHeaderHistory,
|
||||
maxBlockHistory: maxBlockHistory,
|
||||
congestedThreshold: congestedThreshold,
|
||||
defaultBasePrice: defaultBasePrice,
|
||||
defaultGasTipCap: defaultGasTipCap,
|
||||
historyCache: cache,
|
||||
backend: backend,
|
||||
lastPrice: params.Default,
|
||||
maxPrice: maxPrice,
|
||||
ignorePrice: ignorePrice,
|
||||
checkBlocks: blocks,
|
||||
percentile: percent,
|
||||
maxHeaderHistory: maxHeaderHistory,
|
||||
maxBlockHistory: maxBlockHistory,
|
||||
defaultBasePrice: defaultBasePrice,
|
||||
defaultGasTipCap: defaultGasTipCap,
|
||||
historyCache: cache,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -186,6 +177,10 @@ func (oracle *Oracle) SuggestTipCap(ctx context.Context) (*big.Int, error) {
|
|||
head, _ := oracle.backend.HeaderByNumber(ctx, rpc.LatestBlockNumber)
|
||||
headHash := head.Hash()
|
||||
|
||||
if oracle.backend.ChainConfig().IsScroll() {
|
||||
return oracle.SuggestScrollPriorityFee(ctx, head), nil
|
||||
}
|
||||
|
||||
// If the latest gasprice is still available, return it.
|
||||
oracle.cacheLock.RLock()
|
||||
lastHead, lastPrice := oracle.lastHead, oracle.lastPrice
|
||||
|
|
@ -204,26 +199,6 @@ func (oracle *Oracle) SuggestTipCap(ctx context.Context) (*big.Int, error) {
|
|||
return new(big.Int).Set(lastPrice), nil
|
||||
}
|
||||
|
||||
// If pending txs are less than oracle.congestedThreshold, we consider the network to be non-congested and suggest
|
||||
// a minimal tip cap. This is to prevent users from overpaying for gas when the network is not congested and a few
|
||||
// high-priced txs are causing the suggested tip cap to be high.
|
||||
pendingTxCount, _ := oracle.backend.StatsWithMinBaseFee(head.BaseFee)
|
||||
if pendingTxCount < oracle.congestedThreshold {
|
||||
// Before Curie (EIP-1559), we need to return the total suggested gas price. After Curie we return defaultGasTipCap wei as the tip cap,
|
||||
// as the base fee is set separately or added manually for legacy transactions.
|
||||
price := oracle.defaultGasTipCap
|
||||
if !oracle.backend.ChainConfig().IsCurie(head.Number) {
|
||||
price = oracle.defaultBasePrice
|
||||
}
|
||||
|
||||
oracle.cacheLock.Lock()
|
||||
oracle.lastHead = headHash
|
||||
oracle.lastPrice = price
|
||||
oracle.cacheLock.Unlock()
|
||||
|
||||
return new(big.Int).Set(price), nil
|
||||
}
|
||||
|
||||
var (
|
||||
sent, exp int
|
||||
number = head.Number.Uint64()
|
||||
|
|
|
|||
|
|
@ -210,63 +210,3 @@ func TestSuggestTipCap(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSuggestTipCapCongestedThreshold(t *testing.T) {
|
||||
expectedDefaultBasePricePreCurie := big.NewInt(2000)
|
||||
expectedDefaultBasePricePostCurie := big.NewInt(100)
|
||||
|
||||
config := Config{
|
||||
Blocks: 3,
|
||||
Percentile: 60,
|
||||
Default: big.NewInt(params.GWei),
|
||||
CongestedThreshold: 50,
|
||||
DefaultBasePrice: expectedDefaultBasePricePreCurie,
|
||||
}
|
||||
var cases = []struct {
|
||||
fork *big.Int // London fork number
|
||||
pendingTx int // Number of pending transactions in the mempool
|
||||
expect *big.Int // Expected gasprice suggestion
|
||||
}{
|
||||
{nil, 0, expectedDefaultBasePricePreCurie}, // No congestion - default base price
|
||||
{nil, 49, expectedDefaultBasePricePreCurie}, // No congestion - default base price
|
||||
{nil, 50, big.NewInt(params.GWei * int64(30))}, // Congestion - normal behavior
|
||||
{nil, 100, big.NewInt(params.GWei * int64(30))}, // Congestion - normal behavior
|
||||
|
||||
// Fork point in genesis
|
||||
{big.NewInt(0), 0, expectedDefaultBasePricePostCurie}, // No congestion - default base price
|
||||
{big.NewInt(0), 49, expectedDefaultBasePricePostCurie}, // No congestion - default base price
|
||||
{big.NewInt(0), 50, big.NewInt(params.GWei * int64(30))}, // Congestion - normal behavior
|
||||
{big.NewInt(0), 100, big.NewInt(params.GWei * int64(30))}, // Congestion - normal behavior
|
||||
|
||||
// Fork point in first block
|
||||
{big.NewInt(1), 0, expectedDefaultBasePricePostCurie}, // No congestion - default base price
|
||||
{big.NewInt(1), 49, expectedDefaultBasePricePostCurie}, // No congestion - default base price
|
||||
{big.NewInt(1), 50, big.NewInt(params.GWei * int64(30))}, // Congestion - normal behavior
|
||||
{big.NewInt(1), 100, big.NewInt(params.GWei * int64(30))}, // Congestion - normal behavior
|
||||
|
||||
// Fork point in last block
|
||||
{big.NewInt(32), 0, expectedDefaultBasePricePostCurie}, // No congestion - default base price
|
||||
{big.NewInt(32), 49, expectedDefaultBasePricePostCurie}, // No congestion - default base price
|
||||
{big.NewInt(32), 50, big.NewInt(params.GWei * int64(30))}, // Congestion - normal behavior
|
||||
{big.NewInt(32), 100, big.NewInt(params.GWei * int64(30))}, // Congestion - normal behavior
|
||||
|
||||
// Fork point in the future
|
||||
{big.NewInt(33), 0, expectedDefaultBasePricePreCurie}, // No congestion - default base price
|
||||
{big.NewInt(33), 49, expectedDefaultBasePricePreCurie}, // No congestion - default base price
|
||||
{big.NewInt(33), 50, big.NewInt(params.GWei * int64(30))}, // Congestion - normal behavior
|
||||
{big.NewInt(33), 100, big.NewInt(params.GWei * int64(30))}, // Congestion - normal behavior
|
||||
}
|
||||
for _, c := range cases {
|
||||
backend := newTestBackend(t, c.fork, false, c.pendingTx)
|
||||
oracle := NewOracle(backend, config)
|
||||
|
||||
// The gas price sampled is: 32G, 31G, 30G, 29G, 28G, 27G
|
||||
got, err := oracle.SuggestTipCap(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve recommended gas price: %v", err)
|
||||
}
|
||||
if got.Cmp(c.expect) != 0 {
|
||||
t.Fatalf("Gas price mismatch, want %d, got %d", c.expect, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
176
eth/gasprice/scroll_gasprice.go
Normal file
176
eth/gasprice/scroll_gasprice.go
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
package gasprice
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/big"
|
||||
"sort"
|
||||
|
||||
"github.com/scroll-tech/go-ethereum/common"
|
||||
"github.com/scroll-tech/go-ethereum/core/types"
|
||||
"github.com/scroll-tech/go-ethereum/log"
|
||||
"github.com/scroll-tech/go-ethereum/rpc"
|
||||
)
|
||||
|
||||
func (oracle *Oracle) calculateSuggestPriorityFee(ctx context.Context, header *types.Header) (*big.Int, bool) {
|
||||
headHash := header.Hash()
|
||||
// If the latest gasprice is still available, return it.
|
||||
oracle.cacheLock.RLock()
|
||||
lastHead, lastPrice, lastIsCongested := oracle.lastHead, oracle.lastPrice, oracle.lastIsCongested
|
||||
oracle.cacheLock.RUnlock()
|
||||
if headHash == lastHead {
|
||||
return new(big.Int).Set(lastPrice), lastIsCongested
|
||||
}
|
||||
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, lastIsCongested = oracle.lastHead, oracle.lastPrice, oracle.lastIsCongested
|
||||
oracle.cacheLock.RUnlock()
|
||||
if headHash == lastHead {
|
||||
return new(big.Int).Set(lastPrice), lastIsCongested
|
||||
}
|
||||
|
||||
var isCongested bool
|
||||
// Before Curie (EIP-1559), we need to return the total suggested gas price. After Curie we return defaultGasTipCap wei as the tip cap,
|
||||
// as the base fee is set separately or added manually for legacy transactions.
|
||||
suggestion := oracle.defaultGasTipCap
|
||||
if !oracle.backend.ChainConfig().IsCurie(header.Number) {
|
||||
suggestion = oracle.defaultBasePrice
|
||||
}
|
||||
|
||||
// find the maximum gas used by any of the transactions in the block to use as the gas limit
|
||||
// capacity margin
|
||||
receipts, err := oracle.backend.GetReceipts(ctx, header.Hash())
|
||||
if receipts == nil || err != nil {
|
||||
log.Error("failed to get block receipts", "block number", header.Number, "err", err)
|
||||
return suggestion, isCongested
|
||||
}
|
||||
var maxTxGasUsed uint64
|
||||
|
||||
for i := range receipts {
|
||||
gu := receipts[i].GasUsed
|
||||
if gu > maxTxGasUsed {
|
||||
maxTxGasUsed = gu
|
||||
}
|
||||
}
|
||||
|
||||
// find the maximum transaction size by any of the transactions in the block to use as the block
|
||||
// size limit capacity margin
|
||||
var (
|
||||
maxTxSizeUsed common.StorageSize
|
||||
totalTxSizeUsed common.StorageSize
|
||||
)
|
||||
block, err := oracle.backend.BlockByNumber(ctx, rpc.BlockNumber(header.Number.Int64()))
|
||||
if block == nil || err != nil {
|
||||
log.Error("failed to get last block", "err", err)
|
||||
return suggestion, isCongested
|
||||
}
|
||||
txs := block.Transactions()
|
||||
|
||||
for i := range txs {
|
||||
su := txs[i].Size()
|
||||
if su > maxTxSizeUsed {
|
||||
maxTxSizeUsed = su
|
||||
}
|
||||
totalTxSizeUsed = totalTxSizeUsed + su
|
||||
}
|
||||
|
||||
// sanity check the max gas used and transaction size value
|
||||
if maxTxGasUsed > header.GasLimit {
|
||||
log.Error("found tx consuming more gas than the block limit", "gas", maxTxGasUsed)
|
||||
return suggestion, isCongested
|
||||
}
|
||||
if !oracle.backend.ChainConfig().Scroll.IsValidBlockSize(maxTxSizeUsed) {
|
||||
log.Error("found tx consuming more size than the block size limit", "size", maxTxSizeUsed)
|
||||
return suggestion, isCongested
|
||||
}
|
||||
|
||||
if header.GasUsed+maxTxGasUsed > header.GasLimit ||
|
||||
!oracle.backend.ChainConfig().Scroll.IsValidBlockSizeForMining(totalTxSizeUsed+maxTxSizeUsed) {
|
||||
// There are two cases that represent a block is "at capacity":
|
||||
// 1. When building the block, there is a pending transaction in the txpool that could not be
|
||||
// included because adding it would exceed the block's gas limit.
|
||||
// 2. Or, there is a pending transaction that could not be included because adding it would
|
||||
// exceed the block's transaction payload size (block size limit).
|
||||
//
|
||||
// Since we don't have access to the txpool, we instead adopt the following heuristic:
|
||||
// consider a block as at capacity if either:
|
||||
// - the total gas consumed by its transactions is within max-tx-gas-used of the block gas
|
||||
// limit, where max-tx-gas-used is the most gas used by any one transaction within the block, or
|
||||
// - the total transaction payload size is within max-tx-size-used of the block size limit,
|
||||
// where max-tx-size-used is the largest transaction size in the block.
|
||||
//
|
||||
// This heuristic is almost perfectly accurate when transactions always consume the same amount
|
||||
// of gas and have similar sizes, but becomes less accurate as gas usage or payload size varies
|
||||
// between transactions. The typical error is that we assume a block is at capacity when it was
|
||||
// not, because max-tx-gas-used or max-tx-size-used will in most cases over-estimate the
|
||||
// "capacity margin". But it's better to err on the side of returning a higher-than-needed
|
||||
// suggestion than a lower-than-needed one, in order to satisfy our desire for high chance of
|
||||
// inclusion and rising fees under high demand.
|
||||
baseFee := block.BaseFee()
|
||||
if len(txs) == 0 {
|
||||
log.Error("block was at capacity but doesn't have transactions")
|
||||
return suggestion, isCongested
|
||||
}
|
||||
tips := bigIntArray(make([]*big.Int, len(txs)))
|
||||
for i := range txs {
|
||||
tips[i] = txs[i].EffectiveGasTipValue(baseFee)
|
||||
}
|
||||
sort.Sort(tips)
|
||||
median := tips[len(tips)/2]
|
||||
newSuggestion := new(big.Int).Add(median, new(big.Int).Div(median, big.NewInt(10)))
|
||||
isCongested = true
|
||||
// use the new suggestion only if it's bigger than the minimum
|
||||
if newSuggestion.Cmp(suggestion) > 0 {
|
||||
suggestion = newSuggestion
|
||||
}
|
||||
}
|
||||
|
||||
// the suggestion should be capped by oracle.maxPrice
|
||||
if suggestion.Cmp(oracle.maxPrice) > 0 {
|
||||
suggestion.Set(oracle.maxPrice)
|
||||
}
|
||||
|
||||
// update the cache only if it's latest block header
|
||||
latestHeader, _ := oracle.backend.HeaderByNumber(ctx, rpc.LatestBlockNumber)
|
||||
if header.Hash() == latestHeader.Hash() {
|
||||
oracle.cacheLock.Lock()
|
||||
oracle.lastHead = header.Hash()
|
||||
oracle.lastPrice = suggestion
|
||||
oracle.lastIsCongested = isCongested
|
||||
oracle.cacheLock.Unlock()
|
||||
}
|
||||
|
||||
return suggestion, isCongested
|
||||
}
|
||||
|
||||
// SuggestScrollPriorityFee returns a max priority fee value that can be used such that newly
|
||||
// created transactions have a very high chance to be included in the following blocks, using a
|
||||
// simplified and more predictable algorithm appropriate for chains like Scroll with a single
|
||||
// known block builder.
|
||||
//
|
||||
// In the typical case, which results whenever the last block had room for more transactions, this
|
||||
// function returns a minimum suggested priority fee value. Otherwise it returns the higher of this
|
||||
// minimum suggestion or 10% over the median effective priority fee from the last block.
|
||||
//
|
||||
// Rationale: For a chain such as Scroll where there is a single block builder whose behavior is
|
||||
// known, we know priority fee (as long as it is non-zero) has no impact on the probability for tx
|
||||
// inclusion as long as there is capacity for it in the block. In this case then, there's no reason
|
||||
// to return any value higher than some fixed minimum. Blocks typically reach capacity only under
|
||||
// extreme events such as airdrops, meaning predicting whether the next block is going to be at
|
||||
// capacity is difficult *except* in the case where we're already experiencing the increased demand
|
||||
// from such an event. We therefore expect whether the last known block is at capacity to be one of
|
||||
// the best predictors of whether the next block is likely to be at capacity. (An even better
|
||||
// predictor is to look at the state of the transaction pool, but we want an algorithm that works
|
||||
// even if the txpool is private or unavailable.)
|
||||
//
|
||||
// In the event the next block may be at capacity, the algorithm should allow for average fees to
|
||||
// rise in order to reach a market price that appropriately reflects demand. We accomplish this by
|
||||
// returning a suggestion that is a significant amount (10%) higher than the median effective
|
||||
// priority fee from the previous block.
|
||||
func (oracle *Oracle) SuggestScrollPriorityFee(ctx context.Context, header *types.Header) *big.Int {
|
||||
suggestion, _ := oracle.calculateSuggestPriorityFee(ctx, header)
|
||||
|
||||
return new(big.Int).Set(suggestion)
|
||||
}
|
||||
362
eth/gasprice/scroll_gasprice_test.go
Normal file
362
eth/gasprice/scroll_gasprice_test.go
Normal file
|
|
@ -0,0 +1,362 @@
|
|||
// Copyright 2020 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"
|
||||
"math/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/scroll-tech/go-ethereum/common"
|
||||
"github.com/scroll-tech/go-ethereum/core"
|
||||
"github.com/scroll-tech/go-ethereum/core/state"
|
||||
"github.com/scroll-tech/go-ethereum/core/types"
|
||||
"github.com/scroll-tech/go-ethereum/crypto"
|
||||
"github.com/scroll-tech/go-ethereum/event"
|
||||
"github.com/scroll-tech/go-ethereum/params"
|
||||
"github.com/scroll-tech/go-ethereum/rpc"
|
||||
"github.com/scroll-tech/go-ethereum/trie"
|
||||
)
|
||||
|
||||
var (
|
||||
blockGasLimit = params.TxGas * 3
|
||||
maxTxPayloadBytesPerBlock = 790
|
||||
)
|
||||
|
||||
type testTxData struct {
|
||||
priorityFee int64
|
||||
gasLimit uint64
|
||||
payloadSize uint64
|
||||
}
|
||||
|
||||
type scrollTestBackend struct {
|
||||
block *types.Block
|
||||
receipts []*types.Receipt
|
||||
curieBlock *big.Int
|
||||
}
|
||||
|
||||
func (b *scrollTestBackend) HeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Header, error) {
|
||||
if number == rpc.LatestBlockNumber || number == rpc.BlockNumber(b.block.NumberU64()) {
|
||||
return b.block.Header(), nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (b *scrollTestBackend) BlockByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Block, error) {
|
||||
return b.block, nil
|
||||
}
|
||||
|
||||
func (b *scrollTestBackend) GetReceipts(ctx context.Context, hash common.Hash) (types.Receipts, error) {
|
||||
return b.receipts, nil
|
||||
}
|
||||
|
||||
func (b *scrollTestBackend) Pending() (*types.Block, types.Receipts, *state.StateDB) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (b *scrollTestBackend) ChainConfig() *params.ChainConfig {
|
||||
config := params.TestChainConfig
|
||||
config.Scroll.MaxTxPayloadBytesPerBlock = &maxTxPayloadBytesPerBlock
|
||||
config.CurieBlock = b.curieBlock
|
||||
return config
|
||||
}
|
||||
|
||||
func (b *scrollTestBackend) SubscribeChainHeadEvent(ch chan<- core.ChainHeadEvent) event.Subscription {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *scrollTestBackend) PendingBlockAndReceipts() (*types.Block, types.Receipts) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (b *scrollTestBackend) StateAt(root common.Hash) (*state.StateDB, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var _ OracleBackend = (*scrollTestBackend)(nil)
|
||||
|
||||
func GenerateRandomBytes(length int) []byte {
|
||||
b := make([]byte, length)
|
||||
for i := range b {
|
||||
b[i] = byte(rand.Intn(256))
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func newScrollTestBackend(_ *testing.T, txs []testTxData, curieBlock *big.Int) *scrollTestBackend {
|
||||
var (
|
||||
key, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
|
||||
signer = types.LatestSigner(params.TestChainConfig)
|
||||
)
|
||||
// only the most recent block is considered for optimism priority fee suggestions, so this is
|
||||
// where we add the test transactions
|
||||
ts := []*types.Transaction{}
|
||||
rs := []*types.Receipt{}
|
||||
header := types.Header{}
|
||||
header.GasLimit = blockGasLimit
|
||||
var nonce uint64
|
||||
for _, tx := range txs {
|
||||
txdata := &types.DynamicFeeTx{
|
||||
ChainID: params.TestChainConfig.ChainID,
|
||||
Nonce: nonce,
|
||||
To: &common.Address{},
|
||||
Gas: params.TxGas,
|
||||
GasFeeCap: big.NewInt(100 * params.GWei),
|
||||
GasTipCap: big.NewInt(tx.priorityFee),
|
||||
Data: GenerateRandomBytes(int(tx.payloadSize)),
|
||||
}
|
||||
t := types.MustSignNewTx(key, signer, txdata)
|
||||
ts = append(ts, t)
|
||||
r := types.Receipt{}
|
||||
r.GasUsed = tx.gasLimit
|
||||
header.GasUsed += r.GasUsed
|
||||
rs = append(rs, &r)
|
||||
nonce++
|
||||
}
|
||||
hasher := trie.NewStackTrie(nil)
|
||||
b := types.NewBlock(&header, ts, nil, nil, hasher)
|
||||
return &scrollTestBackend{block: b, receipts: rs, curieBlock: curieBlock}
|
||||
}
|
||||
|
||||
func TestSuggestScrollPriorityFee(t *testing.T) {
|
||||
expectedDefaultBasePricePreCurie := big.NewInt(20000)
|
||||
expectedDefaultBasePricePostCurie := big.NewInt(100)
|
||||
cases := []struct {
|
||||
curieBlock *big.Int
|
||||
txdata []testTxData
|
||||
want *big.Int
|
||||
}{
|
||||
|
||||
// Pre-Curie block gas limit test cases
|
||||
{
|
||||
// block gas limit well under capacity, expect min priority fee suggestion
|
||||
curieBlock: big.NewInt(1),
|
||||
txdata: []testTxData{{params.GWei, 21000, 0}},
|
||||
want: expectedDefaultBasePricePreCurie,
|
||||
},
|
||||
{
|
||||
// 2 txs, gas limit still under capacity, expect min priority fee suggestion
|
||||
curieBlock: big.NewInt(1),
|
||||
txdata: []testTxData{{params.GWei, 21000, 0}, {params.GWei, 21000, 0}},
|
||||
want: expectedDefaultBasePricePreCurie,
|
||||
},
|
||||
{
|
||||
// 2 txs w same priority fee (1 gwei), but second tx puts it right over gas limit capacity
|
||||
curieBlock: big.NewInt(1),
|
||||
txdata: []testTxData{{params.GWei, 21000, 0}, {params.GWei, 21001, 0}},
|
||||
want: big.NewInt(1100000000), // 10 percent over 1 gwei, the median
|
||||
},
|
||||
{
|
||||
// 3 txs, full block. return 10% over the median tx (10 gwei * 10% == 11 gwei)
|
||||
curieBlock: big.NewInt(1),
|
||||
txdata: []testTxData{{10 * params.GWei, 21000, 0}, {1 * params.GWei, 21000, 0}, {100 * params.GWei, 21000, 0}},
|
||||
want: big.NewInt(11 * params.GWei),
|
||||
},
|
||||
|
||||
// Pre-Curie block payload size test cases
|
||||
{
|
||||
// block block payload well under capacity, expect min priority fee suggestion
|
||||
curieBlock: big.NewInt(1),
|
||||
txdata: []testTxData{{params.GWei, 0, 139}},
|
||||
want: expectedDefaultBasePricePreCurie,
|
||||
},
|
||||
{
|
||||
// 2 txs, still under block payload capacity, expect min priority fee suggestion
|
||||
curieBlock: big.NewInt(1),
|
||||
txdata: []testTxData{{params.GWei, 0, 139}, {params.GWei, 0, 139}},
|
||||
want: expectedDefaultBasePricePreCurie,
|
||||
},
|
||||
{
|
||||
// 2 txs w same priority fee (1 gwei), but second tx puts it right over block payload capacity
|
||||
curieBlock: big.NewInt(1),
|
||||
txdata: []testTxData{{params.GWei, 0, 139}, {params.GWei, 0, 140}},
|
||||
want: big.NewInt(1100000000), // 10 percent over 1 gwei, the median
|
||||
},
|
||||
{
|
||||
// 3 txs, full block. return 10% over the median tx (10 gwei * 10% == 11 gwei)
|
||||
curieBlock: big.NewInt(1),
|
||||
txdata: []testTxData{{20 * params.GWei, 0, 139}, {1 * params.GWei, 0, 140}, {100 * params.GWei, 0, 140}},
|
||||
want: big.NewInt(22 * params.GWei),
|
||||
},
|
||||
|
||||
// Post Curie block gas limit test cases
|
||||
{
|
||||
// block gas limit well under capacity, expect min priority fee suggestion
|
||||
curieBlock: big.NewInt(0),
|
||||
txdata: []testTxData{{params.GWei, 21000, 0}},
|
||||
want: expectedDefaultBasePricePostCurie,
|
||||
},
|
||||
{
|
||||
// 2 txs, gas limit still under capacity, expect min priority fee suggestion
|
||||
curieBlock: big.NewInt(0),
|
||||
txdata: []testTxData{{params.GWei, 21000, 0}, {params.GWei, 21000, 0}},
|
||||
want: expectedDefaultBasePricePostCurie,
|
||||
},
|
||||
{
|
||||
// 2 txs w same priority fee (1 gwei), but second tx puts it right over gas limit capacity
|
||||
curieBlock: big.NewInt(0),
|
||||
txdata: []testTxData{{params.GWei, 21000, 0}, {params.GWei, 21001, 0}},
|
||||
want: big.NewInt(1100000000), // 10 percent over 1 gwei, the median
|
||||
},
|
||||
{
|
||||
// 3 txs, full block. return 10% over the median tx (10 gwei * 10% == 11 gwei)
|
||||
curieBlock: big.NewInt(0),
|
||||
txdata: []testTxData{{10 * params.GWei, 21000, 0}, {1 * params.GWei, 21000, 0}, {100 * params.GWei, 21000, 0}},
|
||||
want: big.NewInt(11 * params.GWei),
|
||||
},
|
||||
|
||||
// Post Curie block payload size test cases
|
||||
{
|
||||
// block block payload well under capacity, expect min priority fee suggestion
|
||||
curieBlock: big.NewInt(0),
|
||||
txdata: []testTxData{{params.GWei, 0, 139}},
|
||||
want: expectedDefaultBasePricePostCurie,
|
||||
},
|
||||
{
|
||||
// 2 txs, still under block payload capacity, expect min priority fee suggestion
|
||||
curieBlock: big.NewInt(0),
|
||||
txdata: []testTxData{{params.GWei, 0, 139}, {params.GWei, 0, 139}},
|
||||
want: expectedDefaultBasePricePostCurie,
|
||||
},
|
||||
{
|
||||
// 2 txs w same priority fee (1 gwei), but second tx puts it right over block payload capacity
|
||||
curieBlock: big.NewInt(0),
|
||||
txdata: []testTxData{{params.GWei, 0, 139}, {params.GWei, 0, 140}},
|
||||
want: big.NewInt(1100000000), // 10 percent over 1 gwei, the median
|
||||
},
|
||||
{
|
||||
// 3 txs, full block. return 10% over the median tx (10 gwei * 10% == 11 gwei)
|
||||
curieBlock: big.NewInt(0),
|
||||
txdata: []testTxData{{20 * params.GWei, 0, 139}, {1 * params.GWei, 0, 140}, {100 * params.GWei, 0, 140}},
|
||||
want: big.NewInt(22 * params.GWei),
|
||||
},
|
||||
}
|
||||
for i, c := range cases {
|
||||
backend := newScrollTestBackend(t, c.txdata, c.curieBlock)
|
||||
oracle := NewOracle(backend, Config{DefaultBasePrice: expectedDefaultBasePricePreCurie, DefaultGasTipCap: expectedDefaultBasePricePostCurie})
|
||||
got := oracle.SuggestScrollPriorityFee(context.Background(), backend.block.Header())
|
||||
if got.Cmp(c.want) != 0 {
|
||||
t.Errorf("Gas price mismatch for test case %d: want %d, got %d", i, c.want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark API QPS for gas price oracle with different transaction patterns
|
||||
func BenchmarkScrollGasPriceAPIQPS(b *testing.B) {
|
||||
// Create diverse transaction patterns to fill blocks
|
||||
createTransactionSet := func(blockIndex int, txCount int) []testTxData {
|
||||
txs := make([]testTxData, txCount)
|
||||
for i := 0; i < txCount; i++ {
|
||||
// Create varied transactions to avoid cache hits on tx.Size()
|
||||
txs[i] = testTxData{
|
||||
priorityFee: int64((blockIndex*txCount + i + 1) * params.GWei), // Unique priority fees
|
||||
gasLimit: uint64(21000 + (i%10)*1000), // Varied gas limits
|
||||
payloadSize: uint64(i%10) * 256, // Varied payload sizes: 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304 bytes
|
||||
}
|
||||
}
|
||||
return txs
|
||||
}
|
||||
|
||||
testScenarios := []struct {
|
||||
name string
|
||||
curieBlock *big.Int
|
||||
blocksCount int
|
||||
txsPerBlock int
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "LightLoad_PreCurie",
|
||||
curieBlock: big.NewInt(100), // After current block
|
||||
blocksCount: 1,
|
||||
txsPerBlock: 5,
|
||||
description: "Light load with 5 txs, pre-Curie",
|
||||
},
|
||||
{
|
||||
name: "LightLoad_PostCurie",
|
||||
curieBlock: big.NewInt(0), // Curie from genesis
|
||||
blocksCount: 1,
|
||||
txsPerBlock: 5,
|
||||
description: "Light load with 5 txs, post-Curie",
|
||||
},
|
||||
{
|
||||
name: "MediumLoad_PreCurie",
|
||||
curieBlock: big.NewInt(100),
|
||||
blocksCount: 3,
|
||||
txsPerBlock: 20,
|
||||
description: "Medium load with 20 txs per block, 3 blocks",
|
||||
},
|
||||
{
|
||||
name: "MediumLoad_PostCurie",
|
||||
curieBlock: big.NewInt(0),
|
||||
blocksCount: 3,
|
||||
txsPerBlock: 20,
|
||||
description: "Medium load with 20 txs per block, 3 blocks",
|
||||
},
|
||||
{
|
||||
name: "HighLoad_PostCurie",
|
||||
curieBlock: big.NewInt(0),
|
||||
blocksCount: 1,
|
||||
txsPerBlock: 100,
|
||||
description: "High load with 100 txs, testing congestion scenarios",
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range testScenarios {
|
||||
b.Run(scenario.name, func(b *testing.B) {
|
||||
// Create backend with diverse transactions
|
||||
txs := createTransactionSet(0, scenario.txsPerBlock)
|
||||
backend := newScrollTestBackend(nil, txs, scenario.curieBlock)
|
||||
|
||||
config := Config{
|
||||
DefaultBasePrice: big.NewInt(20000),
|
||||
DefaultGasTipCap: big.NewInt(100),
|
||||
MaxPrice: big.NewInt(500 * params.GWei),
|
||||
}
|
||||
oracle := NewOracle(backend, config)
|
||||
ctx := context.Background()
|
||||
|
||||
// Sub-benchmarks for different API methods
|
||||
b.Run("SuggestScrollPriorityFee", func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
_ = oracle.SuggestScrollPriorityFee(ctx, backend.block.Header())
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
b.Run("SuggestTipCap", func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
_, _ = oracle.SuggestTipCap(ctx)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
b.Run("calculateSuggestPriorityFee", func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
_, _ = oracle.calculateSuggestPriorityFee(ctx, backend.block.Header())
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1019,6 +1019,11 @@ func (c *ChainConfig) IsFeynmanTransitionBlock(blockTimestamp uint64, parentTime
|
|||
return isForkedTime(blockTimestamp, c.FeynmanTime) && !isForkedTime(parentTimestamp, c.FeynmanTime)
|
||||
}
|
||||
|
||||
// IsScroll returns whether the node is an scroll node or not.
|
||||
func (c *ChainConfig) IsScroll() bool {
|
||||
return c.Scroll.L1Config != nil
|
||||
}
|
||||
|
||||
// IsTerminalPoWBlock returns whether the given block is the last block of PoW stage.
|
||||
func (c *ChainConfig) IsTerminalPoWBlock(parentTotalDiff *big.Int, totalDiff *big.Int) bool {
|
||||
if c.TerminalTotalDifficulty == nil {
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import (
|
|||
const (
|
||||
VersionMajor = 5 // Major version component of the current release
|
||||
VersionMinor = 8 // Minor version component of the current release
|
||||
VersionPatch = 68 // Patch version component of the current release
|
||||
VersionPatch = 69 // Patch version component of the current release
|
||||
VersionMeta = "mainnet" // Version metadata to append to the version string
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue