-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
op-batcher: Implement dynamic blob/calldata selection
- Loading branch information
1 parent
b7f8188
commit 9c988d2
Showing
14 changed files
with
327 additions
and
54 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
package batcher | ||
|
||
import ( | ||
"context" | ||
"math/big" | ||
"time" | ||
|
||
"github.com/ethereum-optimism/optimism/op-service/eth" | ||
"github.com/ethereum/go-ethereum/log" | ||
"github.com/ethereum/go-ethereum/params" | ||
) | ||
|
||
const randomByteCalldataGas = params.TxDataNonZeroGasEIP2028 | ||
|
||
type ( | ||
ChannelConfigProvider interface { | ||
ChannelConfig() ChannelConfig | ||
} | ||
|
||
GasPricer interface { | ||
SuggestGasPriceCaps(ctx context.Context) (tipCap *big.Int, baseFee *big.Int, blobBaseFee *big.Int, err error) | ||
} | ||
|
||
DynamicEthChannelConfig struct { | ||
log log.Logger | ||
timeout time.Duration // query timeout | ||
gasPricer GasPricer | ||
|
||
blobConfig ChannelConfig | ||
calldataConfig ChannelConfig | ||
lastConfig *ChannelConfig | ||
} | ||
) | ||
|
||
func NewDynamicEthChannelConfig(lgr log.Logger, | ||
reqTimeout time.Duration, gasPricer GasPricer, | ||
blobConfig ChannelConfig, calldataConfig ChannelConfig, | ||
) *DynamicEthChannelConfig { | ||
// Copy blobConfig and statically configure fallback calldata config. | ||
// In the future, we might want to make the calldata config configurable. | ||
// cdCfg := blobConfig | ||
// cdCfg.TargetNumFrames = 1 | ||
// cdCfg.MaxFrameSize = 120_000 | ||
// cdCfg.MultiFrameTxs = false | ||
|
||
dec := &DynamicEthChannelConfig{ | ||
log: lgr, | ||
timeout: reqTimeout, | ||
gasPricer: gasPricer, | ||
blobConfig: blobConfig, | ||
calldataConfig: calldataConfig, | ||
} | ||
// start with blob config | ||
dec.lastConfig = &dec.blobConfig | ||
return dec | ||
} | ||
|
||
func (dec *DynamicEthChannelConfig) ChannelConfig() ChannelConfig { | ||
ctx, cancel := context.WithTimeout(context.Background(), dec.timeout) | ||
defer cancel() | ||
tipCap, baseFee, blobBaseFee, err := dec.gasPricer.SuggestGasPriceCaps(ctx) | ||
if err != nil { | ||
dec.log.Warn("Error querying gas prices, returning last config", "err", err) | ||
return *dec.lastConfig | ||
} | ||
|
||
// We estimate the gas costs of a calldata and blob tx under the assumption that we'd fill | ||
// a frame fully and compressed random channel data has few zeros, so they can be | ||
// ignored in the calldata gas price estimation. | ||
// It is also assumed that a calldata tx would contain exactly one full frame | ||
// and a blob tx would contain target-num-frames many blobs. | ||
|
||
// It would be nicer to use core.IntrinsicGas, but we don't have the actual data at hand | ||
calldataBytes := dec.calldataConfig.MaxFrameSize + 1 // + 1 version byte | ||
calldataGas := big.NewInt(int64(calldataBytes*randomByteCalldataGas + params.TxGas)) | ||
calldataPrice := new(big.Int).Add(baseFee, tipCap) | ||
calldataCost := new(big.Int).Mul(calldataGas, calldataPrice) | ||
|
||
blobGas := big.NewInt(eth.BlobSize * int64(dec.blobConfig.TargetNumFrames)) | ||
blobCost := new(big.Int).Mul(blobGas, blobBaseFee) | ||
// blobs still have intrinsic calldata costs | ||
blobCalldataCost := new(big.Int).Mul(big.NewInt(int64(params.TxGas)), calldataPrice) | ||
blobCost = blobCost.Add(blobCost, blobCalldataCost) | ||
|
||
blobDataBytes := big.NewInt(eth.MaxBlobDataSize * int64(dec.blobConfig.TargetNumFrames)) | ||
lgr := dec.log.New("base_fee", baseFee, "blob_base_fee", blobBaseFee, "tip_cap", tipCap, | ||
"calldata_bytes", calldataBytes, "calldata_cost", calldataCost, | ||
"blob_data_bytes", blobDataBytes, "blob_cost", blobCost) | ||
|
||
// Now we compare the prices normalized to the number of bytes that can be | ||
// submitted for that price. | ||
if new(big.Int).Mul(blobCost, big.NewInt(int64(calldataBytes))). | ||
Cmp(new(big.Int).Mul(calldataCost, blobDataBytes)) == 1 { | ||
lgr.Info("Using calldata channel config") | ||
dec.lastConfig = &dec.calldataConfig | ||
return dec.calldataConfig | ||
} | ||
lgr.Info("Using blob channel config") | ||
dec.lastConfig = &dec.blobConfig | ||
return dec.blobConfig | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
package batcher | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"math/big" | ||
"testing" | ||
"time" | ||
|
||
"github.com/ethereum-optimism/optimism/op-service/eth" | ||
"github.com/ethereum-optimism/optimism/op-service/testlog" | ||
"github.com/stretchr/testify/require" | ||
"golang.org/x/exp/slog" | ||
) | ||
|
||
type mockGasPricer struct { | ||
err error | ||
tipCap int64 | ||
baseFee int64 | ||
blobBaseFee int64 | ||
} | ||
|
||
func (gp *mockGasPricer) SuggestGasPriceCaps(context.Context) (tipCap *big.Int, baseFee *big.Int, blobBaseFee *big.Int, err error) { | ||
if gp.err != nil { | ||
return nil, nil, nil, gp.err | ||
} | ||
return big.NewInt(gp.tipCap), big.NewInt(gp.baseFee), big.NewInt(gp.blobBaseFee), nil | ||
} | ||
|
||
func TestDynamicEthChannelConfig_ChannelConfig(t *testing.T) { | ||
calldataCfg := ChannelConfig{ | ||
MaxFrameSize: 120_000, | ||
TargetNumFrames: 1, | ||
} | ||
blobCfg := ChannelConfig{ | ||
MaxFrameSize: eth.MaxBlobDataSize - 1, | ||
TargetNumFrames: 6, | ||
MultiFrameTxs: true, | ||
} | ||
|
||
tests := []struct { | ||
name string | ||
tipCap int64 | ||
baseFee int64 | ||
blobBaseFee int64 | ||
wantCalldata bool | ||
}{ | ||
{ | ||
name: "much-cheaper-blobs", | ||
tipCap: 1e3, | ||
baseFee: 1e6, | ||
blobBaseFee: 1, | ||
}, | ||
{ | ||
name: "close-cheaper-blobs", | ||
tipCap: 1e3, | ||
baseFee: 1e6, | ||
blobBaseFee: 16e6, | ||
}, | ||
{ | ||
name: "close-cheaper-calldata", | ||
tipCap: 1e3, | ||
baseFee: 1e6, | ||
blobBaseFee: 17e6, | ||
wantCalldata: true, | ||
}, | ||
{ | ||
name: "much-cheaper-calldata", | ||
tipCap: 1e3, | ||
baseFee: 1e6, | ||
blobBaseFee: 1e9, | ||
wantCalldata: true, | ||
}, | ||
} | ||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
lgr, ch := testlog.CaptureLogger(t, slog.LevelInfo) | ||
gp := &mockGasPricer{ | ||
tipCap: tt.tipCap, | ||
baseFee: tt.baseFee, | ||
blobBaseFee: tt.blobBaseFee, | ||
} | ||
dec := NewDynamicEthChannelConfig(lgr, 1*time.Second, gp, blobCfg, calldataCfg) | ||
cc := dec.ChannelConfig() | ||
if tt.wantCalldata { | ||
require.Equal(t, cc, calldataCfg) | ||
require.NotNil(t, ch.FindLog(testlog.NewMessageContainsFilter("calldata"))) | ||
require.Same(t, &dec.calldataConfig, dec.lastConfig) | ||
} else { | ||
require.Equal(t, cc, blobCfg) | ||
require.NotNil(t, ch.FindLog(testlog.NewMessageContainsFilter("blob"))) | ||
require.Same(t, &dec.blobConfig, dec.lastConfig) | ||
} | ||
}) | ||
} | ||
|
||
t.Run("error-latest", func(t *testing.T) { | ||
lgr, ch := testlog.CaptureLogger(t, slog.LevelInfo) | ||
gp := &mockGasPricer{ | ||
tipCap: 1, | ||
baseFee: 1e3, | ||
blobBaseFee: 1e6, // should return calldata cfg without error | ||
err: errors.New("gp-error"), | ||
} | ||
dec := NewDynamicEthChannelConfig(lgr, 1*time.Second, gp, blobCfg, calldataCfg) | ||
require.Equal(t, dec.ChannelConfig(), blobCfg) | ||
require.NotNil(t, ch.FindLog( | ||
testlog.NewLevelFilter(slog.LevelWarn), | ||
testlog.NewMessageContainsFilter("returning last config"), | ||
)) | ||
|
||
gp.err = nil | ||
require.Equal(t, dec.ChannelConfig(), calldataCfg) | ||
require.NotNil(t, ch.FindLog( | ||
testlog.NewLevelFilter(slog.LevelInfo), | ||
testlog.NewMessageContainsFilter("calldata"), | ||
)) | ||
|
||
gp.err = errors.New("gp-error-2") | ||
require.Equal(t, dec.ChannelConfig(), calldataCfg) | ||
require.NotNil(t, ch.FindLog( | ||
testlog.NewLevelFilter(slog.LevelWarn), | ||
testlog.NewMessageContainsFilter("returning last config"), | ||
)) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.