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:
Morty 2025-07-16 15:46:01 +08:00 committed by GitHub
parent daff298710
commit f641f00a02
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 583 additions and 139 deletions

View file

@ -163,7 +163,6 @@ var (
utils.GpoPercentileFlag, utils.GpoPercentileFlag,
utils.GpoMaxGasPriceFlag, utils.GpoMaxGasPriceFlag,
utils.GpoIgnoreGasPriceFlag, utils.GpoIgnoreGasPriceFlag,
utils.GpoCongestionThresholdFlag,
utils.MinerNotifyFullFlag, utils.MinerNotifyFullFlag,
configFileFlag, configFileFlag,

View file

@ -208,7 +208,6 @@ var AppHelpFlagGroups = []flags.FlagGroup{
utils.GpoPercentileFlag, utils.GpoPercentileFlag,
utils.GpoMaxGasPriceFlag, utils.GpoMaxGasPriceFlag,
utils.GpoIgnoreGasPriceFlag, utils.GpoIgnoreGasPriceFlag,
utils.GpoCongestionThresholdFlag,
}, },
}, },
{ {

View file

@ -749,11 +749,6 @@ var (
Usage: "Gas price below which gpo will ignore transactions", Usage: "Gas price below which gpo will ignore transactions",
Value: ethconfig.Defaults.GPO.IgnorePrice.Int64(), 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{ GpoDefaultGasTipCapFlag = cli.Int64Flag{
Name: "gpo.defaultgastipcap", Name: "gpo.defaultgastipcap",
Usage: "Default minimum gas tip cap (in wei) to be used after Curie fork (EIP-1559) (default: 100)", 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) { if ctx.GlobalIsSet(GpoIgnoreGasPriceFlag.Name) {
cfg.IgnorePrice = big.NewInt(ctx.GlobalInt64(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) { if ctx.GlobalIsSet(GpoDefaultGasTipCapFlag.Name) {
cfg.DefaultGasTipCap = big.NewInt(ctx.GlobalInt64(GpoDefaultGasTipCapFlag.Name)) cfg.DefaultGasTipCap = big.NewInt(ctx.GlobalInt64(GpoDefaultGasTipCapFlag.Name))
} }

View file

@ -245,18 +245,14 @@ func (oracle *Oracle) FeeHistory(ctx context.Context, blocks int, unresolvedLast
} }
oldestBlock := lastBlock + 1 - uint64(blocks) oldestBlock := lastBlock + 1 - uint64(blocks)
// If pending txs are less than oracle.congestedThreshold, we consider the network to be non-congested and suggest // If the suggestedGasPrice equals to oracle.defaultGasTipCap or oracle.defaultBasePrice (before Curie (EIP-1559)),
// a minimal tip cap. This is to prevent users from overpaying for gas when the network is not congested and a few // it means the latest block is NOT out of capacity. We consider the network to be non-congested and suggest a minimal
// high-priced txs are causing the suggested tip cap to be high. // 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 var nonCongestedPrice *big.Int
pendingTxCount, _ := oracle.backend.StatsWithMinBaseFee(headHeader.BaseFee) suggestedGasPrice, isCongested := oracle.calculateSuggestPriorityFee(ctx, headHeader)
if pendingTxCount < oracle.congestedThreshold { if !isCongested {
// Before Curie (EIP-1559), we need to return the total suggested gas price. After Curie we return defaultGasTipCap wei as the tip cap, nonCongestedPrice = suggestedGasPrice
// 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
}
} }
var ( var (

View file

@ -49,16 +49,15 @@ var (
) )
type Config struct { type Config struct {
Blocks int Blocks int
Percentile int Percentile int
MaxHeaderHistory int MaxHeaderHistory int
MaxBlockHistory int MaxBlockHistory int
Default *big.Int `toml:",omitempty"` Default *big.Int `toml:",omitempty"`
MaxPrice *big.Int `toml:",omitempty"` MaxPrice *big.Int `toml:",omitempty"`
IgnorePrice *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).
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).
DefaultGasTipCap *big.Int `toml:",omitempty"` // Default minimum gas tip cap to use after Curie (EIP 1559).
} }
// OracleBackend includes all necessary background APIs for oracle. // OracleBackend includes all necessary background APIs for oracle.
@ -70,24 +69,22 @@ type OracleBackend interface {
ChainConfig() *params.ChainConfig ChainConfig() *params.ChainConfig
SubscribeChainHeadEvent(ch chan<- core.ChainHeadEvent) event.Subscription SubscribeChainHeadEvent(ch chan<- core.ChainHeadEvent) event.Subscription
StateAt(root common.Hash) (*state.StateDB, error) 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 // Oracle recommends gas prices based on the content of recent
// blocks. Suitable for both light and full clients. // blocks. Suitable for both light and full clients.
type Oracle struct { type Oracle struct {
backend OracleBackend backend OracleBackend
lastHead common.Hash lastHead common.Hash
lastPrice *big.Int lastPrice *big.Int
maxPrice *big.Int lastIsCongested bool
ignorePrice *big.Int maxPrice *big.Int
cacheLock sync.RWMutex ignorePrice *big.Int
fetchLock sync.Mutex cacheLock sync.RWMutex
fetchLock sync.Mutex
checkBlocks, percentile int checkBlocks, percentile int
maxHeaderHistory, maxBlockHistory 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). 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. defaultGasTipCap *big.Int // Default gas tip cap to suggest after Curie (EIP 1559) when the network is not congested.
historyCache *lru.Cache historyCache *lru.Cache
@ -131,11 +128,6 @@ func NewOracle(backend OracleBackend, params Config) *Oracle {
maxBlockHistory = 1 maxBlockHistory = 1
log.Warn("Sanitizing invalid gasprice oracle max block history", "provided", params.MaxBlockHistory, "updated", maxBlockHistory) 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 defaultBasePrice := params.DefaultBasePrice
if defaultBasePrice == nil || defaultBasePrice.Int64() < 0 { if defaultBasePrice == nil || defaultBasePrice.Int64() < 0 {
defaultBasePrice = DefaultBasePrice defaultBasePrice = DefaultBasePrice
@ -161,18 +153,17 @@ func NewOracle(backend OracleBackend, params Config) *Oracle {
}() }()
return &Oracle{ return &Oracle{
backend: backend, backend: backend,
lastPrice: params.Default, lastPrice: params.Default,
maxPrice: maxPrice, maxPrice: maxPrice,
ignorePrice: ignorePrice, ignorePrice: ignorePrice,
checkBlocks: blocks, checkBlocks: blocks,
percentile: percent, percentile: percent,
maxHeaderHistory: maxHeaderHistory, maxHeaderHistory: maxHeaderHistory,
maxBlockHistory: maxBlockHistory, maxBlockHistory: maxBlockHistory,
congestedThreshold: congestedThreshold, defaultBasePrice: defaultBasePrice,
defaultBasePrice: defaultBasePrice, defaultGasTipCap: defaultGasTipCap,
defaultGasTipCap: defaultGasTipCap, historyCache: cache,
historyCache: cache,
} }
} }
@ -186,6 +177,10 @@ func (oracle *Oracle) SuggestTipCap(ctx context.Context) (*big.Int, error) {
head, _ := oracle.backend.HeaderByNumber(ctx, rpc.LatestBlockNumber) head, _ := oracle.backend.HeaderByNumber(ctx, rpc.LatestBlockNumber)
headHash := head.Hash() headHash := head.Hash()
if oracle.backend.ChainConfig().IsScroll() {
return oracle.SuggestScrollPriorityFee(ctx, head), nil
}
// If the latest gasprice is still available, return it. // If the latest gasprice is still available, return it.
oracle.cacheLock.RLock() oracle.cacheLock.RLock()
lastHead, lastPrice := oracle.lastHead, oracle.lastPrice 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 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 ( var (
sent, exp int sent, exp int
number = head.Number.Uint64() number = head.Number.Uint64()

View file

@ -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)
}
}
}

View 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)
}

View 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())
}
})
})
})
}
}

View file

@ -1019,6 +1019,11 @@ func (c *ChainConfig) IsFeynmanTransitionBlock(blockTimestamp uint64, parentTime
return isForkedTime(blockTimestamp, c.FeynmanTime) && !isForkedTime(parentTimestamp, c.FeynmanTime) 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. // IsTerminalPoWBlock returns whether the given block is the last block of PoW stage.
func (c *ChainConfig) IsTerminalPoWBlock(parentTotalDiff *big.Int, totalDiff *big.Int) bool { func (c *ChainConfig) IsTerminalPoWBlock(parentTotalDiff *big.Int, totalDiff *big.Int) bool {
if c.TerminalTotalDifficulty == nil { if c.TerminalTotalDifficulty == nil {

View file

@ -24,7 +24,7 @@ import (
const ( const (
VersionMajor = 5 // Major version component of the current release VersionMajor = 5 // Major version component of the current release
VersionMinor = 8 // Minor 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 VersionMeta = "mainnet" // Version metadata to append to the version string
) )