Skip to content

feat: update base fee via contract #1189

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 23 commits into from
May 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions cmd/geth/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"github.com/scroll-tech/go-ethereum/accounts/keystore"
"github.com/scroll-tech/go-ethereum/cmd/utils"
"github.com/scroll-tech/go-ethereum/common"
"github.com/scroll-tech/go-ethereum/consensus/misc"
"github.com/scroll-tech/go-ethereum/console/prompt"
"github.com/scroll-tech/go-ethereum/eth"
"github.com/scroll-tech/go-ethereum/eth/downloader"
Expand Down Expand Up @@ -185,8 +186,6 @@ var (
utils.DARecoverySignBlocksFlag,
utils.DARecoveryL2EndBlockFlag,
utils.DARecoveryProduceBlocksFlag,
utils.L2BaseFeeScalarFlag,
utils.L2BaseFeeOverheadFlag,
}

rpcFlags = []cli.Flag{
Expand Down Expand Up @@ -455,8 +454,9 @@ func startNode(ctx *cli.Context, stack *node.Node, backend ethapi.Backend) {
utils.Fatalf("Ethereum service not running")
}
// Set the gas price to the limits from the CLI and start mining
gasprice := utils.GlobalBig(ctx, utils.MinerGasPriceFlag.Name)
ethBackend.TxPool().SetGasPrice(gasprice)
// gasprice := utils.GlobalBig(ctx, utils.MinerGasPriceFlag.Name)
// ethBackend.TxPool().SetGasPrice(gasprice)
ethBackend.TxPool().SetGasPrice(misc.MinBaseFee()) // override configured min gas price
ethBackend.TxPool().SetIsMiner(true)
// start mining
threads := ctx.GlobalInt(utils.MinerThreadsFlag.Name)
Expand Down
2 changes: 0 additions & 2 deletions cmd/geth/usage.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,8 +236,6 @@ var AppHelpFlagGroups = []flags.FlagGroup{
utils.L1DeploymentBlockFlag,
utils.L1DisableMessageQueueV2Flag,
utils.RollupVerifyEnabledFlag,
utils.L2BaseFeeScalarFlag,
utils.L2BaseFeeOverheadFlag,
utils.DASyncEnabledFlag,
utils.DABlobScanAPIEndpointFlag,
utils.DABlockNativeAPIEndpointFlag,
Expand Down
37 changes: 0 additions & 37 deletions cmd/utils/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ import (
"github.com/scroll-tech/go-ethereum/consensus"
"github.com/scroll-tech/go-ethereum/consensus/clique"
"github.com/scroll-tech/go-ethereum/consensus/ethash"
"github.com/scroll-tech/go-ethereum/consensus/misc"
"github.com/scroll-tech/go-ethereum/core"
"github.com/scroll-tech/go-ethereum/core/rawdb"
"github.com/scroll-tech/go-ethereum/core/vm"
Expand Down Expand Up @@ -935,18 +934,6 @@ var (
Name: "da.recovery.produceblocks",
Usage: "Produce unsigned blocks after L1 recovery for permissionless batch submission",
}

// L2 base fee settings
L2BaseFeeScalarFlag = BigFlag{
Name: "basefee.scalar",
Usage: "Scalar used in the l2 base fee formula. Signer nodes will use this for computing the next block's base fee. Follower nodes will use this in RPC.",
Value: misc.DefaultBaseFeeScalar,
}
L2BaseFeeOverheadFlag = BigFlag{
Name: "basefee.overhead",
Usage: "Overhead used in the l2 base fee formula. Signer nodes will use this for computing the next block's base fee. Follower nodes will use this in RPC.",
Value: misc.DefaultBaseFeeOverhead,
}
)

// MakeDataDir retrieves the currently requested data directory, terminating
Expand Down Expand Up @@ -1722,29 +1709,6 @@ func setDA(ctx *cli.Context, cfg *ethconfig.Config) {
}
}

func setBaseFee(ctx *cli.Context, cfg *ethconfig.Config) {
cfg.BaseFeeScalar = misc.DefaultBaseFeeScalar
if ctx.GlobalIsSet(L2BaseFeeScalarFlag.Name) {
cfg.BaseFeeScalar = GlobalBig(ctx, L2BaseFeeScalarFlag.Name)
}
cfg.BaseFeeOverhead = misc.DefaultBaseFeeOverhead
if ctx.GlobalIsSet(L2BaseFeeOverheadFlag.Name) {
cfg.BaseFeeOverhead = GlobalBig(ctx, L2BaseFeeOverheadFlag.Name)
}

log.Info("L2 base fee coefficients", "scalar", cfg.BaseFeeScalar, "overhead", cfg.BaseFeeOverhead)

var minBaseFee uint64
if fee := misc.MinBaseFee(cfg.BaseFeeScalar, cfg.BaseFeeOverhead); fee.IsUint64() {
minBaseFee = fee.Uint64()
}

if cfg.TxPool.PriceLimit < minBaseFee {
log.Warn("Updating txpool price limit to min L2 base fee", "provided", cfg.TxPool.PriceLimit, "updated", minBaseFee)
cfg.TxPool.PriceLimit = minBaseFee
}
}

func setMaxBlockRange(ctx *cli.Context, cfg *ethconfig.Config) {
if ctx.GlobalIsSet(MaxBlockRangeFlag.Name) {
cfg.MaxBlockRange = ctx.GlobalInt64(MaxBlockRangeFlag.Name)
Expand Down Expand Up @@ -1821,7 +1785,6 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) {
setCircuitCapacityCheck(ctx, cfg)
setEnableRollupVerify(ctx, cfg)
setDA(ctx, cfg)
setBaseFee(ctx, cfg)
setMaxBlockRange(ctx, cfg)
if ctx.GlobalIsSet(ShadowforkPeersFlag.Name) {
cfg.ShadowForkPeerIDs = ctx.GlobalStringSlice(ShadowforkPeersFlag.Name)
Expand Down
128 changes: 110 additions & 18 deletions consensus/misc/eip1559.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,67 @@ package misc
import (
"fmt"
"math/big"
"sync"

"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/params"
"github.com/scroll-tech/go-ethereum/rollup/rcfg"
"github.com/scroll-tech/go-ethereum/rpc"
)

// Protocol-enforced maximum L2 base fee.
// We would only go above this if L1 base fee hits 2931 Gwei.
const MaximumL2BaseFee = 10000000000
const (
// Protocol-enforced maximum L2 base fee.
// We would only go above this if L1 base fee hits 2931 Gwei.
MaximumL2BaseFee = 10000000000

// L2 base fee fallback values, in case the L2 system contract
// is not deployed on not configured yet.
DefaultBaseFeeOverhead = 15680000
DefaultBaseFeeScalar = 34000000000000
)

// L2 base fee formula constants and defaults.
// l2BaseFee = (l1BaseFee * scalar) / PRECISION + overhead.
// `scalar` accounts for finalization costs. `overhead` accounts for sequencing and proving costs.
// we use 1e18 for precision to match the contract implementation.
var (
BaseFeePrecision = new(big.Int).SetUint64(1e18)
DefaultBaseFeeScalar = new(big.Int).SetUint64(34000000000000)
DefaultBaseFeeOverhead = new(big.Int).SetUint64(15680000)
// We use 1e18 for precision to match the contract implementation.
BaseFeePrecision = new(big.Int).SetUint64(1e18)

// scalar and overhead are updated automatically in `Blockchain.writeBlockWithState`.
baseFeeScalar = big.NewInt(0)
baseFeeOverhead = big.NewInt(0)

lock sync.RWMutex
)

func ReadL2BaseFeeCoefficients() (scalar *big.Int, overhead *big.Int) {
lock.RLock()
defer lock.RUnlock()
return new(big.Int).Set(baseFeeScalar), new(big.Int).Set(baseFeeOverhead)
}

func UpdateL2BaseFeeOverhead(newOverhead *big.Int) {
if newOverhead == nil {
log.Error("Failed to set L2 base fee overhead, new value is <nil>")
return
}
lock.Lock()
defer lock.Unlock()
baseFeeOverhead.Set(newOverhead)
}

func UpdateL2BaseFeeScalar(newScalar *big.Int) {
if newScalar == nil {
log.Error("Failed to set L2 base fee scalar, new value is <nil>")
return
}
lock.Lock()
defer lock.Unlock()
baseFeeScalar.Set(newScalar)
}

// VerifyEip1559Header verifies some header attributes which were changed in EIP-1559,
// - gas limit check
// - basefee check
Expand Down Expand Up @@ -65,20 +107,13 @@ func CalcBaseFee(config *params.ChainConfig, parent *types.Header, parentL1BaseF
return big.NewInt(10000000) // 0.01 Gwei
}

scalar := config.Scroll.BaseFeeScalar
if scalar == nil {
scalar = DefaultBaseFeeScalar
}
overhead := config.Scroll.BaseFeeOverhead
if overhead == nil {
overhead = DefaultBaseFeeOverhead
}

scalar, overhead := ReadL2BaseFeeCoefficients()
return calcBaseFee(scalar, overhead, parentL1BaseFee)
}

// MinBaseFee calculates the minimum L2 base fee based on the configured coefficients.
func MinBaseFee(scalar, overhead *big.Int) *big.Int {
// MinBaseFee calculates the minimum L2 base fee based on the current coefficients.
func MinBaseFee() *big.Int {
scalar, overhead := ReadL2BaseFeeCoefficients()
return calcBaseFee(scalar, overhead, big.NewInt(0))
}

Expand All @@ -94,3 +129,60 @@ func calcBaseFee(scalar, overhead, parentL1BaseFee *big.Int) *big.Int {

return baseFee
}

type State interface {
GetState(addr common.Address, hash common.Hash) common.Hash
}

func InitializeL2BaseFeeCoefficients(chainConfig *params.ChainConfig, state State) error {
overhead := common.Big0
scalar := common.Big0

if l2SystemConfig := chainConfig.Scroll.L2SystemConfigAddress(); l2SystemConfig != (common.Address{}) {
overhead = state.GetState(l2SystemConfig, rcfg.L2BaseFeeOverheadSlot).Big()
scalar = state.GetState(l2SystemConfig, rcfg.L2BaseFeeScalarSlot).Big()
} else {
log.Warn("L2SystemConfig address is not configured")
}

// fallback to default if contract is not deployed or configured yet
if overhead.Cmp(common.Big0) == 0 {
overhead = big.NewInt(DefaultBaseFeeOverhead)
}
if scalar.Cmp(common.Big0) == 0 {
scalar = big.NewInt(DefaultBaseFeeScalar)
}

// update local view of coefficients
lock.Lock()
defer lock.Unlock()
baseFeeOverhead.Set(overhead)
baseFeeScalar.Set(scalar)
log.Info("Initialized L2 base fee coefficients", "overhead", overhead, "scalar", scalar)
return nil
}

type API struct{}

type L2BaseFeeConfig struct {
Scalar *big.Int `json:"scalar,omitempty"`
Overhead *big.Int `json:"overhead,omitempty"`
}

func (api *API) GetL2BaseFeeConfig() *L2BaseFeeConfig {
scalar, overhead := ReadL2BaseFeeCoefficients()

return &L2BaseFeeConfig{
Scalar: scalar,
Overhead: overhead,
}
}

func APIs() []rpc.API {
return []rpc.API{{
Namespace: "scroll",
Version: "1.0",
Service: &API{},
Public: false,
}}
}
14 changes: 10 additions & 4 deletions consensus/misc/eip1559_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,8 @@ func TestCalcBaseFee(t *testing.T) {
}
for i, test := range tests {
config := config()
config.Scroll.BaseFeeScalar = big.NewInt(10000000)
config.Scroll.BaseFeeOverhead = big.NewInt(1)
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 {
t.Errorf("test %d: have %d want %d, ", i, have, want)
}
Expand All @@ -142,6 +142,8 @@ func TestCalcBaseFee(t *testing.T) {
{644149677419355, 10000000000}, // cap at max L2 base fee
}
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 {
t.Errorf("test %d: have %d want %d, ", i, have, want)
}
Expand All @@ -150,11 +152,15 @@ func TestCalcBaseFee(t *testing.T) {

// TestMinBaseFee assumes all blocks are 1559-blocks
func TestMinBaseFee(t *testing.T) {
if have, want := MinBaseFee(DefaultBaseFeeScalar, DefaultBaseFeeOverhead), big.NewInt(15680000); have.Cmp(want) != 0 {
UpdateL2BaseFeeScalar(big.NewInt(34000000000000))
UpdateL2BaseFeeOverhead(big.NewInt(15680000))
if have, want := MinBaseFee(), big.NewInt(15680000); have.Cmp(want) != 0 {
t.Errorf("have %d want %d, ", have, want)
}

if have, want := MinBaseFee(big.NewInt(10000000), big.NewInt(1)), big.NewInt(1); have.Cmp(want) != 0 {
UpdateL2BaseFeeScalar(big.NewInt(10000000))
UpdateL2BaseFeeOverhead(big.NewInt(1))
if have, want := MinBaseFee(), big.NewInt(1); have.Cmp(want) != 0 {
t.Errorf("have %d want %d, ", have, want)
}
}
43 changes: 41 additions & 2 deletions core/blockchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import (
"github.com/scroll-tech/go-ethereum/common/mclock"
"github.com/scroll-tech/go-ethereum/common/prque"
"github.com/scroll-tech/go-ethereum/consensus"
"github.com/scroll-tech/go-ethereum/consensus/misc"
"github.com/scroll-tech/go-ethereum/core/rawdb"
"github.com/scroll-tech/go-ethereum/core/state"
"github.com/scroll-tech/go-ethereum/core/state/snapshot"
Expand All @@ -45,6 +46,7 @@ import (
"github.com/scroll-tech/go-ethereum/log"
"github.com/scroll-tech/go-ethereum/metrics"
"github.com/scroll-tech/go-ethereum/params"
"github.com/scroll-tech/go-ethereum/rollup/l2_system_config"
"github.com/scroll-tech/go-ethereum/trie"
)

Expand All @@ -55,7 +57,8 @@ var (
headTimeGapGauge = metrics.NewRegisteredGauge("chain/head/timegap", nil)
headL1MessageGauge = metrics.NewRegisteredGauge("chain/head/l1msg", nil)

l2BaseFeeGauge = metrics.NewRegisteredGauge("chain/fees/l2basefee", nil)
l2BaseFeeGauge = metrics.NewRegisteredGauge("chain/fees/l2basefee", nil)
l2BaseFeeUpdateTimer = metrics.NewRegisteredTimer("chain/fees/updates", nil)

accountReadTimer = metrics.NewRegisteredTimer("chain/account/reads", nil)
accountHashTimer = metrics.NewRegisteredTimer("chain/account/hashes", nil)
Expand Down Expand Up @@ -161,6 +164,39 @@ func updateHeadL1msgGauge(block *types.Block) {
}
}

// updateL2BaseFeeCoefficients updates the global L2 base fee coefficients.
// Coefficient updates are written into L2 state and emit an event.
// We could use either here; we read from the event to avoid state reads.
// In the future, if the base fee setting becomes part of block validation,
// reading from state will be more appropriate.
func updateL2BaseFeeCoefficients(l2SystemConfigAddress common.Address, logs []*types.Log) {
defer func(start time.Time) { l2BaseFeeUpdateTimer.Update(time.Since(start)) }(time.Now())

for _, l := range logs {
if l.Address != l2SystemConfigAddress {
continue
}
switch l.Topics[0] {
case l2_system_config.BaseFeeOverheadUpdatedTopic:
event, err := l2_system_config.UnpackBaseFeeOverheadUpdatedEvent(*l)
if err != nil {
log.Error("failed to unpack base fee overhead updated event log", "err", err, "log", *l)
break // break from switch, continue loop
}
misc.UpdateL2BaseFeeOverhead(event.NewBaseFeeOverhead)
log.Info("Updated L2 base fee overhead", "blockNumber", l.BlockNumber, "blockHash", l.BlockHash.Hex(), "old", event.OldBaseFeeOverhead, "new", event.NewBaseFeeOverhead)
case l2_system_config.BaseFeeScalarUpdatedTopic:
event, err := l2_system_config.UnpackBaseFeeScalarUpdatedEvent(*l)
if err != nil {
log.Error("failed to unpack base fee scalar updated event log", "err", err, "log", *l)
break // break from switch, continue loop
}
misc.UpdateL2BaseFeeScalar(event.NewBaseFeeScalar)
log.Info("Updated L2 base fee scalar", "blockNumber", l.BlockNumber, "blockHash", l.BlockHash.Hex(), "old", event.OldBaseFeeScalar, "new", event.NewBaseFeeScalar)
}
}
}

// BlockChain represents the canonical chain given a database with a genesis
// block. The Blockchain manages chain imports, reverts, chain reorganisations.
//
Expand Down Expand Up @@ -1272,6 +1308,9 @@ func (bc *BlockChain) writeBlockWithState(block *types.Block, receipts []*types.
// Note the latest relayed L1 message queue index (if any)
updateHeadL1msgGauge(block)

// Execute L2 base fee coefficient updates (if any)
updateL2BaseFeeCoefficients(bc.Config().Scroll.L2SystemConfigAddress(), logs)

parent := bc.GetHeaderByHash(block.ParentHash())
// block.Time is guaranteed to be larger than parent.Time,
// and the time gap should fit into int64.
Expand Down Expand Up @@ -1302,7 +1341,7 @@ func (bc *BlockChain) writeBlockWithState(block *types.Block, receipts []*types.
if queueIndex == nil {
// We expect that we only insert contiguous chain segments,
// so the parent will always be inserted first.
log.Crit("Queue index in DB is nil", "parent", block.ParentHash(), "hash", block.Hash())
log.Crit("Queue index in DB is nil", "parent", block.ParentHash().Hex(), "hash", block.Hash().Hex())
}
numProcessed := uint64(block.NumL1MessagesProcessed(*queueIndex))
// do not overwrite the index written by the miner worker
Expand Down
Loading
Loading