-
Notifications
You must be signed in to change notification settings - Fork 3.3k
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 f846a89
Showing
17 changed files
with
335 additions
and
75 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,95 @@ | ||
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 { | ||
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(params.BlobTxBlobGasPerBlob * 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 divided by the number of bytes that can be | ||
// submitted for that price. | ||
// This is comparing blobCost/blobDataBytes > calldataCost/calldataBytes: | ||
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 - 1, | ||
TargetNumFrames: 1, | ||
} | ||
blobCfg := ChannelConfig{ | ||
MaxFrameSize: eth.MaxBlobDataSize - 1, | ||
TargetNumFrames: 3, // gets closest to amortized fixed tx costs | ||
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, // because of amortized fixed 21000 tx cost, blobs are still cheaper here... | ||
}, | ||
{ | ||
name: "close-cheaper-calldata", | ||
tipCap: 1e3, | ||
baseFee: 1e6, | ||
blobBaseFee: 161e5, // ...but then increasing the fee just a tiny bit makes blobs more expensive | ||
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.