Skip to content
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

feat(taiko-client): introduce TxBuilderWithFallback #18690

Merged
merged 9 commits into from
Jan 2, 2025
Merged
8 changes: 8 additions & 0 deletions packages/taiko-client/cmd/flags/proposer.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,13 @@ var (
Value: false,
EnvVars: []string{"L1_BLOB_ALLOWED"},
}
FallbackToCalldata = &cli.BoolFlag{
Name: "l1.fallbackToCalldata",
Usage: "If set to true, proposer will use calldata as DA when blob fee is more expensive than using calldata",
Value: false,
Category: proposerCategory,
EnvVars: []string{"L1_FALLBACK_TO_CALLDATA"},
}
RevertProtectionEnabled = &cli.BoolFlag{
Name: "revertProtection",
Usage: "Enable revert protection with the support of endpoint and contract",
Expand Down Expand Up @@ -133,5 +140,6 @@ var ProposerFlags = MergeFlags(CommonFlags, []cli.Flag{
AllowZeroInterval,
MaxProposedTxListsPerEpoch,
BlobAllowed,
FallbackToCalldata,
RevertProtectionEnabled,
}, TxmgrFlags)
2 changes: 2 additions & 0 deletions packages/taiko-client/internal/metrics/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ var (
ProposerProposedTxListsCounter = factory.NewCounter(prometheus.CounterOpts{Name: "proposer_proposed_txLists"})
ProposerProposedTxsCounter = factory.NewCounter(prometheus.CounterOpts{Name: "proposer_proposed_txs"})
ProposerPoolContentFetchTime = factory.NewGauge(prometheus.GaugeOpts{Name: "proposer_pool_content_fetch_time"})
ProposerEstimatedCostCalldata = factory.NewGauge(prometheus.GaugeOpts{Name: "proposer_estimated_cost_calldata"})
ProposerEstimatedCostBlob = factory.NewGauge(prometheus.GaugeOpts{Name: "proposer_estimated_cost_blob"})

// Prover
ProverLatestVerifiedIDGauge = factory.NewGauge(prometheus.GaugeOpts{Name: "prover_latestVerified_id"})
Expand Down
2 changes: 1 addition & 1 deletion packages/taiko-client/pkg/rpc/methods.go
Original file line number Diff line number Diff line change
Expand Up @@ -642,7 +642,7 @@ func (c *Client) checkSyncedL1SnippetFromAnchor(
blockID *big.Int,
l1Height uint64,
) (bool, error) {
log.Info("Check synced L1 snippet from anchor", "blockID", blockID, "l1Height", l1Height)
log.Debug("Check synced L1 snippet from anchor", "blockID", blockID, "l1Height", l1Height)
block, err := c.L2.BlockByNumber(ctx, blockID)
if err != nil {
return false, err
Expand Down
2 changes: 2 additions & 0 deletions packages/taiko-client/proposer/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ type Config struct {
MaxProposedTxListsPerEpoch uint64
ProposeBlockTxGasLimit uint64
BlobAllowed bool
FallbackToCalldata bool
RevertProtectionEnabled bool
TxmgrConfigs *txmgr.CLIConfig
PrivateTxmgrConfigs *txmgr.CLIConfig
Expand Down Expand Up @@ -104,6 +105,7 @@ func NewConfigFromCliContext(c *cli.Context) (*Config, error) {
AllowZeroInterval: c.Uint64(flags.AllowZeroInterval.Name),
ProposeBlockTxGasLimit: c.Uint64(flags.TxGasLimit.Name),
BlobAllowed: c.Bool(flags.BlobAllowed.Name),
FallbackToCalldata: c.Bool(flags.FallbackToCalldata.Name),
RevertProtectionEnabled: c.Bool(flags.RevertProtectionEnabled.Name),
TxmgrConfigs: pkgFlags.InitTxmgrConfigsFromCli(
c.String(flags.L1WSEndpoint.Name),
Expand Down
44 changes: 15 additions & 29 deletions packages/taiko-client/proposer/proposer.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,35 +114,21 @@ func (p *Proposer) InitFromConfig(
}

p.txmgrSelector = utils.NewTxMgrSelector(txMgr, privateTxMgr, nil)

chainConfig := config.NewChainConfig(p.protocolConfigs)
p.chainConfig = chainConfig

if cfg.BlobAllowed {
p.txBuilder = builder.NewBlobTransactionBuilder(
p.rpc,
p.L1ProposerPrivKey,
cfg.TaikoL1Address,
cfg.ProverSetAddress,
cfg.L2SuggestedFeeRecipient,
cfg.ProposeBlockTxGasLimit,
cfg.ExtraData,
chainConfig,
cfg.RevertProtectionEnabled,
)
} else {
p.txBuilder = builder.NewCalldataTransactionBuilder(
p.rpc,
p.L1ProposerPrivKey,
cfg.L2SuggestedFeeRecipient,
cfg.TaikoL1Address,
cfg.ProverSetAddress,
cfg.ProposeBlockTxGasLimit,
cfg.ExtraData,
chainConfig,
cfg.RevertProtectionEnabled,
)
}
p.chainConfig = config.NewChainConfig(p.protocolConfigs)
p.txBuilder = builder.NewBuilderWithFallback(
p.rpc,
p.L1ProposerPrivKey,
cfg.L2SuggestedFeeRecipient,
cfg.TaikoL1Address,
cfg.ProverSetAddress,
cfg.ProposeBlockTxGasLimit,
cfg.ExtraData,
p.chainConfig,
p.txmgrSelector,
cfg.RevertProtectionEnabled,
cfg.BlobAllowed,
cfg.FallbackToCalldata,
)

return nil
}
Expand Down
1 change: 1 addition & 0 deletions packages/taiko-client/proposer/proposer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ func (s *ProposerTestSuite) SetupTest() {
MaxProposedTxListsPerEpoch: 1,
ExtraData: "test",
ProposeBlockTxGasLimit: 10_000_000,
FallbackToCalldata: true,
TxmgrConfigs: &txmgr.CLIConfig{
L1RPCURL: os.Getenv("L1_WS"),
NumConfirmations: 0,
Expand Down
185 changes: 185 additions & 0 deletions packages/taiko-client/proposer/transaction_builder/fallback.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
package builder

import (
"context"
"crypto/ecdsa"
"fmt"
"math/big"

"github.com/ethereum-optimism/optimism/op-service/txmgr"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
"golang.org/x/sync/errgroup"

"github.com/taikoxyz/taiko-mono/packages/taiko-client/internal/metrics"
"github.com/taikoxyz/taiko-mono/packages/taiko-client/pkg/config"
"github.com/taikoxyz/taiko-mono/packages/taiko-client/pkg/rpc"
"github.com/taikoxyz/taiko-mono/packages/taiko-client/pkg/utils"
)

// TxBuilderWithFallback builds type-2 or type-3 transactions based on the
// the realtime onchain cost, if the fallback feature is enabled.
type TxBuilderWithFallback struct {
rpc *rpc.Client
blobTransactionBuilder *BlobTransactionBuilder
calldataTransactionBuilder *CalldataTransactionBuilder
txmgrSelector *utils.TxMgrSelector
fallback bool
}

// NewBuilderWithFallback creates a new TxBuilderWithFallback instance.
func NewBuilderWithFallback(
rpc *rpc.Client,
proposerPrivateKey *ecdsa.PrivateKey,
l2SuggestedFeeRecipient common.Address,
taikoL1Address common.Address,
proverSetAddress common.Address,
gasLimit uint64,
extraData string,
chainConfig *config.ChainConfig,
txmgrSelector *utils.TxMgrSelector,
revertProtectionEnabled bool,
blobAllowed bool,
fallback bool,
) *TxBuilderWithFallback {
builder := &TxBuilderWithFallback{
rpc: rpc,
fallback: fallback,
txmgrSelector: txmgrSelector,
}

if blobAllowed {
builder.blobTransactionBuilder = NewBlobTransactionBuilder(
rpc,
proposerPrivateKey,
taikoL1Address,
proverSetAddress,
l2SuggestedFeeRecipient,
gasLimit,
extraData,
chainConfig,
revertProtectionEnabled,
)
}

builder.calldataTransactionBuilder = NewCalldataTransactionBuilder(
rpc,
proposerPrivateKey,
l2SuggestedFeeRecipient,
taikoL1Address,
proverSetAddress,
gasLimit,
extraData,
chainConfig,
revertProtectionEnabled,
)

return builder
}

// BuildOntake builds a type-2 or type-3 transaction based on the
// the realtime onchain cost, if the fallback feature is enabled.
func (b *TxBuilderWithFallback) BuildOntake(
ctx context.Context,
txListBytesArray [][]byte,
) (*txmgr.TxCandidate, error) {
// If calldata is the only option, just use it.
if b.blobTransactionBuilder == nil {
return b.calldataTransactionBuilder.BuildOntake(ctx, txListBytesArray)
}
// If blob is enabled, and fallback is not enabled, just build a blob transaction.
if !b.fallback {
return b.blobTransactionBuilder.BuildOntake(ctx, txListBytesArray)
}

// Otherwise, compare the cost, and choose the cheaper option.
var (
g = new(errgroup.Group)
txWithCalldata *txmgr.TxCandidate
txWithBlob *txmgr.TxCandidate
costCalldata *big.Int
costBlob *big.Int
err error
)

g.Go(func() error {
if txWithCalldata, err = b.calldataTransactionBuilder.BuildOntake(ctx, txListBytesArray); err != nil {
return err
}
if costCalldata, err = b.estimateCandidateCost(ctx, txWithCalldata); err != nil {
return err
}
return nil
})
g.Go(func() error {
if txWithBlob, err = b.blobTransactionBuilder.BuildOntake(ctx, txListBytesArray); err != nil {
return err
}
if costBlob, err = b.estimateCandidateCost(ctx, txWithBlob); err != nil {
return err
}
return nil
})

if err = g.Wait(); err != nil {
return nil, err
}

metrics.ProposerEstimatedCostCalldata.Set(float64(costCalldata.Uint64()))
metrics.ProposerEstimatedCostBlob.Set(float64(costBlob.Uint64()))

if costCalldata.Cmp(costBlob) < 0 {
log.Info("Building a type-2 transaction", "costCalldata", costCalldata, "costBlob", costBlob)
return txWithCalldata, nil
}

log.Info("Building a type-3 transaction", "costCalldata", costCalldata, "costBlob", costBlob)
return txWithBlob, nil
}

// estimateCandidateCost estimates the realtime onchain cost of the given transaction.
func (b *TxBuilderWithFallback) estimateCandidateCost(
ctx context.Context,
candidate *txmgr.TxCandidate,
) (*big.Int, error) {
txmgr, _ := b.txmgrSelector.Select()
gasTipCap, baseFee, blobBaseFee, err := txmgr.SuggestGasPriceCaps(ctx)
if err != nil {
return nil, err
}
log.Debug("Suggested gas price", "gasTipCap", gasTipCap, "baseFee", baseFee, "blobBaseFee", blobBaseFee)

gasPrice := new(big.Int).Add(baseFee, gasTipCap)
gasUsed, err := b.rpc.L1.EstimateGas(ctx, ethereum.CallMsg{
From: txmgr.From(),
To: candidate.To,
Gas: candidate.GasLimit,
GasPrice: gasPrice,
GasFeeCap: gasPrice,
GasTipCap: gasTipCap,
Value: candidate.Value,
Data: candidate.TxData,
})
if err != nil {
return nil, fmt.Errorf("failed to estimate gas used: %w", err)
}

feeWithoutBlob := new(big.Int).Mul(gasPrice, new(big.Int).SetUint64(gasUsed))

// If its a type-2 transaction, we won't calculate blob fee.
if len(candidate.Blobs) == 0 {
return feeWithoutBlob, nil
}

// Otherwise, we add blob fee to the cost.
return new(big.Int).Add(
feeWithoutBlob,
new(big.Int).Mul(new(big.Int).SetUint64(uint64(len(candidate.Blobs))), blobBaseFee),
), nil
}

// TxBuilderWithFallback returns whether the blob transactions is enabled.
func (b *TxBuilderWithFallback) BlobAllow() bool {
return b.blobTransactionBuilder != nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package builder

import (
"context"
"os"
"time"

"github.com/ethereum-optimism/optimism/op-service/txmgr"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/log"

"github.com/taikoxyz/taiko-mono/packages/taiko-client/internal/metrics"
"github.com/taikoxyz/taiko-mono/packages/taiko-client/pkg/config"
"github.com/taikoxyz/taiko-mono/packages/taiko-client/pkg/rpc"
"github.com/taikoxyz/taiko-mono/packages/taiko-client/pkg/utils"
)

func (s *TransactionBuilderTestSuite) TestBuildCalldataOnly() {
builder := s.newTestBuilderWithFallback(false, false)
candidate, err := builder.BuildOntake(context.Background(), [][]byte{{1}, {2}})
s.Nil(err)
s.Zero(len(candidate.Blobs))
}

func (s *TransactionBuilderTestSuite) TestBuildCalldataWithBlobAllowed() {
builder := s.newTestBuilderWithFallback(true, false)
candidate, err := builder.BuildOntake(context.Background(), [][]byte{{1}, {2}})
s.Nil(err)
s.NotZero(len(candidate.Blobs))
}

func (s *TransactionBuilderTestSuite) newTestBuilderWithFallback(blobAllowed, fallback bool) *TxBuilderWithFallback {
l1ProposerPrivKey, err := crypto.ToECDSA(common.FromHex(os.Getenv("L1_PROPOSER_PRIVATE_KEY")))
s.Nil(err)

protocolConfigs, err := rpc.GetProtocolConfigs(s.RPCClient.TaikoL1, nil)
s.Nil(err)

chainConfig := config.NewChainConfig(&protocolConfigs)

txMgr, err := txmgr.NewSimpleTxManager(
"tx_builder_test",
log.Root(),
&metrics.TxMgrMetrics,
txmgr.CLIConfig{
L1RPCURL: os.Getenv("L1_WS"),
NumConfirmations: 0,
SafeAbortNonceTooLowCount: txmgr.DefaultBatcherFlagValues.SafeAbortNonceTooLowCount,
PrivateKey: common.Bytes2Hex(crypto.FromECDSA(l1ProposerPrivKey)),
FeeLimitMultiplier: txmgr.DefaultBatcherFlagValues.FeeLimitMultiplier,
FeeLimitThresholdGwei: txmgr.DefaultBatcherFlagValues.FeeLimitThresholdGwei,
MinBaseFeeGwei: txmgr.DefaultBatcherFlagValues.MinBaseFeeGwei,
MinTipCapGwei: txmgr.DefaultBatcherFlagValues.MinTipCapGwei,
ResubmissionTimeout: txmgr.DefaultBatcherFlagValues.ResubmissionTimeout,
ReceiptQueryInterval: 1 * time.Second,
NetworkTimeout: txmgr.DefaultBatcherFlagValues.NetworkTimeout,
TxSendTimeout: txmgr.DefaultBatcherFlagValues.TxSendTimeout,
TxNotInMempoolTimeout: txmgr.DefaultBatcherFlagValues.TxNotInMempoolTimeout,
},
)

s.Nil(err)

txmgrSelector := utils.NewTxMgrSelector(txMgr, nil, nil)

return NewBuilderWithFallback(
s.RPCClient,
l1ProposerPrivKey,
common.HexToAddress(os.Getenv("TAIKO_L2")),
common.HexToAddress(os.Getenv("TAIKO_L1")),
common.Address{},
10_000_000,
"test_fallback_builder",
chainConfig,
txmgrSelector,
true,
blobAllowed,
fallback,
)
}
Loading