From 1d29e3ec0ed0afc7c2cc7ebe2b4b694cc5485b9a Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Fri, 15 Aug 2025 14:07:27 +0200 Subject: [PATCH] consensus/misc/eip4844: use blob parameters of current header (#32424) This changes the implementation to resolve the blob parameters according to the current header timestamp. This matters for EIP-7918, where we would previously resolve the UpdateFraction according to the parent header fork, leading to a confusing situation at the fork transition block. --------- Co-authored-by: MariusVanDerWijden --- consensus/misc/eip4844/eip4844.go | 190 +++++++++++++------------ consensus/misc/eip4844/eip4844_test.go | 65 ++++++++- 2 files changed, 161 insertions(+), 94 deletions(-) diff --git a/consensus/misc/eip4844/eip4844.go b/consensus/misc/eip4844/eip4844.go index fc143027dd..e14d129561 100644 --- a/consensus/misc/eip4844/eip4844.go +++ b/consensus/misc/eip4844/eip4844.go @@ -19,6 +19,7 @@ package eip4844 import ( "errors" "fmt" + "math" "math/big" "github.com/ethereum/go-ethereum/core/types" @@ -29,6 +30,66 @@ var ( minBlobGasPrice = big.NewInt(params.BlobTxMinBlobGasprice) ) +// BlobConfig contains the parameters for blob-related formulas. +// These can be adjusted in a fork. +type BlobConfig struct { + Target int + Max int + UpdateFraction uint64 +} + +func (bc *BlobConfig) maxBlobGas() uint64 { + return uint64(bc.Max) * params.BlobTxBlobGasPerBlob +} + +// blobBaseFee computes the blob fee. +func (bc *BlobConfig) blobBaseFee(excessBlobGas uint64) *big.Int { + return fakeExponential(minBlobGasPrice, new(big.Int).SetUint64(excessBlobGas), new(big.Int).SetUint64(bc.UpdateFraction)) +} + +// blobPrice returns the price of one blob in Wei. +func (bc *BlobConfig) blobPrice(excessBlobGas uint64) *big.Int { + f := bc.blobBaseFee(excessBlobGas) + return new(big.Int).Mul(f, big.NewInt(params.BlobTxBlobGasPerBlob)) +} + +func latestBlobConfig(cfg *params.ChainConfig, time uint64) *BlobConfig { + if cfg.BlobScheduleConfig == nil { + return nil + } + var ( + london = cfg.LondonBlock + s = cfg.BlobScheduleConfig + bc *params.BlobConfig + ) + switch { + case cfg.IsBPO5(london, time) && s.BPO5 != nil: + bc = s.BPO5 + case cfg.IsBPO4(london, time) && s.BPO4 != nil: + bc = s.BPO4 + case cfg.IsBPO3(london, time) && s.BPO3 != nil: + bc = s.BPO3 + case cfg.IsBPO2(london, time) && s.BPO2 != nil: + bc = s.BPO2 + case cfg.IsBPO1(london, time) && s.BPO1 != nil: + bc = s.BPO1 + case cfg.IsOsaka(london, time) && s.Osaka != nil: + bc = s.Osaka + case cfg.IsPrague(london, time) && s.Prague != nil: + bc = s.Prague + case cfg.IsCancun(london, time) && s.Cancun != nil: + bc = s.Cancun + default: + return nil + } + + return &BlobConfig{ + Target: bc.Target, + Max: bc.Max, + UpdateFraction: bc.UpdateFraction, + } +} + // VerifyEIP4844Header verifies the presence of the excessBlobGas field and that // if the current block contains no transactions, the excessBlobGas is updated // accordingly. @@ -36,21 +97,27 @@ func VerifyEIP4844Header(config *params.ChainConfig, parent, header *types.Heade if header.Number.Uint64() != parent.Number.Uint64()+1 { panic("bad header pair") } - // Verify the header is not malformed + + bcfg := latestBlobConfig(config, header.Time) + if bcfg == nil { + panic("called before EIP-4844 is active") + } + if header.ExcessBlobGas == nil { return errors.New("header is missing excessBlobGas") } if header.BlobGasUsed == nil { return errors.New("header is missing blobGasUsed") } + // Verify that the blob gas used remains within reasonable limits. - maxBlobGas := MaxBlobGasPerBlock(config, header.Time) - if *header.BlobGasUsed > maxBlobGas { - return fmt.Errorf("blob gas used %d exceeds maximum allowance %d", *header.BlobGasUsed, maxBlobGas) + if *header.BlobGasUsed > bcfg.maxBlobGas() { + return fmt.Errorf("blob gas used %d exceeds maximum allowance %d", *header.BlobGasUsed, bcfg.maxBlobGas()) } if *header.BlobGasUsed%params.BlobTxBlobGasPerBlob != 0 { return fmt.Errorf("blob gas used %d not a multiple of blob gas per blob %d", header.BlobGasUsed, params.BlobTxBlobGasPerBlob) } + // Verify the excessBlobGas is correct based on the parent header expectedExcessBlobGas := CalcExcessBlobGas(config, parent, header.Time) if *header.ExcessBlobGas != expectedExcessBlobGas { @@ -62,38 +129,41 @@ func VerifyEIP4844Header(config *params.ChainConfig, parent, header *types.Heade // CalcExcessBlobGas calculates the excess blob gas after applying the set of // blobs on top of the excess blob gas. func CalcExcessBlobGas(config *params.ChainConfig, parent *types.Header, headTimestamp uint64) uint64 { - var ( - parentExcessBlobGas uint64 - parentBlobGasUsed uint64 - ) + isOsaka := config.IsOsaka(config.LondonBlock, headTimestamp) + bcfg := latestBlobConfig(config, headTimestamp) + return calcExcessBlobGas(isOsaka, bcfg, parent) +} + +func calcExcessBlobGas(isOsaka bool, bcfg *BlobConfig, parent *types.Header) uint64 { + var parentExcessBlobGas, parentBlobGasUsed uint64 if parent.ExcessBlobGas != nil { parentExcessBlobGas = *parent.ExcessBlobGas parentBlobGasUsed = *parent.BlobGasUsed } + var ( excessBlobGas = parentExcessBlobGas + parentBlobGasUsed - target = targetBlobsPerBlock(config, headTimestamp) - targetGas = uint64(target) * params.BlobTxBlobGasPerBlob + targetGas = uint64(bcfg.Target) * params.BlobTxBlobGasPerBlob ) if excessBlobGas < targetGas { return 0 } - if !config.IsOsaka(config.LondonBlock, headTimestamp) { - // Pre-Osaka, we use the formula defined by EIP-4844. - return excessBlobGas - targetGas + + // EIP-7918 (post-Osaka) introduces a different formula for computing excess, + // in cases where the price is lower than a 'reserve price'. + if isOsaka { + var ( + baseCost = big.NewInt(params.BlobBaseCost) + reservePrice = baseCost.Mul(baseCost, parent.BaseFee) + blobPrice = bcfg.blobPrice(parentExcessBlobGas) + ) + if reservePrice.Cmp(blobPrice) > 0 { + scaledExcess := parentBlobGasUsed * uint64(bcfg.Max-bcfg.Target) / uint64(bcfg.Max) + return parentExcessBlobGas + scaledExcess + } } - // EIP-7918 (post-Osaka) introduces a different formula for computing excess. - var ( - baseCost = big.NewInt(params.BlobBaseCost) - reservePrice = baseCost.Mul(baseCost, parent.BaseFee) - blobPrice = calcBlobPrice(config, parent) - ) - if reservePrice.Cmp(blobPrice) > 0 { - max := MaxBlobsPerBlock(config, headTimestamp) - scaledExcess := parentBlobGasUsed * uint64(max-target) / uint64(max) - return parentExcessBlobGas + scaledExcess - } + // Original EIP-4844 formula. return excessBlobGas - targetGas } @@ -103,7 +173,7 @@ func CalcBlobFee(config *params.ChainConfig, header *types.Header) *big.Int { if blobConfig == nil { panic("calculating blob fee on unsupported fork") } - return fakeExponential(minBlobGasPrice, new(big.Int).SetUint64(*header.ExcessBlobGas), new(big.Int).SetUint64(blobConfig.UpdateFraction)) + return blobConfig.blobBaseFee(*header.ExcessBlobGas) } // MaxBlobsPerBlock returns the max blobs per block for a block at the given timestamp. @@ -115,36 +185,6 @@ func MaxBlobsPerBlock(cfg *params.ChainConfig, time uint64) int { return blobConfig.Max } -func latestBlobConfig(cfg *params.ChainConfig, time uint64) *params.BlobConfig { - if cfg.BlobScheduleConfig == nil { - return nil - } - var ( - london = cfg.LondonBlock - s = cfg.BlobScheduleConfig - ) - switch { - case cfg.IsBPO5(london, time) && s.BPO5 != nil: - return s.BPO5 - case cfg.IsBPO4(london, time) && s.BPO4 != nil: - return s.BPO4 - case cfg.IsBPO3(london, time) && s.BPO3 != nil: - return s.BPO3 - case cfg.IsBPO2(london, time) && s.BPO2 != nil: - return s.BPO2 - case cfg.IsBPO1(london, time) && s.BPO1 != nil: - return s.BPO1 - case cfg.IsOsaka(london, time) && s.Osaka != nil: - return s.Osaka - case cfg.IsPrague(london, time) && s.Prague != nil: - return s.Prague - case cfg.IsCancun(london, time) && s.Cancun != nil: - return s.Cancun - default: - return nil - } -} - // MaxBlobGasPerBlock returns the maximum blob gas that can be spent in a block at the given timestamp. func MaxBlobGasPerBlock(cfg *params.ChainConfig, time uint64) uint64 { return uint64(MaxBlobsPerBlock(cfg, time)) * params.BlobTxBlobGasPerBlob @@ -153,39 +193,11 @@ func MaxBlobGasPerBlock(cfg *params.ChainConfig, time uint64) uint64 { // LatestMaxBlobsPerBlock returns the latest max blobs per block defined by the // configuration, regardless of the currently active fork. func LatestMaxBlobsPerBlock(cfg *params.ChainConfig) int { - s := cfg.BlobScheduleConfig - if s == nil { + bcfg := latestBlobConfig(cfg, math.MaxUint64) + if bcfg == nil { return 0 } - switch { - case s.BPO5 != nil: - return s.BPO5.Max - case s.BPO4 != nil: - return s.BPO4.Max - case s.BPO3 != nil: - return s.BPO3.Max - case s.BPO2 != nil: - return s.BPO2.Max - case s.BPO1 != nil: - return s.BPO1.Max - case s.Osaka != nil: - return s.Osaka.Max - case s.Prague != nil: - return s.Prague.Max - case s.Cancun != nil: - return s.Cancun.Max - default: - return 0 - } -} - -// targetBlobsPerBlock returns the target number of blobs in a block at the given timestamp. -func targetBlobsPerBlock(cfg *params.ChainConfig, time uint64) int { - blobConfig := latestBlobConfig(cfg, time) - if blobConfig == nil { - return 0 - } - return blobConfig.Target + return bcfg.Max } // fakeExponential approximates factor * e ** (numerator / denominator) using @@ -204,9 +216,3 @@ func fakeExponential(factor, numerator, denominator *big.Int) *big.Int { } return output.Div(output, denominator) } - -// calcBlobPrice calculates the blob price for a block. -func calcBlobPrice(config *params.ChainConfig, header *types.Header) *big.Int { - blobBaseFee := CalcBlobFee(config, header) - return new(big.Int).Mul(blobBaseFee, big.NewInt(params.BlobTxBlobGasPerBlob)) -} diff --git a/consensus/misc/eip4844/eip4844_test.go b/consensus/misc/eip4844/eip4844_test.go index 555324db65..35934370af 100644 --- a/consensus/misc/eip4844/eip4844_test.go +++ b/consensus/misc/eip4844/eip4844_test.go @@ -28,9 +28,10 @@ import ( func TestCalcExcessBlobGas(t *testing.T) { var ( config = params.MainnetChainConfig - targetBlobs = targetBlobsPerBlock(config, *config.CancunTime) + targetBlobs = config.BlobScheduleConfig.Cancun.Target targetBlobGas = uint64(targetBlobs) * params.BlobTxBlobGasPerBlob ) + var tests = []struct { excess uint64 blobs int @@ -90,6 +91,65 @@ func TestCalcBlobFee(t *testing.T) { } } +func TestCalcBlobFeePostOsaka(t *testing.T) { + zero := uint64(0) + bpo1 := uint64(1754836608) + bpo2 := uint64(1754934912) + bpo3 := uint64(1755033216) + + tests := []struct { + excessBlobGas uint64 + blobGasUsed uint64 + blobfee uint64 + basefee uint64 + parenttime uint64 + headertime uint64 + }{ + {5149252, 1310720, 5617366, 30, 1754904516, 1754904528}, + {19251039, 2490368, 20107103, 50, 1755033204, 1755033216}, + } + for i, tt := range tests { + config := ¶ms.ChainConfig{ + LondonBlock: big.NewInt(0), + CancunTime: &zero, + PragueTime: &zero, + OsakaTime: &zero, + BPO1Time: &bpo1, + BPO2Time: &bpo2, + BPO3Time: &bpo3, + BlobScheduleConfig: ¶ms.BlobScheduleConfig{ + Cancun: params.DefaultCancunBlobConfig, + Prague: params.DefaultPragueBlobConfig, + Osaka: params.DefaultOsakaBlobConfig, + BPO1: ¶ms.BlobConfig{ + Target: 9, + Max: 14, + UpdateFraction: 8832827, + }, + BPO2: ¶ms.BlobConfig{ + Target: 14, + Max: 21, + UpdateFraction: 13739630, + }, + BPO3: ¶ms.BlobConfig{ + Target: 21, + Max: 32, + UpdateFraction: 20609697, + }, + }} + parent := &types.Header{ + ExcessBlobGas: &tt.excessBlobGas, + BlobGasUsed: &tt.blobGasUsed, + BaseFee: big.NewInt(int64(tt.basefee)), + Time: tt.parenttime, + } + have := CalcExcessBlobGas(config, parent, tt.headertime) + if have != tt.blobfee { + t.Errorf("test %d: blobfee mismatch: have %v want %v", i, have, tt.blobfee) + } + } +} + func TestFakeExponential(t *testing.T) { tests := []struct { factor int64 @@ -131,9 +191,10 @@ func TestFakeExponential(t *testing.T) { func TestCalcExcessBlobGasEIP7918(t *testing.T) { var ( cfg = params.MergedTestChainConfig - targetBlobs = targetBlobsPerBlock(cfg, *cfg.CancunTime) + targetBlobs = cfg.BlobScheduleConfig.Osaka.Target blobGasTarget = uint64(targetBlobs) * params.BlobTxBlobGasPerBlob ) + makeHeader := func(parentExcess, parentBaseFee uint64, blobsUsed int) *types.Header { blobGasUsed := uint64(blobsUsed) * params.BlobTxBlobGasPerBlob return &types.Header{