diff --git a/eth/backend.go b/eth/backend.go index 064f58c24a..b9185f3348 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -406,7 +406,7 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { if eth.APIBackend.allowUnprotectedTxs { log.Info("Unprotected transactions allowed") } - eth.APIBackend.gpo = gasprice.NewOracle(eth.APIBackend, config.GPO, config.Miner.GasPrice) + eth.APIBackend.gpo = gasprice.NewOracle(eth.APIBackend, config.GPO, config.Miner.GasPrice, config.Miner.MaxDABlockSize) if config.RollupSequencerHTTP != "" { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) diff --git a/eth/gasprice/feehistory_test.go b/eth/gasprice/feehistory_test.go index 3fc942953e..f8ce7614da 100644 --- a/eth/gasprice/feehistory_test.go +++ b/eth/gasprice/feehistory_test.go @@ -68,7 +68,7 @@ func testFeeHistory(t *testing.T, opStack bool) { MaxBlockHistory: c.maxBlock, } backend := newTestBackend(t, big.NewInt(16), big.NewInt(28), c.pending, opStack) - oracle := NewOracle(backend, config, nil) + oracle := NewOracle(backend, config, nil, big.NewInt(1000000)) first, reward, baseFee, ratio, blobBaseFee, blobRatio, err := oracle.FeeHistory(context.Background(), c.count, c.last, c.percent) backend.teardown() diff --git a/eth/gasprice/gasprice.go b/eth/gasprice/gasprice.go index 58a59cbdb9..e9e64bbf38 100644 --- a/eth/gasprice/gasprice.go +++ b/eth/gasprice/gasprice.go @@ -76,6 +76,7 @@ type Oracle struct { checkBlocks, percentile int maxHeaderHistory, maxBlockHistory uint64 + maxDABlockSize *big.Int historyCache *lru.Cache[cacheKey, processedFees] @@ -84,7 +85,7 @@ type Oracle struct { // NewOracle returns a new gasprice oracle which can recommend suitable // gasprice for newly created transaction. -func NewOracle(backend OracleBackend, params Config, startPrice *big.Int) *Oracle { +func NewOracle(backend OracleBackend, params Config, startPrice *big.Int, maxDABlockSize *big.Int) *Oracle { blocks := params.Blocks if blocks < 1 { blocks = 1 @@ -123,6 +124,9 @@ func NewOracle(backend OracleBackend, params Config, startPrice *big.Int) *Oracl if startPrice == nil { startPrice = new(big.Int) } + if maxDABlockSize == nil { + maxDABlockSize = new(big.Int) + } cache := lru.NewCache[cacheKey, processedFees](2048) headEvent := make(chan core.ChainHeadEvent, 1) @@ -153,6 +157,7 @@ func NewOracle(backend OracleBackend, params Config, startPrice *big.Int) *Oracl percentile: percent, maxHeaderHistory: maxHeaderHistory, maxBlockHistory: maxBlockHistory, + maxDABlockSize: maxDABlockSize, historyCache: cache, } diff --git a/eth/gasprice/gasprice_test.go b/eth/gasprice/gasprice_test.go index 1d130c311d..a6aec8f3cc 100644 --- a/eth/gasprice/gasprice_test.go +++ b/eth/gasprice/gasprice_test.go @@ -278,7 +278,7 @@ func TestSuggestTipCap(t *testing.T) { } for _, c := range cases { backend := newTestBackend(t, c.fork, nil, false, false) - oracle := NewOracle(backend, config, big.NewInt(params.GWei)) + oracle := NewOracle(backend, config, big.NewInt(params.GWei), big.NewInt(1000000)) // The gas price sampled is: 32G, 31G, 30G, 29G, 28G, 27G got, err := oracle.SuggestTipCap(context.Background()) diff --git a/eth/gasprice/optimism-gasprice.go b/eth/gasprice/optimism-gasprice.go index 94686f2644..76d627a106 100644 --- a/eth/gasprice/optimism-gasprice.go +++ b/eth/gasprice/optimism-gasprice.go @@ -38,8 +38,8 @@ import ( func (oracle *Oracle) SuggestOptimismPriorityFee(ctx context.Context, h *types.Header, headHash common.Hash) *big.Int { suggestion := new(big.Int).Set(oracle.minSuggestedPriorityFee) - // find the maximum gas used by any of the transactions in the block to use as the capacity - // margin + // 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, headHash) if receipts == nil || err != nil { log.Error("failed to get block receipts", "err", err) @@ -59,19 +59,54 @@ func (oracle *Oracle) SuggestOptimismPriorityFee(ctx context.Context, h *types.H return suggestion } - if h.GasUsed+maxTxGasUsed > h.GasLimit { - // A block is "at capacity" if, when it is built, there is a pending tx in the txpool that - // could not be included because the block's gas limit would be exceeded. Since we don't - // have access to the txpool, we instead adopt the following heuristic: consider a block as - // at capacity if the total gas consumed by its transactions is within max-tx-gas-used of - // the block limit, where max-tx-gas-used is the most gas used by any one transaction - // within the block. This heuristic is almost perfectly accurate when transactions always - // consume the same amount of gas, but becomes less accurate as tx gas consumption begins - // to vary. The typical error is we assume a block is at capacity when it was not because - // max-tx-gas-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. + // find the maximum transaction size by any of the transactions in the block to use as the block + // size limit capacity margin + var ( + maxTxSizeUsed uint64 + totalTxSizeUsed uint64 + ) + block, err := oracle.backend.BlockByNumber(ctx, rpc.BlockNumber(h.Number.Int64())) + if block == nil || err != nil { + log.Error("failed to get last block", "err", err) + return suggestion + } + txs := block.Transactions() + + for i := range txs { + su := txs[i].Size() + if su > maxTxSizeUsed { + maxTxSizeUsed = su + } + totalTxSizeUsed = totalTxSizeUsed + su + } + + if maxTxSizeUsed > oracle.maxDABlockSize.Uint64() { + log.Error("found tx consuming more size than the block size limit", "size", maxTxSizeUsed) + return suggestion + } + + if h.GasUsed+maxTxGasUsed > h.GasLimit || + totalTxSizeUsed+maxTxSizeUsed > oracle.maxDABlockSize.Uint64() { + // 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. block, err := oracle.backend.BlockByNumber(ctx, rpc.BlockNumber(h.Number.Int64())) if block == nil || err != nil { log.Error("failed to get last block", "err", err) diff --git a/eth/gasprice/optimism-gasprice_test.go b/eth/gasprice/optimism-gasprice_test.go index 94599cdd78..024e8362ea 100644 --- a/eth/gasprice/optimism-gasprice_test.go +++ b/eth/gasprice/optimism-gasprice_test.go @@ -136,7 +136,7 @@ func TestSuggestOptimismPriorityFee(t *testing.T) { } for i, c := range cases { backend := newOpTestBackend(t, c.txdata) - oracle := NewOracle(backend, Config{MinSuggestedPriorityFee: minSuggestion}, big.NewInt(params.GWei)) + oracle := NewOracle(backend, Config{MinSuggestedPriorityFee: minSuggestion}, big.NewInt(params.GWei), big.NewInt(1000000)) got := oracle.SuggestOptimismPriorityFee(context.Background(), backend.block.Header(), backend.block.Hash()) if got.Cmp(c.want) != 0 { t.Errorf("Gas price mismatch for test case %d: want %d, got %d", i, c.want, got)