diff --git a/cmd/interchaincmd/messengercmd/deploy.go b/cmd/interchaincmd/messengercmd/deploy.go new file mode 100644 index 000000000..746ab9d55 --- /dev/null +++ b/cmd/interchaincmd/messengercmd/deploy.go @@ -0,0 +1,273 @@ +// Copyright (C) 2022, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. +package messengercmd + +import ( + "fmt" + + "github.com/ava-labs/avalanche-cli/pkg/cobrautils" + "github.com/ava-labs/avalanche-cli/pkg/contract" + "github.com/ava-labs/avalanche-cli/pkg/localnet" + "github.com/ava-labs/avalanche-cli/pkg/models" + "github.com/ava-labs/avalanche-cli/pkg/networkoptions" + "github.com/ava-labs/avalanche-cli/pkg/prompts" + "github.com/ava-labs/avalanche-cli/pkg/teleporter" + "github.com/ava-labs/avalanche-cli/pkg/ux" + "github.com/ava-labs/avalanchego/utils/logging" + + "github.com/spf13/cobra" +) + +type DeployFlags struct { + Network networkoptions.NetworkFlags + ChainFlags contract.ChainSpec + KeyName string + GenesisKey bool + DeployMessenger bool + DeployRegistry bool + ForceRegistryDeploy bool + RPCURL string + Version string + MessengerContractAddressPath string + MessengerDeployerAddressPath string + MessengerDeployerTxPath string + RegistryBydecodePath string + PrivateKeyFlags contract.PrivateKeyFlags + IncludeCChain bool + CChainKeyName string +} + +const ( + cChainAlias = "C" + cChainName = "c-chain" +) + +var ( + deploySupportedNetworkOptions = []networkoptions.NetworkOption{ + networkoptions.Local, + networkoptions.Devnet, + networkoptions.Fuji, + } + deployFlags DeployFlags +) + +// avalanche interchain messenger deploy +func NewDeployCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "deploy", + Short: "Deploys ICM Messenger and Registry into a given L1", + Long: `Deploys ICM Messenger and Registry into a given L1.`, + RunE: deploy, + Args: cobrautils.ExactArgs(0), + } + networkoptions.AddNetworkFlagsToCmd(cmd, &deployFlags.Network, true, deploySupportedNetworkOptions) + deployFlags.PrivateKeyFlags.AddToCmd(cmd, "to fund ICM deploy") + deployFlags.ChainFlags.SetEnabled(true, true, false, false, true) + deployFlags.ChainFlags.AddToCmd(cmd, "deploy ICM into %s") + cmd.Flags().BoolVar(&deployFlags.DeployMessenger, "deploy-messenger", true, "deploy ICM Messenger") + cmd.Flags().BoolVar(&deployFlags.DeployRegistry, "deploy-registry", true, "deploy ICM Registry") + cmd.Flags().BoolVar(&deployFlags.ForceRegistryDeploy, "force-registry-deploy", false, "deploy ICM Registry even if Messenger has already been deployed") + cmd.Flags().StringVar(&deployFlags.RPCURL, "rpc-url", "", "use the given RPC URL to connect to the subnet") + cmd.Flags().StringVar(&deployFlags.Version, "version", "latest", "version to deploy") + cmd.Flags().StringVar(&deployFlags.MessengerContractAddressPath, "messenger-contract-address-path", "", "path to a messenger contract address file") + cmd.Flags().StringVar(&deployFlags.MessengerDeployerAddressPath, "messenger-deployer-address-path", "", "path to a messenger deployer address file") + cmd.Flags().StringVar(&deployFlags.MessengerDeployerTxPath, "messenger-deployer-tx-path", "", "path to a messenger deployer tx file") + cmd.Flags().StringVar(&deployFlags.RegistryBydecodePath, "registry-bytecode-path", "", "path to a registry bytecode file") + cmd.Flags().BoolVar(&deployFlags.IncludeCChain, "include-cchain", false, "deploy ICM also to C-Chain") + cmd.Flags().StringVar(&deployFlags.CChainKeyName, "cchain-key", "", "key to be used to pay fees to deploy ICM to C-Chain") + return cmd +} + +func deploy(_ *cobra.Command, args []string) error { + return CallDeploy(args, deployFlags, models.UndefinedNetwork) +} + +func CallDeploy(_ []string, flags DeployFlags, network models.Network) error { + var err error + if network == models.UndefinedNetwork { + network, err = networkoptions.GetNetworkFromCmdLineFlags( + app, + "On what Network do you want to deploy the ICM Messenger?", + flags.Network, + true, + false, + deploySupportedNetworkOptions, + "", + ) + if err != nil { + return err + } + } + if err := flags.ChainFlags.CheckMutuallyExclusiveFields(); err != nil { + return err + } + if !flags.DeployMessenger && !flags.DeployRegistry { + return fmt.Errorf("you should set at least one of --deploy-messenger/--deploy-registry to true") + } + if !flags.ChainFlags.Defined() { + prompt := "Which Blockchain would you like to deploy ICM to?" + if cancel, err := contract.PromptChain( + app, + network, + prompt, + "", + &flags.ChainFlags, + ); err != nil { + return err + } else if cancel { + return nil + } + } + rpcURL := flags.RPCURL + if rpcURL == "" { + rpcURL, _, err = contract.GetBlockchainEndpoints(app, network, flags.ChainFlags, true, false) + if err != nil { + return err + } + ux.Logger.PrintToUser(logging.Yellow.Wrap("RPC Endpoint: %s"), rpcURL) + } + + genesisAddress, genesisPrivateKey, err := contract.GetEVMSubnetPrefundedKey( + app, + network, + flags.ChainFlags, + ) + if err != nil { + return err + } + privateKey, err := flags.PrivateKeyFlags.GetPrivateKey(app, genesisPrivateKey) + if err != nil { + return err + } + if privateKey == "" { + privateKey, err = prompts.PromptPrivateKey( + app.Prompt, + "deploy ICM", + app.GetKeyDir(), + app.GetKey, + genesisAddress, + genesisPrivateKey, + ) + if err != nil { + return err + } + } + var icmVersion string + switch { + case flags.MessengerContractAddressPath != "" || flags.MessengerDeployerAddressPath != "" || flags.MessengerDeployerTxPath != "" || flags.RegistryBydecodePath != "": + if flags.MessengerContractAddressPath == "" || flags.MessengerDeployerAddressPath == "" || flags.MessengerDeployerTxPath == "" || flags.RegistryBydecodePath == "" { + return fmt.Errorf("if setting any ICM asset path, you must set all ICM asset paths") + } + case flags.Version != "" && flags.Version != "latest": + icmVersion = flags.Version + default: + icmInfo, err := teleporter.GetInfo(app) + if err != nil { + return err + } + icmVersion = icmInfo.Version + } + // deploy to subnet + td := teleporter.Deployer{} + if flags.MessengerContractAddressPath != "" { + if err := td.SetAssetsFromPaths( + flags.MessengerContractAddressPath, + flags.MessengerDeployerAddressPath, + flags.MessengerDeployerTxPath, + flags.RegistryBydecodePath, + ); err != nil { + return err + } + } else { + if err := td.DownloadAssets( + app.GetTeleporterBinDir(), + icmVersion, + ); err != nil { + return err + } + } + blockchainDesc, err := contract.GetBlockchainDesc(flags.ChainFlags) + if err != nil { + return err + } + alreadyDeployed, messengerAddress, registryAddress, err := td.Deploy( + blockchainDesc, + rpcURL, + privateKey, + flags.DeployMessenger, + flags.DeployRegistry, + flags.ForceRegistryDeploy, + ) + if err != nil { + return err + } + if flags.ChainFlags.BlockchainName != "" && (!alreadyDeployed || flags.ForceRegistryDeploy) { + // update sidecar + sc, err := app.LoadSidecar(flags.ChainFlags.BlockchainName) + if err != nil { + return fmt.Errorf("failed to load sidecar: %w", err) + } + sc.TeleporterReady = true + sc.TeleporterVersion = icmVersion + networkInfo := sc.Networks[network.Name()] + if messengerAddress != "" { + networkInfo.TeleporterMessengerAddress = messengerAddress + } + if registryAddress != "" { + networkInfo.TeleporterRegistryAddress = registryAddress + } + sc.Networks[network.Name()] = networkInfo + if err := app.UpdateSidecar(&sc); err != nil { + return err + } + } + // automatic deploy to cchain for local + if !flags.ChainFlags.CChain && (network.Kind == models.Local || flags.IncludeCChain) { + if flags.CChainKeyName == "" { + flags.CChainKeyName = "ewoq" + } + ewoq, err := app.GetKey(flags.CChainKeyName, network, false) + if err != nil { + return err + } + alreadyDeployed, messengerAddress, registryAddress, err := td.Deploy( + cChainName, + network.BlockchainEndpoint(cChainAlias), + ewoq.PrivKeyHex(), + flags.DeployMessenger, + flags.DeployRegistry, + false, + ) + if err != nil { + return err + } + if !alreadyDeployed { + if network.Kind == models.Local { + if err := localnet.WriteExtraLocalNetworkData( + "", + "", + messengerAddress, + registryAddress, + ); err != nil { + return err + } + } + if network.ClusterName != "" { + clusterConfig, err := app.GetClusterConfig(network.ClusterName) + if err != nil { + return err + } + if messengerAddress != "" { + clusterConfig.ExtraNetworkData.CChainTeleporterMessengerAddress = messengerAddress + } + if registryAddress != "" { + clusterConfig.ExtraNetworkData.CChainTeleporterRegistryAddress = registryAddress + } + if err := app.SetClusterConfig(network.ClusterName, clusterConfig); err != nil { + return err + } + } + } + } + return nil +} diff --git a/cmd/interchaincmd/messengercmd/messenger.go b/cmd/interchaincmd/messengercmd/messenger.go new file mode 100644 index 000000000..04082ad88 --- /dev/null +++ b/cmd/interchaincmd/messengercmd/messenger.go @@ -0,0 +1,28 @@ +// Copyright (C) 2022, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. +package messengercmd + +import ( + "github.com/ava-labs/avalanche-cli/pkg/application" + "github.com/ava-labs/avalanche-cli/pkg/cobrautils" + "github.com/spf13/cobra" +) + +var app *application.Avalanche + +// avalanche interchain messenger +func NewCmd(injectedApp *application.Avalanche) *cobra.Command { + cmd := &cobra.Command{ + Use: "messenger", + Short: "Interact with ICM messenger contracts", + Long: `The messenger command suite provides a collection of tools for interacting +with ICM messenger contracts.`, + RunE: cobrautils.CommandSuiteUsage, + } + app = injectedApp + // interchain messenger sendMsg + cmd.AddCommand(NewSendMsgCmd()) + // interchain messenger deploy + cmd.AddCommand(NewDeployCmd()) + return cmd +} diff --git a/cmd/interchaincmd/messengercmd/send_msg.go b/cmd/interchaincmd/messengercmd/send_msg.go new file mode 100644 index 000000000..48e2ace85 --- /dev/null +++ b/cmd/interchaincmd/messengercmd/send_msg.go @@ -0,0 +1,237 @@ +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. +package messengercmd + +import ( + "fmt" + "strings" + "time" + + "github.com/ava-labs/avalanche-cli/pkg/cobrautils" + "github.com/ava-labs/avalanche-cli/pkg/contract" + "github.com/ava-labs/avalanche-cli/pkg/evm" + "github.com/ava-labs/avalanche-cli/pkg/networkoptions" + "github.com/ava-labs/avalanche-cli/pkg/prompts" + "github.com/ava-labs/avalanche-cli/pkg/teleporter" + "github.com/ava-labs/avalanche-cli/pkg/ux" + "github.com/ava-labs/avalanchego/ids" + "github.com/ethereum/go-ethereum/common" + + "github.com/spf13/cobra" +) + +type MsgFlags struct { + Network networkoptions.NetworkFlags + DestinationAddress string + HexEncodedMessage bool + PrivateKeyFlags contract.PrivateKeyFlags + SourceRPCEndpoint string + DestRPCEndpoint string +} + +var ( + msgSupportedNetworkOptions = []networkoptions.NetworkOption{ + networkoptions.Local, + networkoptions.Devnet, + networkoptions.EtnaDevnet, + networkoptions.Fuji, + } + msgFlags MsgFlags +) + +// avalanche interchain messenger sendMsg +func NewSendMsgCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "sendMsg [sourceBlockchainName] [destinationBlockchainName] [messageContent]", + Short: "Verifies exchange of ICM message between two subnets", + Long: `Sends and wait reception for a ICM msg between two subnets.`, + RunE: sendMsg, + Args: cobrautils.ExactArgs(3), + } + networkoptions.AddNetworkFlagsToCmd(cmd, &msgFlags.Network, true, msgSupportedNetworkOptions) + msgFlags.PrivateKeyFlags.AddToCmd(cmd, "as message originator and to pay source blockchain fees") + cmd.Flags().BoolVar(&msgFlags.HexEncodedMessage, "hex-encoded", false, "given message is hex encoded") + cmd.Flags().StringVar(&msgFlags.DestinationAddress, "destination-address", "", "deliver the message to the given contract destination address") + cmd.Flags().StringVar(&msgFlags.SourceRPCEndpoint, "source-rpc", "", "use the given source blockchain rpc endpoint") + cmd.Flags().StringVar(&msgFlags.DestRPCEndpoint, "dest-rpc", "", "use the given destination blockchain rpc endpoint") + return cmd +} + +func sendMsg(_ *cobra.Command, args []string) error { + sourceBlockchainName := args[0] + destBlockchainName := args[1] + message := args[2] + + network, err := networkoptions.GetNetworkFromCmdLineFlags( + app, + "", + msgFlags.Network, + true, + false, + msgSupportedNetworkOptions, + "", + ) + if err != nil { + return err + } + + sourceChainSpec := contract.ChainSpec{} + if isCChain(sourceBlockchainName) { + sourceChainSpec.CChain = true + } else { + sourceChainSpec.BlockchainName = sourceBlockchainName + } + sourceRPCEndpoint := msgFlags.SourceRPCEndpoint + if sourceRPCEndpoint == "" { + sourceRPCEndpoint, _, err = contract.GetBlockchainEndpoints(app, network, sourceChainSpec, true, false) + if err != nil { + return err + } + } + + destChainSpec := contract.ChainSpec{} + if isCChain(destBlockchainName) { + destChainSpec.CChain = true + } else { + destChainSpec.BlockchainName = destBlockchainName + } + destRPCEndpoint := msgFlags.DestRPCEndpoint + if destRPCEndpoint == "" { + destRPCEndpoint, _, err = contract.GetBlockchainEndpoints(app, network, destChainSpec, true, false) + if err != nil { + return err + } + } + + genesisAddress, genesisPrivateKey, err := contract.GetEVMSubnetPrefundedKey( + app, + network, + contract.ChainSpec{ + BlockchainName: sourceBlockchainName, + CChain: isCChain(sourceBlockchainName), + }, + ) + if err != nil { + return err + } + privateKey, err := msgFlags.PrivateKeyFlags.GetPrivateKey(app, genesisPrivateKey) + if err != nil { + return err + } + if privateKey == "" { + privateKey, err = prompts.PromptPrivateKey( + app.Prompt, + "pay for fees at source blockchain", + app.GetKeyDir(), + app.GetKey, + genesisAddress, + genesisPrivateKey, + ) + if err != nil { + return err + } + } + + sourceBlockchainID, err := contract.GetBlockchainID(app, network, sourceChainSpec) + if err != nil { + return err + } + _, sourceMessengerAddress, err := contract.GetICMInfo(app, network, sourceChainSpec, false, false, true) + if err != nil { + return err + } + destBlockchainID, err := contract.GetBlockchainID(app, network, destChainSpec) + if err != nil { + return err + } + _, destMessengerAddress, err := contract.GetICMInfo(app, network, destChainSpec, false, false, true) + if err != nil { + return err + } + + if sourceMessengerAddress != destMessengerAddress { + return fmt.Errorf("different ICM messenger addresses among subnets: %s vs %s", sourceMessengerAddress, destMessengerAddress) + } + + encodedMessage := []byte(message) + if msgFlags.HexEncodedMessage { + encodedMessage = common.FromHex(message) + } + destAddr := common.Address{} + if msgFlags.DestinationAddress != "" { + if err := prompts.ValidateAddress(msgFlags.DestinationAddress); err != nil { + return fmt.Errorf("failure validating address %s: %w", msgFlags.DestinationAddress, err) + } + destAddr = common.HexToAddress(msgFlags.DestinationAddress) + } + // send tx to the ICM contract at the source + ux.Logger.PrintToUser("Delivering message %q from source subnet %q (%s)", message, sourceBlockchainName, sourceBlockchainID) + tx, receipt, err := teleporter.SendCrossChainMessage( + sourceRPCEndpoint, + common.HexToAddress(sourceMessengerAddress), + privateKey, + destBlockchainID, + destAddr, + encodedMessage, + ) + if err != nil { + return err + } + if err == contract.ErrFailedReceiptStatus { + txHash := tx.Hash().String() + ux.Logger.PrintToUser("error: source receipt status for tx %s is not ReceiptStatusSuccessful", txHash) + trace, err := evm.GetTrace(sourceRPCEndpoint, txHash) + if err != nil { + ux.Logger.PrintToUser("error obtaining tx trace: %s", err) + ux.Logger.PrintToUser("") + } else { + ux.Logger.PrintToUser("") + ux.Logger.PrintToUser("trace: %#v", trace) + ux.Logger.PrintToUser("") + } + return fmt.Errorf("source receipt status for tx %s is not ReceiptStatusSuccessful", txHash) + } + + event, err := evm.GetEventFromLogs(receipt.Logs, teleporter.ParseSendCrossChainMessage) + if err != nil { + return err + } + + if destBlockchainID != ids.ID(event.DestinationBlockchainID[:]) { + return fmt.Errorf("invalid destination blockchain id at source event, expected %s, got %s", destBlockchainID, ids.ID(event.DestinationBlockchainID[:])) + } + if message != string(event.Message.Message) { + return fmt.Errorf("invalid message content at source event, expected %s, got %s", message, string(event.Message.Message)) + } + + // receive and process head from destination + ux.Logger.PrintToUser("Waiting for message to be delivered to destination subnet %q (%s)", destBlockchainName, destBlockchainID) + + arrivalCheckInterval := 100 * time.Millisecond + arrivalCheckTimeout := 10 * time.Second + t0 := time.Now() + for { + if b, err := teleporter.MessageReceived( + destRPCEndpoint, + common.HexToAddress(destMessengerAddress), + event.MessageID, + ); err != nil { + return err + } else if b { + break + } + elapsed := time.Since(t0) + if elapsed > arrivalCheckTimeout { + return fmt.Errorf("timeout waiting for message to be teleported") + } + time.Sleep(arrivalCheckInterval) + } + + ux.Logger.PrintToUser("Message successfully Teleported!") + + return nil +} + +func isCChain(subnetName string) bool { + return strings.ToLower(subnetName) == "c-chain" || strings.ToLower(subnetName) == "cchain" +}