Skip to content
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
16 changes: 12 additions & 4 deletions op-batcher/batcher/channel_config_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ type (
}

GasPricer interface {
SuggestGasPriceCaps(ctx context.Context) (tipCap *big.Int, baseFee *big.Int, blobBaseFee *big.Int, err error)
SuggestGasPriceCaps(ctx context.Context) (tipCap *big.Int, baseFee *big.Int, blobTipCap *big.Int, blobBaseFee *big.Int, err error)
}

DynamicEthChannelConfig struct {
Expand Down Expand Up @@ -61,7 +61,7 @@ func (dec *DynamicEthChannelConfig) ChannelConfig(isPectra, isThrottling bool) C
}
ctx, cancel := context.WithTimeout(context.Background(), dec.timeout)
defer cancel()
tipCap, baseFee, blobBaseFee, err := dec.gasPricer.SuggestGasPriceCaps(ctx)
tipCap, baseFee, blobTipCap, blobBaseFee, err := dec.gasPricer.SuggestGasPriceCaps(ctx)
if err != nil {
dec.log.Warn("Error querying gas prices, returning last config", "err", err)
return *dec.lastConfig
Expand All @@ -81,8 +81,14 @@ func (dec *DynamicEthChannelConfig) ChannelConfig(isPectra, isThrottling bool) C
numBlobsPerTx := dec.blobConfig.TargetNumFrames

// Compute the total absolute cost of submitting either a single calldata tx or a single blob tx.
calldataCost, blobCost := computeSingleCalldataTxCost(tokensPerCalldataTx, baseFee, tipCap, isPectra),
computeSingleBlobTxCost(numBlobsPerTx, baseFee, tipCap, blobBaseFee)
calldataCost, blobCost, oracleBlobCost :=
computeSingleCalldataTxCost(tokensPerCalldataTx, baseFee, tipCap, isPectra),
computeSingleBlobTxCost(numBlobsPerTx, baseFee, tipCap, blobBaseFee),
computeSingleBlobTxCost(numBlobsPerTx, baseFee, blobTipCap, blobBaseFee)

// TODO(18618): before activating the blob tip oracle, confirm in prod that we mostly get newBlobSavings == true, otherwise
// it is not worth it using the oracle
oracleBlobSavings := oracleBlobCost.Cmp(blobCost) < 0

// Now we compare the absolute cost per tx divided by the number of bytes per tx:
blobDataBytesPerTx := big.NewInt(eth.MaxBlobDataSize * int64(numBlobsPerTx))
Expand All @@ -97,6 +103,8 @@ func (dec *DynamicEthChannelConfig) ChannelConfig(isPectra, isThrottling bool) C
lgr := dec.log.New("base_fee", baseFee, "blob_base_fee", blobBaseFee, "tip_cap", tipCap,
"calldata_bytes", calldataBytesPerTx, "calldata_cost", calldataCost,
"blob_data_bytes", blobDataBytesPerTx, "blob_cost", blobCost,
"oracle_blob_cost", oracleBlobCost,
"oracle_blob_savings", oracleBlobSavings,
"cost_ratio", costRatio)

if ay.Cmp(bx) == 1 {
Expand Down
7 changes: 4 additions & 3 deletions op-batcher/batcher/channel_config_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,15 @@ type mockGasPricer struct {
err error
tipCap int64
baseFee int64
blobTipCap int64
blobBaseFee int64
}

func (gp *mockGasPricer) SuggestGasPriceCaps(context.Context) (tipCap *big.Int, baseFee *big.Int, blobBaseFee *big.Int, err error) {
func (gp *mockGasPricer) SuggestGasPriceCaps(context.Context) (tipCap *big.Int, baseFee *big.Int, blobTipCap *big.Int, blobBaseFee *big.Int, err error) {
if gp.err != nil {
return nil, nil, nil, gp.err
return nil, nil, nil, nil, gp.err
}
return big.NewInt(gp.tipCap), big.NewInt(gp.baseFee), big.NewInt(gp.blobBaseFee), nil
return big.NewInt(gp.tipCap), big.NewInt(gp.baseFee), big.NewInt(gp.blobTipCap), big.NewInt(gp.blobBaseFee), nil
}

func TestDynamicEthChannelConfig_ChannelConfig(t *testing.T) {
Expand Down
123 changes: 119 additions & 4 deletions op-batcher/batcher/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"io"
"math/big"
"sync/atomic"
"time"

Expand All @@ -19,7 +20,9 @@ import (
"github.com/ethereum-optimism/optimism/op-node/chaincfg"
"github.com/ethereum-optimism/optimism/op-node/params"
"github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum-optimism/optimism/op-service/bgpo"
"github.com/ethereum-optimism/optimism/op-service/cliapp"
"github.com/ethereum-optimism/optimism/op-service/client"
"github.com/ethereum-optimism/optimism/op-service/dial"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum-optimism/optimism/op-service/httputil"
Expand Down Expand Up @@ -80,6 +83,10 @@ type BatcherService struct {
stopped atomic.Bool

NotSubmittingOnStart bool

// BlobGasPriceOracle tracks blob base gas prices for dynamic pricing
blobTipOracle *bgpo.BlobTipOracle
oracleStopCh chan struct{}
}

type DriverSetupOption func(setup *DriverSetup)
Expand Down Expand Up @@ -169,7 +176,7 @@ func (bs *BatcherService) initFromCLIConfig(ctx context.Context, closeApp contex
if err := bs.initRollupConfig(ctx); err != nil {
return fmt.Errorf("failed to load rollup config: %w", err)
}
if err := bs.initTxManager(cfg); err != nil {
if err := bs.initTxManager(ctx, cfg); err != nil {
return fmt.Errorf("failed to init Tx manager: %w", err)
}
// must be init before driver and channel config
Expand Down Expand Up @@ -254,6 +261,56 @@ func (bs *BatcherService) initRollupConfig(ctx context.Context) error {
return nil
}

func (bs *BatcherService) initBlobTipOracle(ctx context.Context, cfg *CLIConfig) error {
// Only initialize the oracle if we're using blobs or auto mode
if cfg.DataAvailabilityType != flags.BlobsType && cfg.DataAvailabilityType != flags.AutoType {
bs.Log.Debug("Skipping blob tip oracle initialization (not using blobs)")
return nil
}

// Get RPC client from L1 client
// The ethclient.Client has a Client() method that returns the underlying *rpc.Client
rpcClient := bs.L1Client.Client()
if rpcClient == nil {
return fmt.Errorf("failed to get RPC client from L1 client")
}

// Get L1 chain config from rollup config
l1ChainID := eth.ChainIDFromBig(bs.RollupConfig.L1ChainID)
l1ChainConfig := eth.L1ChainConfigByChainID(l1ChainID)
if l1ChainConfig == nil {
bs.Log.Info("Blob tip oracle not initialized when L1 chain ID is not known (Ethereum mainnet, Sepolia, Holesky, Hoodi)")
return nil
}

// Wrap the RPC client to match the client.RPC interface
baseRPCClient := client.NewBaseRPCClient(rpcClient)

// Create the oracle with default config
oracleConfig := bgpo.DefaultBlobTipOracleConfig()
oracleConfig.NetworkTimeout = bs.NetworkTimeout
minTipCap, err := eth.GweiToWei(cfg.TxMgrConfig.MinTipCapGwei)
if err != nil {
return fmt.Errorf("invalid min tip cap: %w", err)
}
oracleConfig.DefaultPriorityFee = minTipCap
bs.blobTipOracle = bgpo.NewBlobTipOracle(ctx, baseRPCClient, l1ChainConfig, bs.Log, oracleConfig)
bs.oracleStopCh = make(chan struct{})

bs.Log.Info("Initialized blob tip oracle")

// Start the blob tip oracle if it's initialized
go func() {
if err := bs.blobTipOracle.Start(); err != nil {
bs.Log.Error("Blob tip oracle stopped with error", "err", err)
}
close(bs.oracleStopCh)
}()
bs.blobTipOracle.WaitCachePopulated()
bs.Log.Info("Started blob tip oracle")
return nil
}

func (bs *BatcherService) initChannelConfig(cfg *CLIConfig) error {
channelTimeout := bs.RollupConfig.ChannelTimeoutBedrock
// Use lower channel timeout if granite is scheduled.
Expand Down Expand Up @@ -340,8 +397,52 @@ func (bs *BatcherService) initChannelConfig(cfg *CLIConfig) error {
return nil
}

func (bs *BatcherService) initTxManager(cfg *CLIConfig) error {
txManager, err := txmgr.NewSimpleTxManager("batcher", bs.Log, bs.Metrics, cfg.TxMgrConfig)
func (bs *BatcherService) initTxManager(ctx context.Context, cfg *CLIConfig) error {
// Initialize the blob tip oracle first
if err := bs.initBlobTipOracle(ctx, cfg); err != nil {
return fmt.Errorf("failed to init blob tip oracle: %w", err)
}

// Create the base config from CLI config
txmgrConfig, err := txmgr.NewConfig(cfg.TxMgrConfig, bs.Log)
if err != nil {
return err
}

// Create a custom gas price estimator that uses the blob tip oracle if available
if bs.blobTipOracle != nil {
txmgrConfig.GasPriceEstimatorFn = func(ctx context.Context, backend txmgr.ETHBackend) (*big.Int, *big.Int, *big.Int, *big.Int, error) {
// Get tip and base fee from backend (standard way for execution gas)
tip, err := backend.SuggestGasTipCap(ctx)
if err != nil {
return nil, nil, nil, nil, err
}

head, err := backend.HeaderByNumber(ctx, nil)
if err != nil {
return nil, nil, nil, nil, err
}
if head.BaseFee == nil {
return nil, nil, nil, nil, errors.New("txmgr does not support pre-london blocks that do not have a base fee")
}

blobBaseFee, err := backend.BlobBaseFee(ctx)
if err != nil {
return nil, nil, nil, nil, err
}

// Use the oracle's SuggestBlobTipCap for blob tip fee suggestion
// This analyzes recent blob transactions to suggest an appropriate blob tip fee
suggestedBlobFeeCap, err := bs.blobTipOracle.SuggestBlobTipCap(ctx, 0, 0)
if err != nil {
return nil, nil, nil, nil, fmt.Errorf("blob tip oracle failed to suggest blob tip fee: %w", err)
}

return tip, head.BaseFee, suggestedBlobFeeCap, blobBaseFee, nil
}
}

txManager, err := txmgr.NewSimpleTxManagerFromConfig("batcher", bs.Log, bs.Metrics, txmgrConfig)
if err != nil {
return err
}
Expand Down Expand Up @@ -439,7 +540,7 @@ func (bs *BatcherService) initAltDA(cfg *CLIConfig) error {

// Start runs once upon start of the batcher lifecycle,
// and starts batch-submission work if the batcher is configured to start submit data on startup.
func (bs *BatcherService) Start(_ context.Context) error {
func (bs *BatcherService) Start(ctx context.Context) error {
bs.driver.Log.Info("Starting batcher", "notSubmittingOnStart", bs.NotSubmittingOnStart)

if !bs.NotSubmittingOnStart {
Expand Down Expand Up @@ -475,6 +576,20 @@ func (bs *BatcherService) Stop(ctx context.Context) error {
bs.TxManager.Close()
}

// Stop the blob tip oracle if it's running
if bs.blobTipOracle != nil {
bs.blobTipOracle.Close()
// Wait for the oracle goroutine to finish
if bs.oracleStopCh != nil {
select {
case <-bs.oracleStopCh:
// Oracle stopped
case <-ctx.Done():
// Context cancelled, force stop
}
}
}

var result error
if bs.driver != nil {
if err := bs.driver.StopBatchSubmittingIfRunning(ctx); err != nil {
Expand Down
2 changes: 1 addition & 1 deletion op-challenger/sender/sender_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,6 @@ func (s *stubTxMgr) API() rpc.API {
func (s *stubTxMgr) Close() {
}

func (s *stubTxMgr) SuggestGasPriceCaps(context.Context) (*big.Int, *big.Int, *big.Int, error) {
func (s *stubTxMgr) SuggestGasPriceCaps(context.Context) (*big.Int, *big.Int, *big.Int, *big.Int, error) {
panic("unimplemented")
}
11 changes: 7 additions & 4 deletions op-deployer/pkg/deployer/broadcaster/gas_estimator.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ var (
// dummyBlobFee is a dummy value for the blob fee. Since this gas estimator will never
// post blobs, it's just set to 1.
dummyBlobFee = big.NewInt(1)
// dummyBlobTipCap is a dummy value for the blob tip cap. Since this gas estimator will never
// post blobs, it's just set to 0.
dummyBlobTipCap = big.NewInt(0)
// maxTip is the maximum tip that can be suggested by this estimator.
maxTip = big.NewInt(50 * 1e9)
// minTip is the minimum tip that can be suggested by this estimator.
Expand All @@ -25,15 +28,15 @@ var (
// DeployerGasPriceEstimator is a custom gas price estimator for use with op-deployer.
// It pads the base fee by 50% and multiplies the suggested tip by 5 up to a max of
// 50 gwei.
func DeployerGasPriceEstimator(ctx context.Context, client txmgr.ETHBackend) (*big.Int, *big.Int, *big.Int, error) {
func DeployerGasPriceEstimator(ctx context.Context, client txmgr.ETHBackend) (*big.Int, *big.Int, *big.Int, *big.Int, error) {
chainHead, err := client.HeaderByNumber(ctx, nil)
if err != nil {
return nil, nil, nil, fmt.Errorf("failed to get block: %w", err)
return nil, nil, nil, nil, fmt.Errorf("failed to get block: %w", err)
}

tip, err := client.SuggestGasTipCap(ctx)
if err != nil {
return nil, nil, nil, fmt.Errorf("failed to get gas tip cap: %w", err)
return nil, nil, nil, nil, fmt.Errorf("failed to get gas tip cap: %w", err)
}

baseFeePad := new(big.Int).Div(chainHead.BaseFee, baseFeePadFactor)
Expand All @@ -48,5 +51,5 @@ func DeployerGasPriceEstimator(ctx context.Context, client txmgr.ETHBackend) (*b
paddedTip.Set(maxTip)
}

return paddedTip, paddedBaseFee, dummyBlobFee, nil
return paddedTip, paddedBaseFee, dummyBlobTipCap, dummyBlobFee, nil
}
2 changes: 1 addition & 1 deletion op-e2e/actions/helpers/l2_proposer.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ func (f fakeTxMgr) API() rpc.API {
panic("unimplemented")
}

func (f fakeTxMgr) SuggestGasPriceCaps(context.Context) (*big.Int, *big.Int, *big.Int, error) {
func (f fakeTxMgr) SuggestGasPriceCaps(context.Context) (*big.Int, *big.Int, *big.Int, *big.Int, error) {
panic("unimplemented")
}

Expand Down
8 changes: 4 additions & 4 deletions op-e2e/system/da/eip4844_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -293,11 +293,11 @@ func TestBatcherAutoDA(t *testing.T) {

// Helpers
mustGetFees := func() (*big.Int, *big.Int, *big.Int, float64) {
tip, baseFee, blobFee, err := txmgr.DefaultGasPriceEstimatorFn(ctx, l1Client)
tip, baseFee, _, blobBaseFee, err := txmgr.DefaultGasPriceEstimatorFn(ctx, l1Client)
require.NoError(t, err)
feeRatio := float64(blobFee.Int64()) / float64(baseFee.Int64()+tip.Int64())
t.Logf("L1 fees are: baseFee(%d), tip(%d), blobBaseFee(%d). feeRatio: %f", baseFee, tip, blobFee, feeRatio)
return tip, baseFee, blobFee, feeRatio
feeRatio := float64(blobBaseFee.Int64()) / float64(baseFee.Int64()+tip.Int64())
t.Logf("L1 fees are: baseFee(%d), tip(%d), blobBaseFee(%d). feeRatio: %f", baseFee, tip, blobBaseFee, feeRatio)
return tip, baseFee, blobBaseFee, feeRatio
}
requireEventualBatcherTxType := func(txType uint8, timeout time.Duration, strict bool) {
var foundOtherTxType bool
Expand Down
Loading