feat(feynman): L2 fee calculation (#1199)

* Update L2 base fee calculation to account for Feynman

* Update consensus/misc/eip1559.go

Co-authored-by: Péter Garamvölgyi <peter@scroll.io>

* Use dedicated system parameter provingBaseFee

* Merge and change initialBaseFee

* Check on parent not to fail tests

* fix test

* fix golint

* fix goimport

* fix unit tests

* check config.IsFeynman(parent.Time)

* Update consensus/misc/eip1559.go

* fix fee transition

* remove an incorrect comment

* reuse baseFeeOverhead as proving base fee

* update da-codec commit

* address comments

* add a comment

* add base fee max check

* add upstream 1559 tests

* bump version

---------

Co-authored-by: Péter Garamvölgyi <peter@scroll.io>
Co-authored-by: colin <102356659+colinlyguo@users.noreply.github.com>
Co-authored-by: colinlyguo <colinlyguo@scroll.io>
This commit is contained in:
Alejandro Ranchal-Pedrosa 2025-06-26 14:07:15 +02:00 committed by GitHub
parent 27e6e9e168
commit c604c90159
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 134 additions and 20 deletions

View file

@ -102,14 +102,88 @@ func VerifyEip1559Header(config *params.ChainConfig, parent, header *types.Heade
}
// CalcBaseFee calculates the basefee of the header.
func CalcBaseFee(config *params.ChainConfig, parent *types.Header, parentL1BaseFee *big.Int) *big.Int {
func CalcBaseFee(config *params.ChainConfig, parent *types.Header, parentL1BaseFee *big.Int, currentHeaderTime uint64) *big.Int {
if config.Clique != nil && config.Clique.ShadowForkHeight != 0 && parent.Number.Uint64() >= config.Clique.ShadowForkHeight {
return big.NewInt(10000000) // 0.01 Gwei
}
scalar, overhead := ReadL2BaseFeeCoefficients()
if parent == nil || parent.Number == nil || !config.IsFeynman(currentHeaderTime) {
return calcBaseFee(scalar, overhead, parentL1BaseFee)
}
// In Feynman base fee calculation, we reuse the contract's baseFeeOverhead slot as the proving base fee.
return calcBaseFeeFeynman(config, parent, overhead)
}
// calcBaseFeeFeynman calculates the basefee of the header for Feynman fork.
func calcBaseFeeFeynman(config *params.ChainConfig, parent *types.Header, overhead *big.Int) *big.Int {
baseFeeEIP1559 := calcBaseFeeEIP1559(config, parent)
baseFee := new(big.Int).Set(baseFeeEIP1559)
baseFee.Add(baseFee, overhead)
return baseFee
}
// CalcBaseFee calculates the basefee of the header.
func calcBaseFeeEIP1559(config *params.ChainConfig, parent *types.Header) *big.Int {
// If the current block is the first EIP-1559 block, return the InitialBaseFee.
if !config.IsFeynman(parent.Time) {
// If the parent block is not nil, return its base fee to make fee transition smooth.
if parent.BaseFee != nil {
return new(big.Int).Set(parent.BaseFee)
}
return new(big.Int).SetUint64(params.InitialBaseFee)
}
parentBaseFeeEIP1559 := extractBaseFeeEIP1559(config, parent.BaseFee)
parentGasTarget := parent.GasLimit / config.ElasticityMultiplier()
// If the parent gasUsed is the same as the target, the baseFee remains unchanged.
if parent.GasUsed == parentGasTarget {
return new(big.Int).Set(parentBaseFeeEIP1559)
}
var (
num = new(big.Int)
denom = new(big.Int)
)
if parent.GasUsed > parentGasTarget {
// If the parent block used more gas than its target, the baseFee should increase.
// max(1, parentBaseFee * gasUsedDelta / parentGasTarget / baseFeeChangeDenominator)
num.SetUint64(parent.GasUsed - parentGasTarget)
num.Mul(num, parentBaseFeeEIP1559)
num.Div(num, denom.SetUint64(parentGasTarget))
num.Div(num, denom.SetUint64(config.BaseFeeChangeDenominator()))
if num.Cmp(common.Big1) < 0 {
return num.Add(parentBaseFeeEIP1559, common.Big1)
}
baseFee := num.Add(parentBaseFeeEIP1559, num)
if baseFee.Cmp(big.NewInt(MaximumL2BaseFee)) > 0 {
baseFee = big.NewInt(MaximumL2BaseFee)
}
return baseFee
} else {
// Otherwise if the parent block used less gas than its target, the baseFee should decrease.
// max(0, parentBaseFee * gasUsedDelta / parentGasTarget / baseFeeChangeDenominator)
num.SetUint64(parentGasTarget - parent.GasUsed)
num.Mul(num, parentBaseFeeEIP1559)
num.Div(num, denom.SetUint64(parentGasTarget))
num.Div(num, denom.SetUint64(config.BaseFeeChangeDenominator()))
baseFee := num.Sub(parentBaseFeeEIP1559, num)
if baseFee.Cmp(common.Big0) < 0 {
baseFee = common.Big0
}
return baseFee
}
}
func extractBaseFeeEIP1559(config *params.ChainConfig, baseFee *big.Int) *big.Int {
_, overhead := ReadL2BaseFeeCoefficients()
// In Feynman base fee calculation, we reuse the contract's baseFeeOverhead slot as the proving base fee.
return new(big.Int).Sub(baseFee, overhead)
}
// MinBaseFee calculates the minimum L2 base fee based on the current coefficients.
func MinBaseFee() *big.Int {

View file

@ -20,6 +20,7 @@ import (
"math/big"
"testing"
"github.com/scroll-tech/go-ethereum/common"
"github.com/scroll-tech/go-ethereum/core/types"
"github.com/scroll-tech/go-ethereum/params"
)
@ -124,7 +125,33 @@ func TestCalcBaseFee(t *testing.T) {
config := config()
UpdateL2BaseFeeScalar(big.NewInt(10000000))
UpdateL2BaseFeeOverhead(big.NewInt(1))
if have, want := CalcBaseFee(config, nil, big.NewInt(test.parentL1BaseFee)), big.NewInt(test.expectedL2BaseFee); have.Cmp(want) != 0 {
if have, want := CalcBaseFee(config, nil, big.NewInt(test.parentL1BaseFee), 0), big.NewInt(test.expectedL2BaseFee); have.Cmp(want) != 0 {
t.Errorf("test %d: have %d want %d, ", i, have, want)
}
}
tests1559 := []struct {
parentBaseFee int64
parentGasLimit uint64
parentGasUsed uint64
expectedBaseFee int64
}{
{1000000000, 20000000, 10000000, 1000000000}, // usage == target
{1000000001, 20000000, 9000000, 987500001}, // usage below target
{1000000001, 20000000, 11000000, 1012500001}, // usage above target
}
for i, test := range tests1559 {
parent := &types.Header{
Number: common.Big32,
GasLimit: test.parentGasLimit,
GasUsed: test.parentGasUsed,
BaseFee: big.NewInt(test.parentBaseFee),
}
config := config()
UpdateL2BaseFeeOverhead(big.NewInt(1))
var feynmanTime uint64
config.FeynmanTime = &feynmanTime
if have, want := CalcBaseFee(config, parent, big.NewInt(1), 1), big.NewInt(test.expectedBaseFee); have.Cmp(want) != 0 {
t.Errorf("test %d: have %d want %d, ", i, have, want)
}
}
@ -144,7 +171,7 @@ func TestCalcBaseFee(t *testing.T) {
for i, test := range testsWithDefaults {
UpdateL2BaseFeeScalar(big.NewInt(34000000000000))
UpdateL2BaseFeeOverhead(big.NewInt(15680000))
if have, want := CalcBaseFee(config(), nil, big.NewInt(test.parentL1BaseFee)), big.NewInt(test.expectedL2BaseFee); have.Cmp(want) != 0 {
if have, want := CalcBaseFee(config(), nil, big.NewInt(test.parentL1BaseFee), 0), big.NewInt(test.expectedL2BaseFee); have.Cmp(want) != 0 {
t.Errorf("test %d: have %d want %d, ", i, have, want)
}
}

View file

@ -308,7 +308,7 @@ func makeHeader(chain consensus.ChainReader, parent *types.Block, state *state.S
}
if chain.Config().IsCurie(header.Number) {
parentL1BaseFee := fees.GetL1BaseFee(state)
header.BaseFee = misc.CalcBaseFee(chain.Config(), parent.Header(), parentL1BaseFee)
header.BaseFee = misc.CalcBaseFee(chain.Config(), parent.Header(), parentL1BaseFee, header.Time)
}
return header
}

View file

@ -317,7 +317,7 @@ func (g *Genesis) ToBlock(db ethdb.Database) *types.Block {
if g.BaseFee != nil {
head.BaseFee = g.BaseFee
} else {
head.BaseFee = misc.CalcBaseFee(g.Config, nil, big.NewInt(0))
head.BaseFee = misc.CalcBaseFee(g.Config, nil, big.NewInt(0), head.Time)
}
}
statedb.Commit(false)

View file

@ -241,7 +241,7 @@ func TestStateProcessorErrors(t *testing.T) {
txs: []*types.Transaction{
mkSetCodeTx(0, common.Address{}, params.TxGas, big.NewInt(params.InitialBaseFee), big.NewInt(params.InitialBaseFee), nil),
},
want: "could not apply tx 0 [0xc18d10f4c809dbdfa1a074c3300de9bc4b7f16a20f0ec667f6f67312b71b956a]: EIP-7702 transaction with empty auth list (sender 0x71562b71999873DB5b286dF957af199Ec94617F7)",
want: "could not apply tx 0 [0xa230ea82ab24a8e60aca9edfe901bab53c92d0d8850f13c3f72513608229e2f3]: EIP-7702 transaction with empty auth list (sender 0x71562b71999873DB5b286dF957af199Ec94617F7)",
},
// ErrSetCodeTxCreate cannot be tested here: it is impossible to create a SetCode-tx with nil `to`.
// The EstimateGas API tests test this case.
@ -391,13 +391,13 @@ func TestStateProcessorErrors(t *testing.T) {
}{
{ // ErrMaxInitCodeSizeExceeded
txs: []*types.Transaction{
mkDynamicCreationTx(0, 520000, common.Big0, misc.CalcBaseFee(config, genesis.Header(), parentL1BaseFee), tooBigInitCode[:]),
mkDynamicCreationTx(0, 520000, common.Big0, misc.CalcBaseFee(config, genesis.Header(), parentL1BaseFee, 0), tooBigInitCode[:]),
},
want: "could not apply tx 0 [0xe0d03426cecc04467410064cb4de02012fc069d2462282735d7dfcb9dea9f63b]: max initcode size exceeded: code size 49153 limit 49152",
},
{ // ErrIntrinsicGas: Not enough gas to cover init code
txs: []*types.Transaction{
mkDynamicCreationTx(0, 54299, common.Big0, misc.CalcBaseFee(config, genesis.Header(), parentL1BaseFee), smallInitCode[:]),
mkDynamicCreationTx(0, 54299, common.Big0, misc.CalcBaseFee(config, genesis.Header(), parentL1BaseFee, 0), smallInitCode[:]),
},
want: "could not apply tx 0 [0x98e54c5ecfa7986a66480d65ba32f2c6a2a6aedc3a67abb91b1e118b0717ed2d]: intrinsic gas too low: have 54299, want 54300",
},
@ -436,7 +436,7 @@ func GenerateBadBlock(parent *types.Block, engine consensus.Engine, txs types.Tr
if config.IsCurie(header.Number) {
parentL1BaseFee := big.NewInt(1000000000) // 1 gwei
header.BaseFee = misc.CalcBaseFee(config, parent.Header(), parentL1BaseFee)
header.BaseFee = misc.CalcBaseFee(config, parent.Header(), parentL1BaseFee, header.Time)
}
var receipts []*types.Receipt
// The post-state result doesn't need to be correct (this is a bad block), but we do need something there

View file

@ -1459,7 +1459,8 @@ func (pool *TxPool) runReorg(done chan struct{}, reset *txpoolResetRequest, dirt
pool.demoteUnexecutables()
if reset.newHead != nil && pool.chainconfig.IsCurie(new(big.Int).Add(reset.newHead.Number, big.NewInt(1))) {
l1BaseFee := fees.GetL1BaseFee(pool.currentState)
pendingBaseFee := misc.CalcBaseFee(pool.chainconfig, reset.newHead, l1BaseFee)
// Use time.Now() as the current block time to calculate the pending base fee.
pendingBaseFee := misc.CalcBaseFee(pool.chainconfig, reset.newHead, l1BaseFee, uint64(time.Now().Unix()))
pool.priced.SetBaseFee(pendingBaseFee)
}
// Update all accounts to the latest known pending nonce

View file

@ -148,7 +148,7 @@ func (api *consensusAPI) AssembleBlock(params assembleBlockParams) (*executableD
return nil, err
}
parentL1BaseFee := fees.GetL1BaseFee(stateDb)
header.BaseFee = misc.CalcBaseFee(config, parent.Header(), parentL1BaseFee)
header.BaseFee = misc.CalcBaseFee(config, parent.Header(), parentL1BaseFee, header.Time)
}
err = api.eth.Engine().Prepare(bc, header, nil)
if err != nil {
@ -274,7 +274,7 @@ func insertBlockParamsToBlock(config *chainParams.ChainConfig, parent *types.Hea
Time: params.Timestamp,
}
if config.IsCurie(number) {
header.BaseFee = misc.CalcBaseFee(config, parent, parentL1BaseFee)
header.BaseFee = misc.CalcBaseFee(config, parent, parentL1BaseFee, header.Time)
}
block := types.NewBlockWithHeader(header).WithBody(txs, nil /* uncles */)
return block, nil

View file

@ -96,7 +96,8 @@ func (oracle *Oracle) processBlock(bf *blockFees, percentiles []float64, nonCong
return
}
l1BaseFee := fees.GetL1BaseFee(state)
bf.results.nextBaseFee = misc.CalcBaseFee(chainconfig, bf.header, l1BaseFee)
// Use bf.header.Time as the current block time to calculate the next base fee because there are cases to query historical block fees
bf.results.nextBaseFee = misc.CalcBaseFee(chainconfig, bf.header, l1BaseFee, bf.header.Time)
} else {
bf.results.nextBaseFee = new(big.Int)
}

View file

@ -1447,7 +1447,8 @@ func newRPCPendingTransaction(tx *types.Transaction, current *types.Header, conf
blockNumber := uint64(0)
blockTime := uint64(0)
if current != nil {
baseFee = misc.CalcBaseFee(config, current, l1BaseFee)
// Use time.Now() as the current block time to calculate the pending base fee.
baseFee = misc.CalcBaseFee(config, current, l1BaseFee, uint64(time.Now().Unix()))
blockNumber = current.Number.Uint64()
blockTime = current.Time
}

View file

@ -488,7 +488,7 @@ func (w *worker) newWork(now time.Time, parent *types.Block, reorging bool, reor
// Set baseFee if we are on an EIP-1559 chain
if w.chainConfig.IsCurie(header.Number) {
parentL1BaseFee := fees.GetL1BaseFee(parentState)
header.BaseFee = misc.CalcBaseFee(w.chainConfig, parent.Header(), parentL1BaseFee)
header.BaseFee = misc.CalcBaseFee(w.chainConfig, parent.Header(), parentL1BaseFee, header.Time)
}
// Only set the coinbase if our consensus engine is running (avoid spurious block rewards)
if w.isRunning() {

View file

@ -1033,6 +1033,16 @@ func (c *ChainConfig) CheckCompatible(newcfg *ChainConfig, height uint64) *Confi
return lasterr
}
// BaseFeeChangeDenominator bounds the amount the base fee can change between blocks.
func (c *ChainConfig) BaseFeeChangeDenominator() uint64 {
return DefaultBaseFeeChangeDenominator
}
// ElasticityMultiplier bounds the maximum gas limit an EIP-1559 block may have.
func (c *ChainConfig) ElasticityMultiplier() uint64 {
return DefaultElasticityMultiplier
}
// CheckConfigForkOrder checks that we don't "skip" any forks, geth isn't pluggable enough
// to guarantee that forks can be implemented in a different order than on official networks
func (c *ChainConfig) CheckConfigForkOrder() error {

View file

@ -127,9 +127,9 @@ const (
// Introduced in Tangerine Whistle (Eip 150)
CreateBySelfdestructGas uint64 = 25000
BaseFeeChangeDenominator = 8 // Bounds the amount the base fee can change between blocks.
ElasticityMultiplier = 2 // Bounds the maximum gas limit an EIP-1559 block may have.
InitialBaseFee = 1000000000 // Initial base fee for EIP-1559 blocks.
DefaultBaseFeeChangeDenominator = 8 // Bounds the amount the base fee can change between blocks.
DefaultElasticityMultiplier = 2 // Bounds the maximum gas limit an EIP-1559 block may have.
InitialBaseFee = 10000000 // Initial base fee for EIP-1559 blocks.
MaxCodeSize = 24576 // Maximum bytecode to permit for a contract
MaxInitCodeSize = 2 * MaxCodeSize // Maximum initcode to permit in a creation transaction and create instructions

View file

@ -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 = 60 // Patch version component of the current release
VersionPatch = 61 // Patch version component of the current release
VersionMeta = "mainnet" // Version metadata to append to the version string
)