From 09d79acc9b9c0acaf50c0f941b4a0f6fe7fb6b91 Mon Sep 17 00:00:00 2001 From: xiaodino Date: Fri, 22 Mar 2024 17:35:24 -0700 Subject: [PATCH] feat(relayer): add the bridge software to send bridge txs (#16498) --- packages/relayer/bridge.go | 1 + packages/relayer/bridge/bridge.go | 252 +++++++++++++++++++++++++++ packages/relayer/bridge/config.go | 68 ++++++++ packages/relayer/cmd/flags/bridge.go | 30 ++++ packages/relayer/cmd/flags/common.go | 1 + packages/relayer/cmd/main.go | 8 + packages/relayer/pkg/mock/bridge.go | 4 + 7 files changed, 364 insertions(+) create mode 100644 packages/relayer/bridge/bridge.go create mode 100644 packages/relayer/bridge/config.go create mode 100644 packages/relayer/cmd/flags/bridge.go diff --git a/packages/relayer/bridge.go b/packages/relayer/bridge.go index 85b01213d05..47612d59771 100644 --- a/packages/relayer/bridge.go +++ b/packages/relayer/bridge.go @@ -29,5 +29,6 @@ type Bridge interface { InvocationDelay *big.Int InvocationExtraDelay *big.Int }, error) + SendMessage(opts *bind.TransactOpts, _message bridge.IBridgeMessage) (*types.Transaction, error) SuspendMessages(opts *bind.TransactOpts, _msgHashes [][32]byte, _toSuspend bool) (*types.Transaction, error) } diff --git a/packages/relayer/bridge/bridge.go b/packages/relayer/bridge/bridge.go new file mode 100644 index 00000000000..68e16e7dad7 --- /dev/null +++ b/packages/relayer/bridge/bridge.go @@ -0,0 +1,252 @@ +package bridge + +import ( + "context" + "crypto/ecdsa" + "encoding/hex" + "fmt" + "math/big" + "sync" + "time" + + "github.com/cyberhorsey/errors" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/urfave/cli/v2" + "golang.org/x/exp/slog" + + "github.com/taikoxyz/taiko-mono/packages/relayer" + "github.com/taikoxyz/taiko-mono/packages/relayer/bindings/bridge" +) + +type ethClient interface { + PendingNonceAt(ctx context.Context, account common.Address) (uint64, error) + TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) + BlockNumber(ctx context.Context) (uint64, error) + BlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error) + BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) + HeaderByHash(ctx context.Context, hash common.Hash) (*types.Header, error) + SuggestGasPrice(ctx context.Context) (*big.Int, error) + SuggestGasTipCap(ctx context.Context) (*big.Int, error) + ChainID(ctx context.Context) (*big.Int, error) +} + +type Bridge struct { + cancel context.CancelFunc + + srcEthClient ethClient + destEthClient ethClient + + ecdsaKey *ecdsa.PrivateKey + + srcBridge relayer.Bridge + destBridge relayer.Bridge + + mu *sync.Mutex + + addr common.Address + + backOffRetryInterval time.Duration + backOffMaxRetries uint64 + ethClientTimeout time.Duration + + wg *sync.WaitGroup + + srcChainId *big.Int + destChainId *big.Int + + bridgeMessageValue *big.Int +} + +func (b *Bridge) InitFromCli(ctx context.Context, c *cli.Context) error { + cfg, err := NewConfigFromCliContext(c) + if err != nil { + return err + } + + return InitFromConfig(ctx, b, cfg) +} + +// nolint: funlen +func InitFromConfig(ctx context.Context, b *Bridge, cfg *Config) error { + srcEthClient, err := ethclient.Dial(cfg.SrcRPCUrl) + if err != nil { + return err + } + + destEthClient, err := ethclient.Dial(cfg.DestRPCUrl) + if err != nil { + return err + } + + srcBridge, err := bridge.NewBridge(cfg.SrcBridgeAddress, srcEthClient) + if err != nil { + return err + } + + destBridge, err := bridge.NewBridge(cfg.DestBridgeAddress, destEthClient) + if err != nil { + return err + } + + srcChainID, err := srcEthClient.ChainID(context.Background()) + if err != nil { + return err + } + + destChainID, err := destEthClient.ChainID(context.Background()) + if err != nil { + return err + } + + publicKey := cfg.BridgePrivateKey.Public() + + publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey) + if !ok { + return errors.New("unable to convert public key") + } + + b.srcEthClient = srcEthClient + b.destEthClient = destEthClient + + b.destBridge = destBridge + b.srcBridge = srcBridge + + b.ecdsaKey = cfg.BridgePrivateKey + b.addr = crypto.PubkeyToAddress(*publicKeyECDSA) + + b.srcChainId = srcChainID + b.destChainId = destChainID + + b.wg = &sync.WaitGroup{} + b.mu = &sync.Mutex{} + + b.backOffRetryInterval = time.Duration(cfg.BackoffRetryInterval) * time.Second + b.backOffMaxRetries = cfg.BackOffMaxRetrys + b.ethClientTimeout = time.Duration(cfg.ETHClientTimeout) * time.Second + + b.bridgeMessageValue = cfg.BridgeMessageValue + + return nil +} + +func (b *Bridge) Name() string { + return "bridge" +} + +func (b *Bridge) Close(ctx context.Context) { + b.cancel() + + b.wg.Wait() +} + +func (b *Bridge) Start() error { + slog.Info("Start bridge") + + ctx, cancel := context.WithCancel(context.Background()) + + b.cancel = cancel + + _ = b.submitBridgeTx(ctx) + + return nil +} + +func (b *Bridge) setLatestNonce(ctx context.Context, auth *bind.TransactOpts) error { + pendingNonce, err := b.srcEthClient.PendingNonceAt(ctx, b.addr) + if err != nil { + return err + } + + auth.Nonce = big.NewInt(int64(pendingNonce)) + + return nil +} + +func (b *Bridge) estimateGas( + ctx context.Context, message bridge.IBridgeMessage) (uint64, error) { + auth, err := bind.NewKeyedTransactorWithChainID(b.ecdsaKey, new(big.Int).SetUint64(message.SrcChainId)) + if err != nil { + return 0, errors.Wrap(err, "bind.NewKeyedTransactorWithChainID") + } + + // estimate gas with auth.NoSend set to true + auth.NoSend = true + auth.Context = ctx + auth.GasLimit = 500000 + + tx, err := b.srcBridge.SendMessage(auth, message) + if err != nil { + fmt.Println(err) + return 0, errors.Wrap(err, "rcBridge.SendMessage") + } + + gasPaddingAmt := uint64(80000) + + return tx.Gas() + gasPaddingAmt, nil +} + +func (b *Bridge) submitBridgeTx(ctx context.Context) error { + srcChainId, err := b.srcEthClient.ChainID(ctx) + if err != nil { + return errors.Wrap(err, "b.srcEthClient.ChainID") + } + + destChainId, err := b.destEthClient.ChainID(ctx) + if err != nil { + return errors.Wrap(err, "b.destEthClient.ChainID") + } + + auth, err := bind.NewKeyedTransactorWithChainID(b.ecdsaKey, new(big.Int).SetUint64(srcChainId.Uint64())) + if err != nil { + return errors.Wrap(err, "b.NewKeyedTransactorWithChainID") + } + + auth.Context = ctx + + err = b.setLatestNonce(ctx, auth) + if err != nil { + return errors.New("b.setLatestNonce") + } + + processingFee := big.NewInt(10000) + value := new(big.Int) + value.Add(b.bridgeMessageValue, processingFee) + auth.Value = value + + message := bridge.IBridgeMessage{ + Id: big.NewInt(0), + From: b.addr, + SrcChainId: srcChainId.Uint64(), + DestChainId: destChainId.Uint64(), + SrcOwner: b.addr, + DestOwner: b.addr, + To: b.addr, + RefundTo: b.addr, + Value: b.bridgeMessageValue, + Fee: processingFee, + GasLimit: big.NewInt(140000), + Data: []byte{}, + Memo: "", + } + + gas, err := b.estimateGas(ctx, message) + if err != nil || gas == 0 { + slog.Info("gas estimation failed, hardcoding gas limit", "b.estimateGas:", err) + } + + auth.GasLimit = gas + + tx, err := b.srcBridge.SendMessage(auth, message) + if err != nil { + fmt.Println("b.srcBridge.SendMessage", err) + return errors.Wrap(err, "rcBridge.SendMessage") + } + + slog.Info("Sent tx", "txHash", hex.EncodeToString(tx.Hash().Bytes())) + + return nil +} diff --git a/packages/relayer/bridge/config.go b/packages/relayer/bridge/config.go new file mode 100644 index 00000000000..7ac417148fe --- /dev/null +++ b/packages/relayer/bridge/config.go @@ -0,0 +1,68 @@ +package bridge + +import ( + "crypto/ecdsa" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/taikoxyz/taiko-mono/packages/relayer/cmd/flags" + "github.com/urfave/cli/v2" +) + +type Config struct { + // address configs + SrcBridgeAddress common.Address + DestBridgeAddress common.Address + + // private key + BridgePrivateKey *ecdsa.PrivateKey + + // processing configs + Confirmations uint64 + ConfirmationsTimeout uint64 + EnableTaikoL2 bool + + // backoff configs + BackoffRetryInterval uint64 + BackOffMaxRetrys uint64 + + // rpc configs + SrcRPCUrl string + DestRPCUrl string + ETHClientTimeout uint64 + + // BridgeMessage + BridgeMessageValue *big.Int +} + +// NewConfigFromCliContext creates a new config instance from command line flags. +func NewConfigFromCliContext(c *cli.Context) (*Config, error) { + bridgePrivateKey, err := crypto.ToECDSA( + common.Hex2Bytes(c.String(flags.BridgePrivateKey.Name)), + ) + if err != nil { + return nil, fmt.Errorf("invalid bridgePrivateKey: %w", err) + } + + bridgeMessageValue, ok := new(big.Int).SetString(c.String(flags.BridgeMessageValue.Name), 10) + if !ok { + return nil, fmt.Errorf("invalid bridgeMessageValue") + } + + return &Config{ + BridgePrivateKey: bridgePrivateKey, + DestBridgeAddress: common.HexToAddress(c.String(flags.DestBridgeAddress.Name)), + SrcBridgeAddress: common.HexToAddress(c.String(flags.SrcBridgeAddress.Name)), + SrcRPCUrl: c.String(flags.SrcRPCUrl.Name), + DestRPCUrl: c.String(flags.DestRPCUrl.Name), + Confirmations: c.Uint64(flags.Confirmations.Name), + ConfirmationsTimeout: c.Uint64(flags.ConfirmationTimeout.Name), + EnableTaikoL2: c.Bool(flags.EnableTaikoL2.Name), + BackoffRetryInterval: c.Uint64(flags.BackOffRetryInterval.Name), + BackOffMaxRetrys: c.Uint64(flags.BackOffMaxRetrys.Name), + ETHClientTimeout: c.Uint64(flags.ETHClientTimeout.Name), + BridgeMessageValue: bridgeMessageValue, + }, nil +} diff --git a/packages/relayer/cmd/flags/bridge.go b/packages/relayer/cmd/flags/bridge.go new file mode 100644 index 00000000000..4fec1b83d0a --- /dev/null +++ b/packages/relayer/cmd/flags/bridge.go @@ -0,0 +1,30 @@ +package flags + +import ( + "github.com/urfave/cli/v2" +) + +var ( + BridgePrivateKey = &cli.StringFlag{ + Name: "bridgePrivateKey", + Usage: "Private key to send a bridge", + Required: true, + Category: bridegCategory, + EnvVars: []string{"BRIDGE_PRIVATE_KEY"}, + } + BridgeMessageValue = &cli.StringFlag{ + Name: "bridgeMessageValue", + Usage: "Value in the bridge message", + Required: true, + Category: bridegCategory, + EnvVars: []string{"BRIDGE_MESSAGE_VALUE"}, + } +) + +var BridgeFlags = MergeFlags(CommonFlags, QueueFlags, []cli.Flag{ + BridgePrivateKey, + BridgeMessageValue, + SrcBridgeAddress, + DestBridgeAddress, + SrcTaikoAddress, +}) diff --git a/packages/relayer/cmd/flags/common.go b/packages/relayer/cmd/flags/common.go index 408052db536..a5f58af3f0f 100644 --- a/packages/relayer/cmd/flags/common.go +++ b/packages/relayer/cmd/flags/common.go @@ -9,6 +9,7 @@ var ( indexerCategory = "INDEXER" processorCategory = "PROCESSOR" watchdogCategory = "WATCHDOG" + bridegCategory = "BRIDGE" ) var ( diff --git a/packages/relayer/cmd/main.go b/packages/relayer/cmd/main.go index 12230a18a3f..735652155e0 100644 --- a/packages/relayer/cmd/main.go +++ b/packages/relayer/cmd/main.go @@ -7,6 +7,7 @@ import ( "github.com/joho/godotenv" "github.com/taikoxyz/taiko-mono/packages/relayer/api" + "github.com/taikoxyz/taiko-mono/packages/relayer/bridge" "github.com/taikoxyz/taiko-mono/packages/relayer/cmd/flags" "github.com/taikoxyz/taiko-mono/packages/relayer/cmd/utils" "github.com/taikoxyz/taiko-mono/packages/relayer/indexer" @@ -66,6 +67,13 @@ func main() { Description: "Taiko relayer watchdog software", Action: utils.SubcommandAction(new(watchdog.Watchdog)), }, + { + Name: "bridge", + Flags: flags.BridgeFlags, + Usage: "Starts the bridge software", + Description: "Taiko relayer bridge software", + Action: utils.SubcommandAction(new(bridge.Bridge)), + }, } if err := app.Run(os.Args); err != nil { diff --git a/packages/relayer/pkg/mock/bridge.go b/packages/relayer/pkg/mock/bridge.go index f7e560de32a..a738d60addb 100644 --- a/packages/relayer/pkg/mock/bridge.go +++ b/packages/relayer/pkg/mock/bridge.go @@ -123,3 +123,7 @@ func (b *Bridge) ProveMessageReceived( func (b *Bridge) ParseMessageSent(log types.Log) (*bridge.BridgeMessageSent, error) { return &bridge.BridgeMessageSent{}, nil } + +func (b *Bridge) SendMessage(opts *bind.TransactOpts, _message bridge.IBridgeMessage) (*types.Transaction, error) { + return ProcessMessageTx, nil +}