diff --git a/cmd/geth/main.go b/cmd/geth/main.go index 19d72e56af..4b408561e5 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -163,7 +163,6 @@ var ( utils.GpoPercentileFlag, utils.GpoMaxGasPriceFlag, utils.GpoIgnoreGasPriceFlag, - utils.GpoCongestionThresholdFlag, utils.MinerNotifyFullFlag, configFileFlag, diff --git a/cmd/geth/usage.go b/cmd/geth/usage.go index 725a01d4d2..fb20e2a4c4 100644 --- a/cmd/geth/usage.go +++ b/cmd/geth/usage.go @@ -208,7 +208,6 @@ var AppHelpFlagGroups = []flags.FlagGroup{ utils.GpoPercentileFlag, utils.GpoMaxGasPriceFlag, utils.GpoIgnoreGasPriceFlag, - utils.GpoCongestionThresholdFlag, }, }, { diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 5390e8da2a..99ac1417a9 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -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)) } diff --git a/eth/gasprice/feehistory.go b/eth/gasprice/feehistory.go index 6b4b2f72fc..9c18c56365 100644 --- a/eth/gasprice/feehistory.go +++ b/eth/gasprice/feehistory.go @@ -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 ( diff --git a/eth/gasprice/gasprice.go b/eth/gasprice/gasprice.go index b0af967ca4..c40b97e058 100644 --- a/eth/gasprice/gasprice.go +++ b/eth/gasprice/gasprice.go @@ -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() diff --git a/eth/gasprice/gasprice_test.go b/eth/gasprice/gasprice_test.go index 68e028fc4a..07c6204532 100644 --- a/eth/gasprice/gasprice_test.go +++ b/eth/gasprice/gasprice_test.go @@ -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) - } - } -} diff --git a/eth/gasprice/scroll_gasprice.go b/eth/gasprice/scroll_gasprice.go new file mode 100644 index 0000000000..2a03ddac02 --- /dev/null +++ b/eth/gasprice/scroll_gasprice.go @@ -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) +} diff --git a/eth/gasprice/scroll_gasprice_test.go b/eth/gasprice/scroll_gasprice_test.go new file mode 100644 index 0000000000..1748f80947 --- /dev/null +++ b/eth/gasprice/scroll_gasprice_test.go @@ -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 . + +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()) + } + }) + }) + }) + } +} diff --git a/params/config.go b/params/config.go index f41eb218ad..b18b69b99c 100644 --- a/params/config.go +++ b/params/config.go @@ -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 { diff --git a/params/version.go b/params/version.go index 5e1732d573..ce0a0c834a 100644 --- a/params/version.go +++ b/params/version.go @@ -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 )