diff --git a/packages/taiko-client/cmd/flags/proposer.go b/packages/taiko-client/cmd/flags/proposer.go index fef22d7a1fb..63b0934cb00 100644 --- a/packages/taiko-client/cmd/flags/proposer.go +++ b/packages/taiko-client/cmd/flags/proposer.go @@ -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", @@ -133,5 +140,6 @@ var ProposerFlags = MergeFlags(CommonFlags, []cli.Flag{ AllowZeroInterval, MaxProposedTxListsPerEpoch, BlobAllowed, + FallbackToCalldata, RevertProtectionEnabled, }, TxmgrFlags) diff --git a/packages/taiko-client/internal/metrics/metrics.go b/packages/taiko-client/internal/metrics/metrics.go index 27f4da07724..0b2fdced5ad 100644 --- a/packages/taiko-client/internal/metrics/metrics.go +++ b/packages/taiko-client/internal/metrics/metrics.go @@ -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"}) diff --git a/packages/taiko-client/pkg/rpc/methods.go b/packages/taiko-client/pkg/rpc/methods.go index 5baf90c88a8..109d7f5f53d 100644 --- a/packages/taiko-client/pkg/rpc/methods.go +++ b/packages/taiko-client/pkg/rpc/methods.go @@ -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 diff --git a/packages/taiko-client/proposer/config.go b/packages/taiko-client/proposer/config.go index 72b755fe4be..74f77151f45 100644 --- a/packages/taiko-client/proposer/config.go +++ b/packages/taiko-client/proposer/config.go @@ -35,6 +35,7 @@ type Config struct { MaxProposedTxListsPerEpoch uint64 ProposeBlockTxGasLimit uint64 BlobAllowed bool + FallbackToCalldata bool RevertProtectionEnabled bool TxmgrConfigs *txmgr.CLIConfig PrivateTxmgrConfigs *txmgr.CLIConfig @@ -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), diff --git a/packages/taiko-client/proposer/proposer.go b/packages/taiko-client/proposer/proposer.go index 35c2dfc5a3b..db2914cd5e9 100644 --- a/packages/taiko-client/proposer/proposer.go +++ b/packages/taiko-client/proposer/proposer.go @@ -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 } diff --git a/packages/taiko-client/proposer/proposer_test.go b/packages/taiko-client/proposer/proposer_test.go index c81bfa2415a..c6f80a2dc65 100644 --- a/packages/taiko-client/proposer/proposer_test.go +++ b/packages/taiko-client/proposer/proposer_test.go @@ -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, diff --git a/packages/taiko-client/proposer/transaction_builder/fallback.go b/packages/taiko-client/proposer/transaction_builder/fallback.go new file mode 100644 index 00000000000..a97970f431e --- /dev/null +++ b/packages/taiko-client/proposer/transaction_builder/fallback.go @@ -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 +} diff --git a/packages/taiko-client/proposer/transaction_builder/fallback_test.go b/packages/taiko-client/proposer/transaction_builder/fallback_test.go new file mode 100644 index 00000000000..3fcd0eb63c1 --- /dev/null +++ b/packages/taiko-client/proposer/transaction_builder/fallback_test.go @@ -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, + ) +}