diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d76929a9..e3bc4d3f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,3 +1,3 @@ # https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners#codeowners-syntax -* @michaelkaplan13 @cam-schultz @minghinmatthewlam @gwen917 @geoff-vball @bernard-avalabs +* @ava-labs/interop \ No newline at end of file diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 1b89a884..1a17199a 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -21,17 +21,16 @@ jobs: uses: actions/checkout@v4 with: submodules: recursive - - - name: Set Go version - run: | - source ./scripts/versions.sh - echo GO_VERSION=$GO_VERSION >> $GITHUB_ENV - echo SUBNET_EVM_VERSION=$SUBNET_EVM_VERSION >> $GITHUB_ENV - name: Setup Go uses: actions/setup-go@v5 with: - go-version: ${{ env.GO_VERSION }} + go-version-file: 'go.mod' + + - name: Set subnet-evm version + run: | + source ./scripts/versions.sh + echo SUBNET_EVM_VERSION=$SUBNET_EVM_VERSION >> $GITHUB_ENV - name: Checkout subnet-evm repository uses: actions/checkout@v4 diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 9c99f558..089aa183 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -22,15 +22,10 @@ jobs: with: submodules: recursive - - name: Set Go version - run: | - source ./scripts/versions.sh - echo GO_VERSION=$GO_VERSION >> $GITHUB_ENV - - name: Setup Go uses: actions/setup-go@v5 with: - go-version: ${{ env.GO_VERSION }} + go-version-file: 'go.mod' - name: Install buf uses: bufbuild/buf-setup-action@v1.31.0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6272fe92..97c08a1e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,19 +15,18 @@ jobs: - name: Git checkout uses: actions/checkout@v4 with: - fetch-depth: 0 - path: awm-relayer submodules: recursive + # The GO_VERSION must be set explicitly to be used in the Dockerfile. - name: Set Go version run: | - source ./awm-relayer/scripts/versions.sh + source ./scripts/versions.sh echo GO_VERSION=$GO_VERSION >> $GITHUB_ENV - name: Set up Go uses: actions/setup-go@v5 with: - go-version: ${{ env.GO_VERSION }} + go-version-file: 'go.mod' - name: Set up arm64 cross compiler run: | @@ -70,8 +69,6 @@ jobs: distribution: goreleaser version: latest args: release --clean - workdir: ./awm-relayer/ env: # https://docs.github.com/en/actions/security-guides/automatic-token-authentication#about-the-github_token-secret GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GO_VERSION: ${{ env.GO_VERSION }} diff --git a/.github/workflows/snyk.yml b/.github/workflows/snyk.yml index a2a8284f..8934ccc2 100644 --- a/.github/workflows/snyk.yml +++ b/.github/workflows/snyk.yml @@ -17,7 +17,6 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 with: - path: awm-relayer submodules: recursive - name: Run Snyk diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 46b7ead2..a7de23a1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,16 +21,11 @@ jobs: uses: actions/checkout@v4 with: submodules: recursive - - - name: Set Go version - run: | - source ./scripts/versions.sh - echo GO_VERSION=$GO_VERSION >> $GITHUB_ENV - name: Setup Go uses: actions/setup-go@v5 with: - go-version: ${{ env.GO_VERSION }} + go-version-file: 'go.mod' - name: Run Relayer Unit Tests run: ./scripts/test.sh diff --git a/.gitignore b/.gitignore index 40211110..f820db91 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,6 @@ server.log # Foundry outputs cache/ out/ + +# Release build outputs +osxcross/ \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml index 3a7ec484..3a55826f 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -23,6 +23,7 @@ linters: - unconvert - unused - whitespace + - lll linters-settings: gofmt: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index eeed2395..37357164 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ ## Setup -To start developing on AWM Relayer, you'll need Golang >= v1.21.7. +To start developing on AWM Relayer, you'll need Golang v1.21.12. ## Issues diff --git a/README.md b/README.md index 5d04bdc3..e3f9e33b 100644 --- a/README.md +++ b/README.md @@ -199,11 +199,11 @@ The relayer is configured via a JSON file, the path to which is passed in via th `"source-blockchain-id": string` - - cb58-encoded blockchain ID of the source blockchain. + - cb58-encoded or "0x" prefixed hex-encoded blockchain ID of the source blockchain. `"destination-blockchain-id": string` - - cb58-encoded blockchain ID of the destination blockchain. + - cb58-encoded or "0x" prefixed hex-encoded blockchain ID of the destination blockchain. `"source-address": string` @@ -219,11 +219,11 @@ The relayer is configured via a JSON file, the path to which is passed in via th `"subnet-id": string` - - cb58-encoded Subnet ID. + - cb58-encoded or "0x" prefixed hex-encoded Subnet ID. `"blockchain-id": string` - - cb58-encoded blockchain ID. + - cb58-encoded or "0x" prefixed hex-encoded blockchain ID. `"vm": string` @@ -263,11 +263,11 @@ The relayer is configured via a JSON file, the path to which is passed in via th `"subnet-id": string` - - cb58-encoded Subnet ID. + - cb58-encoded or "0x" prefixed hex-encoded Subnet ID. `"blockchain-id": string` - - cb58-encoded blockchain ID. + - cb58-encoded or "0x" prefixed hex-encoded blockchain ID. `"vm": string` @@ -316,6 +316,54 @@ The relayer consists of the following components: +### API + +#### `/relay` +- Used to manually relay a Warp message. The body of the request must contain the following JSON: +```json +{ + "blockchain-id": "", + "message-id": "", + "block-num": "" +} +``` +- If successful, the endpoint will return the following JSON: +```json +{ + "transaction-hash": "" +} +``` + +#### `/relay/message` +- Used to manually relay a warp message. The body of the request must contain the following JSON: +```json +{ + "unsigned-message-bytes": "", + "source-address": "" +} +``` +- If successful, the endpoint will return the following JSON: +```json +{ + "transaction-hash": "", +} +``` + +#### `/health` +- Takes no arguments. Returns a `200` status code if all Application Relayers are healthy. Returns a `503` status if any of the Application Relayers have experienced an unrecoverable error. Here is an example return body: +```json +{ + "status": "down", + "details": { + "relayers-all": { + "status": "down", + "timestamp": "2024-06-01T05:06:07.685522Z", + "error": "" + } + } +} +``` + ## Testing ### Unit Tests diff --git a/api/health_check.go b/api/health_check.go new file mode 100644 index 00000000..1fafe3d8 --- /dev/null +++ b/api/health_check.go @@ -0,0 +1,42 @@ +package api + +import ( + "context" + "fmt" + "net/http" + + "github.com/alexliesenfeld/health" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/logging" + "go.uber.org/atomic" + "go.uber.org/zap" +) + +const HealthAPIPath = "/health" + +func HandleHealthCheck(logger logging.Logger, relayerHealth map[ids.ID]*atomic.Bool) { + http.Handle(HealthAPIPath, healthCheckHandler(logger, relayerHealth)) +} + +func healthCheckHandler(logger logging.Logger, relayerHealth map[ids.ID]*atomic.Bool) http.Handler { + return health.NewHandler(health.NewChecker( + health.WithCheck(health.Check{ + Name: "relayers-all", + Check: func(context.Context) error { + // Store the IDs as the cb58 encoding + var unhealthyRelayers []string + for id, health := range relayerHealth { + if !health.Load() { + unhealthyRelayers = append(unhealthyRelayers, id.String()) + } + } + + if len(unhealthyRelayers) > 0 { + logger.Fatal("Relayers are unhealthy for blockchains", zap.Strings("blockchains", unhealthyRelayers)) + return fmt.Errorf("relayers are unhealthy for blockchains %v", unhealthyRelayers) + } + return nil + }, + }), + )) +} diff --git a/api/relay_message.go b/api/relay_message.go new file mode 100644 index 00000000..8b0d479a --- /dev/null +++ b/api/relay_message.go @@ -0,0 +1,142 @@ +package api + +import ( + "encoding/json" + "math/big" + "net/http" + + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/awm-relayer/relayer" + "github.com/ava-labs/awm-relayer/types" + "github.com/ava-labs/awm-relayer/utils" + "github.com/ethereum/go-ethereum/common" + "go.uber.org/zap" +) + +const ( + RelayAPIPath = "/relay" + RelayMessageAPIPath = RelayAPIPath + "/message" +) + +type RelayMessageRequest struct { + // Required. cb58-encoded or "0x" prefixed hex-encoded source blockchain ID for the message + BlockchainID string `json:"blockchain-id"` + // Required. cb58-encoded or "0x" prefixed hex-encoded warp message ID + MessageID string `json:"message-id"` + // Required. Block number that the message was sent in + BlockNum uint64 `json:"block-num"` +} + +type RelayMessageResponse struct { + // hex encoding of the transaction hash containing the processed message + TransactionHash string `json:"transaction-hash"` +} + +// Defines a manual warp message to be sent from the relayer through the API. +type ManualWarpMessageRequest struct { + UnsignedMessageBytes []byte `json:"unsigned-message-bytes"` + SourceAddress string `json:"source-address"` +} + +func HandleRelayMessage(logger logging.Logger, messageCoordinator *relayer.MessageCoordinator) { + http.Handle(RelayAPIPath, relayAPIHandler(logger, messageCoordinator)) +} + +func HandleRelay(logger logging.Logger, messageCoordinator *relayer.MessageCoordinator) { + http.Handle(RelayMessageAPIPath, relayMessageAPIHandler(logger, messageCoordinator)) +} + +func relayMessageAPIHandler(logger logging.Logger, messageCoordinator *relayer.MessageCoordinator) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var req ManualWarpMessageRequest + err := json.NewDecoder(r.Body).Decode(&req) + if err != nil { + logger.Warn("Could not decode request body") + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + unsignedMessage, err := types.UnpackWarpMessage(req.UnsignedMessageBytes) + if err != nil { + logger.Warn("Error unpacking warp message", zap.Error(err)) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + warpMessageInfo := &types.WarpMessageInfo{ + SourceAddress: common.HexToAddress(req.SourceAddress), + UnsignedMessage: unsignedMessage, + } + + txHash, err := messageCoordinator.ProcessWarpMessage(warpMessageInfo) + if err != nil { + logger.Error("Error processing message", zap.Error(err)) + http.Error(w, "error processing message: "+err.Error(), http.StatusInternalServerError) + return + } + + resp, err := json.Marshal( + RelayMessageResponse{ + TransactionHash: txHash.Hex(), + }, + ) + if err != nil { + logger.Error("Error marshaling response", zap.Error(err)) + http.Error(w, "error marshaling response: "+err.Error(), http.StatusInternalServerError) + return + } + + _, err = w.Write(resp) + if err != nil { + logger.Error("Error writing response", zap.Error(err)) + } + }) +} + +func relayAPIHandler(logger logging.Logger, messageCoordinator *relayer.MessageCoordinator) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var req RelayMessageRequest + err := json.NewDecoder(r.Body).Decode(&req) + if err != nil { + logger.Warn("Could not decode request body") + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + blockchainID, err := utils.HexOrCB58ToID(req.BlockchainID) + if err != nil { + logger.Warn("Invalid blockchainID", zap.String("blockchainID", req.BlockchainID)) + http.Error(w, "invalid blockchainID: "+err.Error(), http.StatusBadRequest) + return + } + messageID, err := utils.HexOrCB58ToID(req.MessageID) + if err != nil { + logger.Warn("Invalid messageID", zap.String("messageID", req.MessageID)) + http.Error(w, "invalid messageID: "+err.Error(), http.StatusBadRequest) + return + } + + txHash, err := messageCoordinator.ProcessMessageID(blockchainID, messageID, new(big.Int).SetUint64(req.BlockNum)) + if err != nil { + logger.Error("Error processing message", zap.Error(err)) + http.Error(w, "error processing message: "+err.Error(), http.StatusInternalServerError) + return + } + + resp, err := json.Marshal( + RelayMessageResponse{ + TransactionHash: txHash.Hex(), + }, + ) + if err != nil { + logger.Error("Error marshalling response", zap.Error(err)) + http.Error(w, "error marshalling response: "+err.Error(), http.StatusInternalServerError) + return + } + + _, err = w.Write(resp) + if err != nil { + logger.Error("Error writing response", zap.Error(err)) + } + }) +} diff --git a/config/config.go b/config/config.go index c9e65108..6aceaca8 100644 --- a/config/config.go +++ b/config/config.go @@ -52,13 +52,12 @@ type Config struct { RedisURL string `mapstructure:"redis-url" json:"redis-url"` APIPort uint16 `mapstructure:"api-port" json:"api-port"` MetricsPort uint16 `mapstructure:"metrics-port" json:"metrics-port"` - DBWriteIntervalSeconds uint64 `mapstructure:"db-write-interval-seconds" json:"db-write-interval-seconds"` + DBWriteIntervalSeconds uint64 `mapstructure:"db-write-interval-seconds" json:"db-write-interval-seconds"` //nolint:lll PChainAPI *APIConfig `mapstructure:"p-chain-api" json:"p-chain-api"` InfoAPI *APIConfig `mapstructure:"info-api" json:"info-api"` SourceBlockchains []*SourceBlockchain `mapstructure:"source-blockchains" json:"source-blockchains"` DestinationBlockchains []*DestinationBlockchain `mapstructure:"destination-blockchains" json:"destination-blockchains"` ProcessMissedBlocks bool `mapstructure:"process-missed-blocks" json:"process-missed-blocks"` - ManualWarpMessages []*ManualWarpMessage `mapstructure:"manual-warp-messages" json:"manual-warp-messages"` DeciderHost string `mapstructure:"decider-host" json:"decider-host"` DeciderPort *uint16 `mapstructure:"decider-port" json:"decider-port"` @@ -73,13 +72,13 @@ func DisplayUsageText() { // Validates the configuration // Does not modify the public fields as derived from the configuration passed to the application, -// but does initialize private fields available through getters +// but does initialize private fields available through getters. func (c *Config) Validate() error { if len(c.SourceBlockchains) == 0 { - return errors.New("relayer not configured to relay from any subnets. A list of source subnets must be provided in the configuration file") + return errors.New("relayer not configured to relay from any subnets. A list of source subnets must be provided in the configuration file") //nolint:lll } if len(c.DestinationBlockchains) == 0 { - return errors.New("relayer not configured to relay to any subnets. A list of destination subnets must be provided in the configuration file") + return errors.New("relayer not configured to relay to any subnets. A list of destination subnets must be provided in the configuration file") //nolint:lll } if err := c.PChainAPI.Validate(); err != nil { return err @@ -122,13 +121,6 @@ func (c *Config) Validate() error { } c.blockchainIDToSubnetID = blockchainIDToSubnetID - // Validate the manual warp messages - for i, msg := range c.ManualWarpMessages { - if err := msg.Validate(); err != nil { - return fmt.Errorf("invalid manual warp message at index %d: %w", i, err) - } - } - return nil } @@ -205,7 +197,11 @@ func (c *Config) InitializeWarpQuorums() error { for _, destinationSubnet := range c.DestinationBlockchains { err := destinationSubnet.initializeWarpQuorum() if err != nil { - return fmt.Errorf("failed to initialize Warp quorum for destination subnet %s: %w", destinationSubnet.SubnetID, err) + return fmt.Errorf( + "failed to initialize Warp quorum for destination subnet %s: %w", + destinationSubnet.SubnetID, + err, + ) } } diff --git a/config/config_test.go b/config/config_test.go index a2221617..cb924bc1 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -78,8 +78,14 @@ func TestGetRelayerAccountPrivateKey_set_pk_in_config(t *testing.T) { resultVerifier: func(c Config) bool { // All destination subnets should have the default private key for i, subnet := range c.DestinationBlockchains { - if subnet.AccountPrivateKey != utils.SanitizeHexString(TestValidConfig.DestinationBlockchains[i].AccountPrivateKey) { - fmt.Printf("expected: %s, got: %s\n", utils.SanitizeHexString(TestValidConfig.DestinationBlockchains[i].AccountPrivateKey), subnet.AccountPrivateKey) + if subnet.AccountPrivateKey != utils.SanitizeHexString( + TestValidConfig.DestinationBlockchains[i].AccountPrivateKey, + ) { + fmt.Printf( + "expected: %s, got: %s\n", + utils.SanitizeHexString(TestValidConfig.DestinationBlockchains[i].AccountPrivateKey), + subnet.AccountPrivateKey, + ) return false } } @@ -102,18 +108,30 @@ func TestGetRelayerAccountPrivateKey_set_pk_with_subnet_env(t *testing.T) { }, envSetter: func() { // Overwrite the PK for the first subnet using an env var - varName := fmt.Sprintf("%s_%s", accountPrivateKeyEnvVarName, TestValidConfig.DestinationBlockchains[0].BlockchainID) + varName := fmt.Sprintf( + "%s_%s", + accountPrivateKeyEnvVarName, + TestValidConfig.DestinationBlockchains[0].BlockchainID, + ) t.Setenv(varName, testPk2) }, expectedOverwritten: true, resultVerifier: func(c Config) bool { // All destination subnets should have testPk1 if c.DestinationBlockchains[0].AccountPrivateKey != utils.SanitizeHexString(testPk2) { - fmt.Printf("expected: %s, got: %s\n", utils.SanitizeHexString(testPk2), c.DestinationBlockchains[0].AccountPrivateKey) + fmt.Printf( + "expected: %s, got: %s\n", + utils.SanitizeHexString(testPk2), + c.DestinationBlockchains[0].AccountPrivateKey, + ) return false } if c.DestinationBlockchains[1].AccountPrivateKey != utils.SanitizeHexString(testPk1) { - fmt.Printf("expected: %s, got: %s\n", utils.SanitizeHexString(testPk1), c.DestinationBlockchains[1].AccountPrivateKey) + fmt.Printf( + "expected: %s, got: %s\n", + utils.SanitizeHexString(testPk1), + c.DestinationBlockchains[1].AccountPrivateKey, + ) return false } return true @@ -339,7 +357,10 @@ func TestGetWarpQuorum(t *testing.T) { t.Run(testCase.name, func(t *testing.T) { client := mock_ethclient.NewMockClient(gomock.NewController(t)) gomock.InOrder( - client.EXPECT().ChainConfig(gomock.Any()).Return(&testCase.chainConfig, nil).Times(testCase.getChainConfigCalls), + client.EXPECT().ChainConfig(gomock.Any()).Return( + &testCase.chainConfig, + nil, + ).Times(testCase.getChainConfigCalls), ) quorum, err := getWarpQuorum(testCase.subnetID, testCase.blockchainID, client) diff --git a/config/destination_blockchain.go b/config/destination_blockchain.go index 80cd9ed0..7146f7ce 100644 --- a/config/destination_blockchain.go +++ b/config/destination_blockchain.go @@ -10,7 +10,8 @@ import ( "github.com/ethereum/go-ethereum/crypto" ) -// Destination blockchain configuration. Specifies how to connect to and issue transactions on the desination blockchain. +// Destination blockchain configuration. Specifies how to connect to and issue +// transactions on the desination blockchain. type DestinationBlockchain struct { SubnetID string `mapstructure:"subnet-id" json:"subnet-id"` BlockchainID string `mapstructure:"blockchain-id" json:"blockchain-id"` @@ -28,14 +29,8 @@ type DestinationBlockchain struct { blockchainID ids.ID } -// Validatees the destination subnet configuration +// Validates the destination subnet configuration func (s *DestinationBlockchain) Validate() error { - if _, err := ids.FromString(s.SubnetID); err != nil { - return fmt.Errorf("invalid subnetID in destination subnet configuration. Provided ID: %s", s.SubnetID) - } - if _, err := ids.FromString(s.BlockchainID); err != nil { - return fmt.Errorf("invalid blockchainID in destination subnet configuration. Provided ID: %s", s.BlockchainID) - } if err := s.RPCEndpoint.Validate(); err != nil { return fmt.Errorf("invalid rpc-endpoint in destination subnet configuration: %w", err) } @@ -59,14 +54,14 @@ func (s *DestinationBlockchain) Validate() error { } // Validate and store the subnet and blockchain IDs for future use - blockchainID, err := ids.FromString(s.BlockchainID) + blockchainID, err := utils.HexOrCB58ToID(s.BlockchainID) if err != nil { - return fmt.Errorf("invalid blockchainID in configuration. error: %w", err) + return fmt.Errorf("invalid blockchainID '%s' in configuration. error: %w", s.BlockchainID, err) } s.blockchainID = blockchainID - subnetID, err := ids.FromString(s.SubnetID) + subnetID, err := utils.HexOrCB58ToID(s.SubnetID) if err != nil { - return fmt.Errorf("invalid subnetID in configuration. error: %w", err) + return fmt.Errorf("invalid subnetID '%s' in configuration. error: %w", s.SubnetID, err) } s.subnetID = subnetID @@ -91,7 +86,12 @@ func (s *DestinationBlockchain) initializeWarpQuorum() error { return fmt.Errorf("invalid subnetID in configuration. error: %w", err) } - client, err := utils.NewEthClientWithConfig(context.Background(), s.RPCEndpoint.BaseURL, s.RPCEndpoint.HTTPHeaders, s.RPCEndpoint.QueryParams) + client, err := utils.NewEthClientWithConfig( + context.Background(), + s.RPCEndpoint.BaseURL, + s.RPCEndpoint.HTTPHeaders, + s.RPCEndpoint.QueryParams, + ) if err != nil { return fmt.Errorf("failed to dial destination blockchain %s: %w", blockchainID, err) } diff --git a/config/manual_warp_message.go b/config/manual_warp_message.go deleted file mode 100644 index db5b26b0..00000000 --- a/config/manual_warp_message.go +++ /dev/null @@ -1,76 +0,0 @@ -package config - -import ( - "encoding/hex" - "errors" - - "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/awm-relayer/utils" - "github.com/ethereum/go-ethereum/common" -) - -// Defines a manual warp message to be sent from the relayer on startup. -type ManualWarpMessage struct { - UnsignedMessageBytes string `mapstructure:"unsigned-message-bytes" json:"unsigned-message-bytes"` - SourceBlockchainID string `mapstructure:"source-blockchain-id" json:"source-blockchain-id"` - DestinationBlockchainID string `mapstructure:"destination-blockchain-id" json:"destination-blockchain-id"` - SourceAddress string `mapstructure:"source-address" json:"source-address"` - DestinationAddress string `mapstructure:"destination-address" json:"destination-address"` - - // convenience fields to access the values after initialization - unsignedMessageBytes []byte - sourceBlockchainID ids.ID - destinationBlockchainID ids.ID - sourceAddress common.Address - destinationAddress common.Address -} - -// Validates the manual Warp message configuration. -// Does not modify the public fields as derived from the configuration passed to the application, -// but does initialize private fields available through getters -func (m *ManualWarpMessage) Validate() error { - unsignedMsg, err := hex.DecodeString(utils.SanitizeHexString(m.UnsignedMessageBytes)) - if err != nil { - return err - } - sourceBlockchainID, err := ids.FromString(m.SourceBlockchainID) - if err != nil { - return err - } - if !common.IsHexAddress(m.SourceAddress) { - return errors.New("invalid source address in manual warp message configuration") - } - destinationBlockchainID, err := ids.FromString(m.DestinationBlockchainID) - if err != nil { - return err - } - if !common.IsHexAddress(m.DestinationAddress) { - return errors.New("invalid destination address in manual warp message configuration") - } - m.unsignedMessageBytes = unsignedMsg - m.sourceBlockchainID = sourceBlockchainID - m.sourceAddress = common.HexToAddress(m.SourceAddress) - m.destinationBlockchainID = destinationBlockchainID - m.destinationAddress = common.HexToAddress(m.DestinationAddress) - return nil -} - -func (m *ManualWarpMessage) GetUnsignedMessageBytes() []byte { - return m.unsignedMessageBytes -} - -func (m *ManualWarpMessage) GetSourceBlockchainID() ids.ID { - return m.sourceBlockchainID -} - -func (m *ManualWarpMessage) GetSourceAddress() common.Address { - return m.sourceAddress -} - -func (m *ManualWarpMessage) GetDestinationBlockchainID() ids.ID { - return m.destinationBlockchainID -} - -func (m *ManualWarpMessage) GetDestinationAddress() common.Address { - return m.destinationAddress -} diff --git a/config/source_blockchain.go b/config/source_blockchain.go index d3e6f035..1786d280 100644 --- a/config/source_blockchain.go +++ b/config/source_blockchain.go @@ -16,15 +16,15 @@ import ( // Specifies the height from which to start processing historical blocks. type SourceBlockchain struct { SubnetID string `mapstructure:"subnet-id" json:"subnet-id"` - BlockchainID string `mapstructure:"blockchain-id" json:"blockchain-id"` + BlockchainID string `mapstructure:"blockchain-id" json:"blockchain-id"` //nolint:lll VM string `mapstructure:"vm" json:"vm"` - RPCEndpoint APIConfig `mapstructure:"rpc-endpoint" json:"rpc-endpoint"` - WSEndpoint APIConfig `mapstructure:"ws-endpoint" json:"ws-endpoint"` - MessageContracts map[string]MessageProtocolConfig `mapstructure:"message-contracts" json:"message-contracts"` - SupportedDestinations []*SupportedDestination `mapstructure:"supported-destinations" json:"supported-destinations"` - ProcessHistoricalBlocksFromHeight uint64 `mapstructure:"process-historical-blocks-from-height" json:"process-historical-blocks-from-height"` - AllowedOriginSenderAddresses []string `mapstructure:"allowed-origin-sender-addresses" json:"allowed-origin-sender-addresses"` - WarpAPIEndpoint APIConfig `mapstructure:"warp-api-endpoint" json:"warp-api-endpoint"` + RPCEndpoint APIConfig `mapstructure:"rpc-endpoint" json:"rpc-endpoint"` //nolint:lll + WSEndpoint APIConfig `mapstructure:"ws-endpoint" json:"ws-endpoint"` //nolint:lll + MessageContracts map[string]MessageProtocolConfig `mapstructure:"message-contracts" json:"message-contracts"` //nolint:lll + SupportedDestinations []*SupportedDestination `mapstructure:"supported-destinations" json:"supported-destinations"` //nolint:lll + ProcessHistoricalBlocksFromHeight uint64 `mapstructure:"process-historical-blocks-from-height" json:"process-historical-blocks-from-height"` //nolint:lll + AllowedOriginSenderAddresses []string `mapstructure:"allowed-origin-sender-addresses" json:"allowed-origin-sender-addresses"` //nolint:lll + WarpAPIEndpoint APIConfig `mapstructure:"warp-api-endpoint" json:"warp-api-endpoint"` //nolint:lll // convenience fields to access parsed data after initialization subnetID ids.ID @@ -33,16 +33,10 @@ type SourceBlockchain struct { useAppRequestNetwork bool } -// Validates the source subnet configuration, including verifying that the supported destinations are present in destinationBlockchainIDs -// Does not modify the public fields as derived from the configuration passed to the application, -// but does initialize private fields available through getters +// Validates the source subnet configuration, including verifying that the supported destinations are present in +// destinationBlockchainIDs. Does not modify the public fields as derived from the configuration passed to the +// application, but does initialize private fields available through getters. func (s *SourceBlockchain) Validate(destinationBlockchainIDs *set.Set[string]) error { - if _, err := ids.FromString(s.SubnetID); err != nil { - return fmt.Errorf("invalid subnetID in source subnet configuration. Provided ID: %s", s.SubnetID) - } - if _, err := ids.FromString(s.BlockchainID); err != nil { - return fmt.Errorf("invalid blockchainID in source subnet configuration. Provided ID: %s", s.BlockchainID) - } if err := s.RPCEndpoint.Validate(); err != nil { return fmt.Errorf("invalid rpc-endpoint in source subnet configuration: %w", err) } @@ -79,14 +73,14 @@ func (s *SourceBlockchain) Validate(destinationBlockchainIDs *set.Set[string]) e } // Validate and store the subnet and blockchain IDs for future use - blockchainID, err := ids.FromString(s.BlockchainID) + blockchainID, err := utils.HexOrCB58ToID(s.BlockchainID) if err != nil { - return fmt.Errorf("invalid blockchainID in configuration. error: %w", err) + return fmt.Errorf("invalid blockchainID '%s' in configuration. error: %w", s.BlockchainID, err) } s.blockchainID = blockchainID - subnetID, err := ids.FromString(s.SubnetID) + subnetID, err := utils.HexOrCB58ToID(s.SubnetID) if err != nil { - return fmt.Errorf("invalid subnetID in configuration. error: %w", err) + return fmt.Errorf("invalid subnetID '%s' in configuration. error: %w", s.SubnetID, err) } s.subnetID = subnetID @@ -99,23 +93,31 @@ func (s *SourceBlockchain) Validate(destinationBlockchainIDs *set.Set[string]) e } } for _, dest := range s.SupportedDestinations { - blockchainID, err := ids.FromString(dest.BlockchainID) + blockchainID, err := utils.HexOrCB58ToID(dest.BlockchainID) if err != nil { return fmt.Errorf("invalid blockchainID in configuration. error: %w", err) } if !destinationBlockchainIDs.Contains(dest.BlockchainID) { - return fmt.Errorf("configured source subnet %s has a supported destination blockchain ID %s that is not configured as a destination blockchain", + return fmt.Errorf( + "configured source subnet %s has a supported destination blockchain ID %s that is not configured as a destination blockchain", //nolint:lll s.SubnetID, - blockchainID) + blockchainID, + ) } dest.blockchainID = blockchainID for _, addressStr := range dest.Addresses { if !common.IsHexAddress(addressStr) { - return fmt.Errorf("invalid allowed destination address in source blockchain configuration: %s", addressStr) + return fmt.Errorf( + "invalid allowed destination address in source blockchain configuration: %s", + addressStr, + ) } address := common.HexToAddress(addressStr) if address == utils.ZeroAddress { - return fmt.Errorf("invalid allowed destination address in source blockchain configuration: %s", addressStr) + return fmt.Errorf( + "invalid allowed destination address in source blockchain configuration: %s", + addressStr, + ) } dest.addresses = append(dest.addresses, address) } @@ -125,11 +127,17 @@ func (s *SourceBlockchain) Validate(destinationBlockchainIDs *set.Set[string]) e allowedOriginSenderAddresses := make([]common.Address, len(s.AllowedOriginSenderAddresses)) for i, addressStr := range s.AllowedOriginSenderAddresses { if !common.IsHexAddress(addressStr) { - return fmt.Errorf("invalid allowed origin sender address in source blockchain configuration: %s", addressStr) + return fmt.Errorf( + "invalid allowed origin sender address in source blockchain configuration: %s", + addressStr, + ) } address := common.HexToAddress(addressStr) if address == utils.ZeroAddress { - return fmt.Errorf("invalid allowed origin sender address in source blockchain configuration: %s", addressStr) + return fmt.Errorf( + "invalid allowed origin sender address in source blockchain configuration: %s", + addressStr, + ) } allowedOriginSenderAddresses[i] = address } diff --git a/config/viper.go b/config/viper.go index 0273ea96..7ea590c4 100644 --- a/config/viper.go +++ b/config/viper.go @@ -89,12 +89,22 @@ func BuildConfig(v *viper.Viper) (Config, error) { privateKey := subnet.AccountPrivateKey if accountPrivateKey != "" { privateKey = accountPrivateKey - cfg.overwrittenOptions = append(cfg.overwrittenOptions, fmt.Sprintf("destination-blockchain(%s).account-private-key", subnet.blockchainID)) + cfg.overwrittenOptions = append( + cfg.overwrittenOptions, + fmt.Sprintf("destination-blockchain(%s).account-private-key", subnet.blockchainID), + ) // Otherwise, check for private keys suffixed with the chain ID and set it for that subnet // Since the key is dynamic, this is only possible through environment variables - } else if privateKeyFromEnv := os.Getenv(fmt.Sprintf("%s_%s", accountPrivateKeyEnvVarName, subnet.BlockchainID)); privateKeyFromEnv != "" { + } else if privateKeyFromEnv := os.Getenv(fmt.Sprintf( + "%s_%s", + accountPrivateKeyEnvVarName, + subnet.BlockchainID, + )); privateKeyFromEnv != "" { privateKey = privateKeyFromEnv - cfg.overwrittenOptions = append(cfg.overwrittenOptions, fmt.Sprintf("destination-blockchain(%s).account-private-key", subnet.blockchainID)) + cfg.overwrittenOptions = append(cfg.overwrittenOptions, fmt.Sprintf( + "destination-blockchain(%s).account-private-key", + subnet.blockchainID), + ) } cfg.DestinationBlockchains[i].AccountPrivateKey = utils.SanitizeHexString(privateKey) } diff --git a/database/json_file_storage_test.go b/database/json_file_storage_test.go index 7e524714..9361a730 100644 --- a/database/json_file_storage_test.go +++ b/database/json_file_storage_test.go @@ -70,10 +70,16 @@ func TestConcurrentWriteReadSingleChain(t *testing.T) { if !success { t.Fatalf("failed to convert latest block to big.Int. err: %v", err) } - assert.Equal(t, finalTargetValue, latestProcessedBlock.Uint64(), "latest processed block height is not correct.") + assert.Equal( + t, + finalTargetValue, + latestProcessedBlock.Uint64(), + "latest processed block height is not correct.", + ) } -// Test that the JSON database can write and read from multiple chains concurrently. Write to any given chain are not concurrent. +// Test that the JSON database can write and read from multiple chains concurrently. +// Writes to any given chain are not concurrent. func TestConcurrentWriteReadMultipleChains(t *testing.T) { relayerIDs := createRelayerIDs( []ids.ID{ @@ -111,7 +117,12 @@ func TestConcurrentWriteReadMultipleChains(t *testing.T) { if !success { t.Fatalf("failed to convert latest block to big.Int. err: %v", err) } - assert.Equal(t, finalTargetValue, latestProcessedBlock.Uint64(), fmt.Sprintf("latest processed block height is not correct. networkID: %d", i)) + assert.Equal( + t, + finalTargetValue, + latestProcessedBlock.Uint64(), + fmt.Sprintf("latest processed block height is not correct. networkID: %d", i), + ) } } diff --git a/database/utils.go b/database/utils.go index 00d1f741..b0a513a4 100644 --- a/database/utils.go +++ b/database/utils.go @@ -18,7 +18,8 @@ func IsKeyNotFoundError(err error) bool { // Determines the height to process from. There are three cases: // 1) The database contains the latest processed block data for the chain -// - In this case, we return the maximum of the latest processed block and the configured processHistoricalBlocksFromHeight +// - In this case, we return the maximum of the latest processed block and the +// configured processHistoricalBlocksFromHeight. // // 2) The database has been configured for the chain, but does not contain the latest processed block data // - In this case, we return the configured processHistoricalBlocksFromHeight diff --git a/go.mod b/go.mod index 12d511ed..b073ab52 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/ava-labs/awm-relayer -go 1.21.11 +go 1.21.12 require ( github.com/ava-labs/avalanche-network-runner v1.7.6 diff --git a/main/main.go b/main/main.go index 40a0db56..1ef462aa 100644 --- a/main/main.go +++ b/main/main.go @@ -5,7 +5,6 @@ package main import ( "context" - "encoding/hex" "fmt" "log" "net/http" @@ -14,18 +13,19 @@ import ( "strconv" "strings" - "github.com/alexliesenfeld/health" "github.com/ava-labs/avalanchego/api/metrics" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/message" "github.com/ava-labs/avalanchego/utils/constants" "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/awm-relayer/api" "github.com/ava-labs/awm-relayer/config" "github.com/ava-labs/awm-relayer/database" + "github.com/ava-labs/awm-relayer/messages" + offchainregistry "github.com/ava-labs/awm-relayer/messages/off-chain-registry" + "github.com/ava-labs/awm-relayer/messages/teleporter" "github.com/ava-labs/awm-relayer/peers" "github.com/ava-labs/awm-relayer/relayer" - "github.com/ava-labs/awm-relayer/types" - relayerTypes "github.com/ava-labs/awm-relayer/types" "github.com/ava-labs/awm-relayer/utils" "github.com/ava-labs/awm-relayer/vms" "github.com/ava-labs/subnet-evm/ethclient" @@ -111,24 +111,27 @@ func main() { logger.Info("Initializing destination clients") destinationClients, err := vms.CreateDestinationClients(logger, cfg) if err != nil { - logger.Error( - "Failed to create destination clients", - zap.Error(err), - ) + logger.Fatal("Failed to create destination clients", zap.Error(err)) + panic(err) + } + + // Initialize all source clients + logger.Info("Initializing source clients") + sourceClients, err := createSourceClients(context.Background(), logger, &cfg) + if err != nil { + logger.Fatal("Failed to create source clients", zap.Error(err)) panic(err) } // Initialize metrics gathered through prometheus gatherer, registerer, err := initializeMetrics() if err != nil { - logger.Fatal("Failed to set up prometheus metrics", - zap.Error(err)) + logger.Fatal("Failed to set up prometheus metrics", zap.Error(err)) panic(err) } // Initialize the global app request network logger.Info("Initializing app request network") - // The app request network generates P2P networking logs that are verbose at the info level. // Unless the log level is debug or lower, set the network log level to error to avoid spamming the logs. // We do not collect metrics for the network. @@ -142,51 +145,15 @@ func main() { &cfg, ) if err != nil { - logger.Error( - "Failed to create app request network", - zap.Error(err), - ) + logger.Fatal("Failed to create app request network", zap.Error(err)) panic(err) } - // Each goroutine will have an atomic bool that it can set to false if it ever disconnects from its subscription. - relayerHealth := make(map[ids.ID]*atomic.Bool) - - checker := health.NewChecker( - health.WithCheck(health.Check{ - Name: "relayers-all", - Check: func(context.Context) error { - // Store the IDs as the cb58 encoding - var unhealthyRelayers []string - for id, health := range relayerHealth { - if !health.Load() { - unhealthyRelayers = append(unhealthyRelayers, id.String()) - } - } - - if len(unhealthyRelayers) > 0 { - return fmt.Errorf("relayers are unhealthy for blockchains %v", unhealthyRelayers) - } - return nil - }, - }), - ) - - http.Handle("/health", health.NewHandler(checker)) - - // start the health check server - go func() { - log.Fatalln(http.ListenAndServe(fmt.Sprintf(":%d", cfg.APIPort), nil)) - }() - startMetricsServer(logger, gatherer, cfg.MetricsPort) - metrics, err := relayer.NewApplicationRelayerMetrics(registerer) + relayerMetrics, err := relayer.NewApplicationRelayerMetrics(registerer) if err != nil { - logger.Error( - "Failed to create application relayer metrics", - zap.Error(err), - ) + logger.Fatal("Failed to create application relayer metrics", zap.Error(err)) panic(err) } @@ -200,20 +167,14 @@ func main() { constants.DefaultNetworkMaximumInboundTimeout, ) if err != nil { - logger.Error( - "Failed to create message creator", - zap.Error(err), - ) + logger.Fatal("Failed to create message creator", zap.Error(err)) panic(err) } // Initialize the database db, err := database.NewDatabase(logger, &cfg) if err != nil { - logger.Error( - "Failed to create database", - zap.Error(err), - ) + logger.Fatal("Failed to create database", zap.Error(err)) panic(err) } @@ -221,25 +182,46 @@ func main() { ticker := utils.NewTicker(cfg.DBWriteIntervalSeconds) go ticker.Run() - // Gather manual Warp messages specified in the configuration - manualWarpMessages := make(map[ids.ID][]*relayerTypes.WarpMessageInfo) - for _, msg := range cfg.ManualWarpMessages { - sourceBlockchainID := msg.GetSourceBlockchainID() - unsignedMsg, err := types.UnpackWarpMessage(msg.GetUnsignedMessageBytes()) - if err != nil { - logger.Error( - "Failed to unpack manual Warp message", - zap.String("warpMessageBytes", hex.EncodeToString(msg.GetUnsignedMessageBytes())), - zap.Error(err), - ) - panic(err) - } - warpLogInfo := relayerTypes.WarpMessageInfo{ - SourceAddress: msg.GetSourceAddress(), - UnsignedMessage: unsignedMsg, - } - manualWarpMessages[sourceBlockchainID] = append(manualWarpMessages[sourceBlockchainID], &warpLogInfo) + relayerHealth := createHealthTrackers(&cfg) + + messageHandlerFactories, err := createMessageHandlerFactories(logger, &cfg) + if err != nil { + logger.Fatal("Failed to create message handler factories", zap.Error(err)) + panic(err) + } + + applicationRelayers, minHeights, err := createApplicationRelayers( + context.Background(), + logger, + relayerMetrics, + db, + ticker, + network, + messageCreator, + &cfg, + sourceClients, + destinationClients, + ) + if err != nil { + logger.Fatal("Failed to create application relayers", zap.Error(err)) + panic(err) } + messageCoordinator := relayer.NewMessageCoordinator( + logger, + messageHandlerFactories, + applicationRelayers, + sourceClients, + ) + + // Each Listener goroutine will have an atomic bool that it can set to false to indicate an unrecoverable error + api.HandleHealthCheck(logger, relayerHealth) + api.HandleRelay(logger, messageCoordinator) + api.HandleRelayMessage(logger, messageCoordinator) + + // start the health check server + go func() { + log.Fatalln(http.ListenAndServe(fmt.Sprintf(":%d", cfg.APIPort), nil)) + }() errGroup, ctx := errgroup.WithContext(context.Background()) @@ -270,139 +252,157 @@ func main() { // Create listeners for each of the subnets configured as a source for _, s := range cfg.SourceBlockchains { - blockchainID, err := ids.FromString(s.BlockchainID) - if err != nil { - logger.Error( - "Invalid subnetID in configuration", - zap.Error(err), - ) - panic(err) - } sourceBlockchain := s - health := atomic.NewBool(true) - relayerHealth[blockchainID] = health - // errgroup will cancel the context when the first goroutine returns an error errGroup.Go(func() error { - // Dial the eth client - ethClient, err := utils.NewEthClientWithConfig( - context.Background(), - sourceBlockchain.RPCEndpoint.BaseURL, - sourceBlockchain.RPCEndpoint.HTTPHeaders, - sourceBlockchain.RPCEndpoint.QueryParams, - ) - if err != nil { - logger.Error( - "Failed to connect to node via RPC", - zap.String("blockchainID", sourceBlockchain.BlockchainID), - zap.Error(err), - ) - return err - } - - // Create the ApplicationRelayers - applicationRelayers, minHeight, err := createApplicationRelayers( - ctx, - logger, - metrics, - db, - ticker, - *sourceBlockchain, - network, - messageCreator, - &cfg, - ethClient, - destinationClients, - ) - if err != nil { - logger.Error( - "Failed to create application relayers", - zap.String("blockchainID", sourceBlockchain.BlockchainID), - zap.Error(err), - ) - return err - } - logger.Info( - "Created application relayers", - zap.String("blockchainID", sourceBlockchain.BlockchainID), - ) - // runListener runs until it errors or the context is cancelled by another goroutine - return runListener( + return relayer.RunListener( ctx, logger, *sourceBlockchain, - health, - manualWarpMessages[blockchainID], - &cfg, - ethClient, - grpcClient, - applicationRelayers, - minHeight, + sourceClients[sourceBlockchain.GetBlockchainID()], + relayerHealth[sourceBlockchain.GetBlockchainID()], + cfg.ProcessMissedBlocks, + minHeights[sourceBlockchain.GetBlockchainID()], + messageCoordinator, + grpcClient, ) }) } err = errGroup.Wait() - logger.Error( - "Relayer exiting.", - zap.Error(err), - ) + logger.Error("Relayer exiting.", zap.Error(err)) } -// runListener creates a Listener instance and the ApplicationRelayers for a subnet. -// The Listener listens for warp messages on that subnet, and the ApplicationRelayers handle delivery to the destination -func runListener( - ctx context.Context, +func createMessageHandlerFactories( logger logging.Logger, - sourceBlockchain config.SourceBlockchain, - relayerHealth *atomic.Bool, - manualWarpMessages []*relayerTypes.WarpMessageInfo, globalConfig *config.Config, - ethClient ethclient.Client, grpcClient *grpc.ClientConn, - applicationRelayers map[common.Hash]*relayer.ApplicationRelayer, - minHeight uint64, -) error { - // Create the Listener - listener, err := relayer.NewListener( - logger, - sourceBlockchain, - relayerHealth, - globalConfig, - applicationRelayers, - minHeight, - ethClient, - grpcClient, - ) - if err != nil { - return fmt.Errorf("failed to create listener instance: %w", err) +) (map[ids.ID]map[common.Address]messages.MessageHandlerFactory, error) { + messageHandlerFactories := make(map[ids.ID]map[common.Address]messages.MessageHandlerFactory) + for _, sourceBlockchain := range globalConfig.SourceBlockchains { + messageHandlerFactoriesForSource := make(map[common.Address]messages.MessageHandlerFactory) + // Create message handler factories for each supported message protocol + for addressStr, cfg := range sourceBlockchain.MessageContracts { + address := common.HexToAddress(addressStr) + format := cfg.MessageFormat + var ( + m messages.MessageHandlerFactory + err error + ) + switch config.ParseMessageProtocol(format) { + case config.TELEPORTER: + m, err = teleporter.NewMessageHandlerFactory( + logger, + address, + cfg, + ) + case config.OFF_CHAIN_REGISTRY: + m, err = offchainregistry.NewMessageHandlerFactory( + logger, + cfg, + ) + default: + m, err = nil, fmt.Errorf("invalid message format %s", format) + } + if err != nil { + logger.Error("Failed to create message handler factory", zap.Error(err)) + return nil, err + } + messageHandlerFactoriesForSource[address] = m + } + messageHandlerFactories[sourceBlockchain.GetBlockchainID()] = messageHandlerFactoriesForSource } - logger.Info( - "Created listener", - zap.String("blockchainID", sourceBlockchain.BlockchainID), - ) - err = listener.ProcessManualWarpMessages(logger, manualWarpMessages, sourceBlockchain) - if err != nil { - logger.Error( - "Failed to process manual Warp messages", - zap.String("blockchainID", sourceBlockchain.BlockchainID), - zap.Error(err), + return messageHandlerFactories, nil +} + +func createSourceClients( + ctx context.Context, + logger logging.Logger, + cfg *config.Config, +) (map[ids.ID]ethclient.Client, error) { + var err error + clients := make(map[ids.ID]ethclient.Client) + + for _, sourceBlockchain := range cfg.SourceBlockchains { + clients[sourceBlockchain.GetBlockchainID()], err = utils.NewEthClientWithConfig( + ctx, + sourceBlockchain.RPCEndpoint.BaseURL, + sourceBlockchain.RPCEndpoint.HTTPHeaders, + sourceBlockchain.RPCEndpoint.QueryParams, ) + if err != nil { + logger.Error( + "Failed to connect to node via RPC", + zap.String("blockchainID", sourceBlockchain.BlockchainID), + zap.Error(err), + ) + return nil, err + } } + return clients, nil +} - logger.Info( - "Listener initialized. Listening for messages to relay.", - zap.String("originBlockchainID", sourceBlockchain.BlockchainID), - ) +// Returns a map of application relayers, as well as a map of source blockchain IDs to starting heights. +func createApplicationRelayers( + ctx context.Context, + logger logging.Logger, + relayerMetrics *relayer.ApplicationRelayerMetrics, + db database.RelayerDatabase, + ticker *utils.Ticker, + network *peers.AppRequestNetwork, + messageCreator message.Creator, + cfg *config.Config, + sourceClients map[ids.ID]ethclient.Client, + destinationClients map[ids.ID]vms.DestinationClient, +) (map[common.Hash]*relayer.ApplicationRelayer, map[ids.ID]uint64, error) { + applicationRelayers := make(map[common.Hash]*relayer.ApplicationRelayer) + minHeights := make(map[ids.ID]uint64) + for _, sourceBlockchain := range cfg.SourceBlockchains { + currentHeight, err := sourceClients[sourceBlockchain.GetBlockchainID()].BlockNumber(ctx) + if err != nil { + logger.Error("Failed to get current block height", zap.Error(err)) + return nil, nil, err + } + + // Create the ApplicationRelayers + applicationRelayersForSource, minHeight, err := createApplicationRelayersForSourceChain( + ctx, + logger, + relayerMetrics, + db, + ticker, + *sourceBlockchain, + network, + messageCreator, + cfg, + currentHeight, + destinationClients, + ) + if err != nil { + logger.Error( + "Failed to create application relayers", + zap.String("blockchainID", sourceBlockchain.BlockchainID), + zap.Error(err), + ) + return nil, nil, err + } - // Wait for logs from the subscribed node - // Will only return on error or context cancellation - return listener.ProcessLogs(ctx) + for relayerID, applicationRelayer := range applicationRelayersForSource { + applicationRelayers[relayerID] = applicationRelayer + } + minHeights[sourceBlockchain.GetBlockchainID()] = minHeight + + logger.Info( + "Created application relayers", + zap.String("blockchainID", sourceBlockchain.BlockchainID), + ) + } + return applicationRelayers, minHeights, nil } // createApplicationRelayers creates Application Relayers for a given source blockchain. -func createApplicationRelayers( +func createApplicationRelayersForSourceChain( ctx context.Context, logger logging.Logger, metrics *relayer.ApplicationRelayerMetrics, @@ -412,7 +412,7 @@ func createApplicationRelayers( network *peers.AppRequestNetwork, messageCreator message.Creator, cfg *config.Config, - srcEthClient ethclient.Client, + currentHeight uint64, destinationClients map[ids.ID]vms.DestinationClient, ) (map[common.Hash]*relayer.ApplicationRelayer, uint64, error) { // Create the ApplicationRelayers @@ -422,17 +422,8 @@ func createApplicationRelayers( ) applicationRelayers := make(map[common.Hash]*relayer.ApplicationRelayer) - currentHeight, err := srcEthClient.BlockNumber(context.Background()) - if err != nil { - logger.Error( - "Failed to get current block height", - zap.Error(err), - ) - return nil, 0, err - } - // Each ApplicationRelayer determines its starting height based on the database state. - // The Listener begins processing messages starting from the minimum height across all of the ApplicationRelayers + // The Listener begins processing messages starting from the minimum height across all the ApplicationRelayers minHeight := uint64(0) for _, relayerID := range database.GetSourceBlockchainRelayerIDs(&sourceBlockchain) { height, err := database.CalculateStartingBlockHeight( @@ -479,6 +470,14 @@ func createApplicationRelayers( return applicationRelayers, minHeight, nil } +func createHealthTrackers(cfg *config.Config) map[ids.ID]*atomic.Bool { + healthTrackers := make(map[ids.ID]*atomic.Bool, len(cfg.SourceBlockchains)) + for _, sourceBlockchain := range cfg.SourceBlockchains { + healthTrackers[sourceBlockchain.GetBlockchainID()] = atomic.NewBool(true) + } + return healthTrackers +} + func startMetricsServer(logger logging.Logger, gatherer prometheus.Gatherer, port uint16) { http.Handle("/metrics", promhttp.HandlerFor(gatherer, promhttp.HandlerOpts{})) diff --git a/messages/message_handler.go b/messages/message_handler.go index 1ebfc04c..8a7419bc 100644 --- a/messages/message_handler.go +++ b/messages/message_handler.go @@ -27,9 +27,11 @@ type MessageHandler interface { // SendMessage sends the signed message to the destination chain. The payload parsed according to // the VM rules is also passed in, since MessageManager does not assume any particular VM - SendMessage(signedMessage *warp.Message, destinationClient vms.DestinationClient) error + // returns the transaction hash if the transaction is successful. + SendMessage(signedMessage *warp.Message, destinationClient vms.DestinationClient) (common.Hash, error) - // GetMessageRoutingInfo returns the source chain ID, origin sender address, destination chain ID, and destination address + // GetMessageRoutingInfo returns the source chain ID, origin sender address, + // destination chain ID, and destination address. GetMessageRoutingInfo() ( ids.ID, common.Address, diff --git a/messages/off-chain-registry/message_handler.go b/messages/off-chain-registry/message_handler.go index 8abd17e4..75f2c3cf 100644 --- a/messages/off-chain-registry/message_handler.go +++ b/messages/off-chain-registry/message_handler.go @@ -81,8 +81,9 @@ func (m *messageHandler) GetUnsignedMessage() *warp.UnsignedMessage { return m.unsignedMessage } -// ShouldSendMessage returns false if any contract is already registered as the specified version in the TeleporterRegistry contract. -// This is because a single contract address can be registered to multiple versions, but each version may only map to a single contract address. +// ShouldSendMessage returns false if any contract is already registered as the specified version +// in the TeleporterRegistry contract. This is because a single contract address can be registered +// to multiple versions, but each version may only map to a single contract address. func (m *messageHandler) ShouldSendMessage(destinationClient vms.DestinationClient) (bool, error) { addressedPayload, err := warpPayload.ParseAddressedCall(m.unsignedMessage.Payload) if err != nil { @@ -92,7 +93,9 @@ func (m *messageHandler) ShouldSendMessage(destinationClient vms.DestinationClie ) return false, err } - entry, destination, err := teleporterregistry.UnpackTeleporterRegistryWarpPayload(addressedPayload.Payload) + entry, destination, err := teleporterregistry.UnpackTeleporterRegistryWarpPayload( + addressedPayload.Payload, + ) if err != nil { m.logger.Error( "Failed unpacking teleporter registry warp payload", @@ -112,7 +115,10 @@ func (m *messageHandler) ShouldSendMessage(destinationClient vms.DestinationClie // Get the correct destination client from the global map client, ok := destinationClient.Client().(ethclient.Client) if !ok { - panic(fmt.Sprintf("Destination client for chain %s is not an Ethereum client", destinationClient.DestinationBlockchainID().String())) + panic(fmt.Sprintf( + "Destination client for chain %s is not an Ethereum client", + destinationClient.DestinationBlockchainID().String()), + ) } // Check if the version is already registered in the TeleporterRegistry contract. @@ -144,35 +150,49 @@ func (m *messageHandler) ShouldSendMessage(destinationClient vms.DestinationClie return false, nil } -func (m *messageHandler) SendMessage(signedMessage *warp.Message, destinationClient vms.DestinationClient) error { +func (m *messageHandler) SendMessage( + signedMessage *warp.Message, + destinationClient vms.DestinationClient, +) (common.Hash, error) { // Construct the transaction call data to call the TeleporterRegistry contract. // Only one off-chain registry Warp message is sent at a time, so we hardcode the index to 0 in the call. callData, err := teleporterregistry.PackAddProtocolVersion(0) if err != nil { m.logger.Error( "Failed packing receiveCrossChainMessage call data", - zap.String("destinationBlockchainID", destinationClient.DestinationBlockchainID().String()), + zap.String( + "destinationBlockchainID", + destinationClient.DestinationBlockchainID().String(), + ), zap.String("warpMessageID", signedMessage.ID().String()), ) - return err + return common.Hash{}, err } - _, err = destinationClient.SendTx(signedMessage, m.factory.registryAddress.Hex(), addProtocolVersionGasLimit, callData) + txHash, err := destinationClient.SendTx( + signedMessage, + m.factory.registryAddress.Hex(), + addProtocolVersionGasLimit, + callData, + ) if err != nil { m.logger.Error( "Failed to send tx.", - zap.String("destinationBlockchainID", destinationClient.DestinationBlockchainID().String()), + zap.String( + "destinationBlockchainID", + destinationClient.DestinationBlockchainID().String(), + ), zap.String("warpMessageID", signedMessage.ID().String()), zap.Error(err), ) - return err + return common.Hash{}, err } m.logger.Info( "Sent message to destination chain", zap.String("destinationBlockchainID", destinationClient.DestinationBlockchainID().String()), zap.String("warpMessageID", signedMessage.ID().String()), ) - return nil + return txHash, nil } func (m *messageHandler) GetMessageRoutingInfo() ( diff --git a/messages/off-chain-registry/message_handler_test.go b/messages/off-chain-registry/message_handler_test.go index a71b895f..550dff55 100644 --- a/messages/off-chain-registry/message_handler_test.go +++ b/messages/off-chain-registry/message_handler_test.go @@ -152,9 +152,19 @@ func TestShouldSendMessage(t *testing.T) { // construct the signed message var unsignedMessage *warp.UnsignedMessage if test.isMessageInvalid { - unsignedMessage = createInvalidRegistryUnsignedWarpMessage(t, test.entry, teleporterRegistryAddress, test.destinationBlockchainID) + unsignedMessage = createInvalidRegistryUnsignedWarpMessage( + t, + test.entry, + teleporterRegistryAddress, + test.destinationBlockchainID, + ) } else { - unsignedMessage = createRegistryUnsignedWarpMessage(t, test.entry, teleporterRegistryAddress, test.destinationBlockchainID) + unsignedMessage = createRegistryUnsignedWarpMessage( + t, + test.entry, + teleporterRegistryAddress, + test.destinationBlockchainID, + ) } messageHandler, err := factory.NewMessageHandler(unsignedMessage) require.NoError(t, err) @@ -199,7 +209,10 @@ func createInvalidRegistryUnsignedWarpMessage( payloadBytes, err := teleporterregistry.PackTeleporterRegistryWarpPayload(entry, teleporterRegistryAddress) require.NoError(t, err) - invalidAddressedPayload, err := payload.NewAddressedCall(messageProtocolAddress[:], append(payloadBytes, []byte{1, 2, 3, 4}...)) + invalidAddressedPayload, err := payload.NewAddressedCall( + messageProtocolAddress[:], + append(payloadBytes, []byte{1, 2, 3, 4}...), + ) require.NoError(t, err) invalidUnsignedMessage, err := warp.NewUnsignedMessage( diff --git a/messages/teleporter/message_handler.go b/messages/teleporter/message_handler.go index 812e7eff..d124eeee 100644 --- a/messages/teleporter/message_handler.go +++ b/messages/teleporter/message_handler.go @@ -226,9 +226,13 @@ func (m *messageHandler) ShouldSendMessage(destinationClient vms.DestinationClie return decision, nil } -// SendMessage extracts the gasLimit and packs the call data to call the receiveCrossChainMessage method of the Teleporter contract, -// and dispatches transaction construction and broadcast to the destination client -func (m *messageHandler) SendMessage(signedMessage *warp.Message, destinationClient vms.DestinationClient) error { +// SendMessage extracts the gasLimit and packs the call data to call the receiveCrossChainMessage +// method of the Teleporter contract, and dispatches transaction construction and broadcast to the +// destination client. +func (m *messageHandler) SendMessage( + signedMessage *warp.Message, + destinationClient vms.DestinationClient, +) (common.Hash, error) { destinationBlockchainID := destinationClient.DestinationBlockchainID() teleporterMessageID, err := teleporterUtils.CalculateMessageID( m.factory.protocolAddress, @@ -237,7 +241,7 @@ func (m *messageHandler) SendMessage(signedMessage *warp.Message, destinationCli m.teleporterMessage.MessageNonce, ) if err != nil { - return fmt.Errorf("failed to calculate Teleporter message ID: %w", err) + return common.Hash{}, fmt.Errorf("failed to calculate Teleporter message ID: %w", err) } m.logger.Info( @@ -254,7 +258,7 @@ func (m *messageHandler) SendMessage(signedMessage *warp.Message, destinationCli zap.String("warpMessageID", signedMessage.ID().String()), zap.String("teleporterMessageID", teleporterMessageID.String()), ) - return err + return common.Hash{}, err } gasLimit, err := gasUtils.CalculateReceiveMessageGasLimit( @@ -271,10 +275,13 @@ func (m *messageHandler) SendMessage(signedMessage *warp.Message, destinationCli zap.String("warpMessageID", signedMessage.ID().String()), zap.String("teleporterMessageID", teleporterMessageID.String()), ) - return err + return common.Hash{}, err } // Construct the transaction call data to call the receive cross chain message method of the receiver precompile. - callData, err := teleportermessenger.PackReceiveCrossChainMessage(0, common.HexToAddress(m.factory.messageConfig.RewardAddress)) + callData, err := teleportermessenger.PackReceiveCrossChainMessage( + 0, + common.HexToAddress(m.factory.messageConfig.RewardAddress), + ) if err != nil { m.logger.Error( "Failed packing receiveCrossChainMessage call data", @@ -282,10 +289,15 @@ func (m *messageHandler) SendMessage(signedMessage *warp.Message, destinationCli zap.String("warpMessageID", signedMessage.ID().String()), zap.String("teleporterMessageID", teleporterMessageID.String()), ) - return err + return common.Hash{}, err } - txHash, err := destinationClient.SendTx(signedMessage, m.factory.protocolAddress.Hex(), gasLimit, callData) + txHash, err := destinationClient.SendTx( + signedMessage, + m.factory.protocolAddress.Hex(), + gasLimit, + callData, + ) if err != nil { m.logger.Error( "Failed to send tx.", @@ -294,13 +306,13 @@ func (m *messageHandler) SendMessage(signedMessage *warp.Message, destinationCli zap.String("teleporterMessageID", teleporterMessageID.String()), zap.Error(err), ) - return err + return common.Hash{}, err } // Wait for the message to be included in a block before returning err = m.waitForReceipt(signedMessage, destinationClient, txHash, teleporterMessageID) if err != nil { - return err + return common.Hash{}, err } m.logger.Info( @@ -310,10 +322,15 @@ func (m *messageHandler) SendMessage(signedMessage *warp.Message, destinationCli zap.String("teleporterMessageID", teleporterMessageID.String()), zap.String("txHash", txHash.String()), ) - return nil + return txHash, nil } -func (m *messageHandler) waitForReceipt(signedMessage *warp.Message, destinationClient vms.DestinationClient, txHash common.Hash, teleporterMessageID ids.ID) error { +func (m *messageHandler) waitForReceipt( + signedMessage *warp.Message, + destinationClient vms.DestinationClient, + txHash common.Hash, + teleporterMessageID ids.ID, +) error { destinationBlockchainID := destinationClient.DestinationBlockchainID() callCtx, callCtxCancel := context.WithTimeout(context.Background(), 30*time.Second) defer callCtxCancel() @@ -348,7 +365,9 @@ func (m *messageHandler) waitForReceipt(signedMessage *warp.Message, destination // parseTeleporterMessage returns the Warp message's corresponding Teleporter message from the cache if it exists. // Otherwise parses the Warp message payload. -func (f *factory) parseTeleporterMessage(unsignedMessage *warp.UnsignedMessage) (*teleportermessenger.TeleporterMessage, error) { +func (f *factory) parseTeleporterMessage( + unsignedMessage *warp.UnsignedMessage, +) (*teleportermessenger.TeleporterMessage, error) { addressedPayload, err := warpPayload.ParseAddressedCall(unsignedMessage.Payload) if err != nil { f.logger.Error( @@ -372,10 +391,15 @@ func (f *factory) parseTeleporterMessage(unsignedMessage *warp.UnsignedMessage) // getTeleporterMessenger returns the Teleporter messenger instance for the destination chain. // Panic instead of returning errors because this should never happen, and if it does, we do not // want to log and swallow the error, since operations after this will fail too. -func (f *factory) getTeleporterMessenger(destinationClient vms.DestinationClient) *teleportermessenger.TeleporterMessenger { +func (f *factory) getTeleporterMessenger( + destinationClient vms.DestinationClient, +) *teleportermessenger.TeleporterMessenger { client, ok := destinationClient.Client().(ethclient.Client) if !ok { - panic(fmt.Sprintf("Destination client for chain %s is not an Ethereum client", destinationClient.DestinationBlockchainID().String())) + panic(fmt.Sprintf( + "Destination client for chain %s is not an Ethereum client", + destinationClient.DestinationBlockchainID().String()), + ) } // Get the teleporter messenger contract diff --git a/messages/teleporter/message_handler_test.go b/messages/teleporter/message_handler_test.go index 73ae035d..e74bff62 100644 --- a/messages/teleporter/message_handler_test.go +++ b/messages/teleporter/message_handler_test.go @@ -73,14 +73,26 @@ func TestShouldSendMessage(t *testing.T) { validMessageBytes, err := teleportermessenger.PackTeleporterMessage(validTeleporterMessage) require.NoError(t, err) - validAddressedCall, err := warpPayload.NewAddressedCall(messageProtocolAddress.Bytes(), validMessageBytes) + validAddressedCall, err := warpPayload.NewAddressedCall( + messageProtocolAddress.Bytes(), + validMessageBytes, + ) require.NoError(t, err) sourceBlockchainID := ids.Empty - warpUnsignedMessage, err := warp.NewUnsignedMessage(0, sourceBlockchainID, validAddressedCall.Bytes()) + warpUnsignedMessage, err := warp.NewUnsignedMessage( + 0, + sourceBlockchainID, + validAddressedCall.Bytes(), + ) require.NoError(t, err) - messageID, err := teleporterUtils.CalculateMessageID(messageProtocolAddress, sourceBlockchainID, destinationBlockchainID, validTeleporterMessage.MessageNonce) + messageID, err := teleporterUtils.CalculateMessageID( + messageProtocolAddress, + sourceBlockchainID, + destinationBlockchainID, + validTeleporterMessage.MessageNonce, + ) require.NoError(t, err) messageReceivedInput, err := teleportermessenger.PackMessageReceived(messageID) @@ -92,9 +104,16 @@ func TestShouldSendMessage(t *testing.T) { messageDelivered, err := teleportermessenger.PackMessageReceivedOutput(true) require.NoError(t, err) - invalidAddressedCall, err := warpPayload.NewAddressedCall(messageProtocolAddress.Bytes(), validMessageBytes) + invalidAddressedCall, err := warpPayload.NewAddressedCall( + messageProtocolAddress.Bytes(), + validMessageBytes, + ) require.NoError(t, err) - invalidWarpUnsignedMessage, err := warp.NewUnsignedMessage(0, sourceBlockchainID, append(invalidAddressedCall.Bytes(), []byte{1, 2, 3, 4}...)) + invalidWarpUnsignedMessage, err := warp.NewUnsignedMessage( + 0, + sourceBlockchainID, + append(invalidAddressedCall.Bytes(), []byte{1, 2, 3, 4}...), + ) require.NoError(t, err) testCases := []struct { diff --git a/peers/app_request_network.go b/peers/app_request_network.go index 8a915cde..736154a6 100644 --- a/peers/app_request_network.go +++ b/peers/app_request_network.go @@ -263,8 +263,12 @@ func (n *AppRequestNetwork) ConnectToCanonicalValidators(subnetID ids.ID) (*Conn // Private helpers -// Connect to the validators of the source blockchain. For each destination blockchain, verify that we have connected to a threshold of stake. -func (n *AppRequestNetwork) connectToNonPrimaryNetworkPeers(cfg *config.Config, sourceBlockchain *config.SourceBlockchain) error { +// Connect to the validators of the source blockchain. For each destination blockchain, +// verify that we have connected to a threshold of stake. +func (n *AppRequestNetwork) connectToNonPrimaryNetworkPeers( + cfg *config.Config, + sourceBlockchain *config.SourceBlockchain, +) error { subnetID := sourceBlockchain.GetSubnetID() connectedValidators, err := n.ConnectToCanonicalValidators(subnetID) if err != nil { @@ -291,8 +295,12 @@ func (n *AppRequestNetwork) connectToNonPrimaryNetworkPeers(cfg *config.Config, return nil } -// Connect to the validators of the destination blockchains. Verify that we have connected to a threshold of stake for each blockchain. -func (n *AppRequestNetwork) connectToPrimaryNetworkPeers(cfg *config.Config, sourceBlockchain *config.SourceBlockchain) error { +// Connect to the validators of the destination blockchains. Verify that we have connected +// to a threshold of stake for each blockchain. +func (n *AppRequestNetwork) connectToPrimaryNetworkPeers( + cfg *config.Config, + sourceBlockchain *config.SourceBlockchain, +) error { for _, destination := range sourceBlockchain.SupportedDestinations { blockchainID := destination.GetBlockchainID() subnetID := cfg.GetSubnetID(blockchainID) diff --git a/peers/external_handler.go b/peers/external_handler.go index 544597a9..7c925e1a 100644 --- a/peers/external_handler.go +++ b/peers/external_handler.go @@ -37,7 +37,8 @@ type expectedResponses struct { expected, received int } -// Create a new RelayerExternalHandler to forward relevant inbound app messages to the respective Teleporter application relayer, as well as handle timeouts. +// Create a new RelayerExternalHandler to forward relevant inbound app messages to the respective +// Teleporter application relayer, as well as handle timeouts. func NewRelayerExternalHandler( logger logging.Logger, registerer prometheus.Registerer, @@ -76,9 +77,10 @@ func NewRelayerExternalHandler( // For each inboundMessage, OnFinishedHandling must be called exactly once. However, since we handle relayer messages // async, we must call OnFinishedHandling manually across all code paths. // -// This diagram illustrates how HandleInbound forwards relevant AppResponses to the corresponding Teleporter application relayer. -// On startup, one Relayer goroutine is created per source subnet, which listens to the subscriber for cross-chain messages -// When a cross-chain message is picked up by a Relayer, HandleInbound routes AppResponses traffic to the appropriate Relayer +// This diagram illustrates how HandleInbound forwards relevant AppResponses to the corresponding +// Teleporter application relayer. On startup, one Relayer goroutine is created per source subnet, +// which listens to the subscriber for cross-chain messages. When a cross-chain message is picked +// up by a Relayer, HandleInbound routes AppResponses traffic to the appropriate Relayer. func (h *RelayerExternalHandler) HandleInbound(_ context.Context, inboundMessage message.InboundMessage) { h.log.Debug( "Handling app response", @@ -109,9 +111,13 @@ func (h *RelayerExternalHandler) Disconnected(nodeID ids.NodeID) { ) } -// RegisterRequestID registers an AppRequest by requestID, and marks the number of expected responses, equivalent to the number of nodes requested. -// requestID should be globally unique for the lifetime of the AppRequest. This is upper bounded by the timeout duration. -func (h *RelayerExternalHandler) RegisterRequestID(requestID uint32, numExpectedResponses int) chan message.InboundMessage { +// RegisterRequestID registers an AppRequest by requestID, and marks the number of +// expected responses, equivalent to the number of nodes requested. requestID should +// be globally unique for the lifetime of the AppRequest. This is upper bounded by the timeout duration. +func (h *RelayerExternalHandler) RegisterRequestID( + requestID uint32, + numExpectedResponses int, +) chan message.InboundMessage { // Create a channel to receive the response h.lock.Lock() defer h.lock.Unlock() diff --git a/peers/validators/canonical_validator_client.go b/peers/validators/canonical_validator_client.go index 225638bc..c3d01dc7 100644 --- a/peers/validators/canonical_validator_client.go +++ b/peers/validators/canonical_validator_client.go @@ -36,7 +36,9 @@ func NewCanonicalValidatorClient(logger logging.Logger, apiConfig *config.APICon } } -func (v *CanonicalValidatorClient) GetCurrentCanonicalValidatorSet(subnetID ids.ID) ([]*avalancheWarp.Validator, uint64, error) { +func (v *CanonicalValidatorClient) GetCurrentCanonicalValidatorSet( + subnetID ids.ID, +) ([]*avalancheWarp.Validator, uint64, error) { height, err := v.GetCurrentHeight(context.Background()) if err != nil { v.logger.Error( @@ -109,7 +111,8 @@ func (v *CanonicalValidatorClient) GetValidatorSet( // as well as their BLS public keys. func (v *CanonicalValidatorClient) getCurrentValidatorSet( ctx context.Context, - subnetID ids.ID) (map[ids.NodeID]*validators.GetValidatorOutput, error) { + subnetID ids.ID, +) (map[ids.NodeID]*validators.GetValidatorOutput, error) { // Get the current subnet validators. These validators are not expected to include // BLS signing information given that addPermissionlessValidatorTx is only used to // add primary network validators. diff --git a/relayer/application_relayer.go b/relayer/application_relayer.go index c1f59600..e2bcfde9 100644 --- a/relayer/application_relayer.go +++ b/relayer/application_relayer.go @@ -31,6 +31,7 @@ import ( coreEthMsg "github.com/ava-labs/coreth/plugin/evm/message" msg "github.com/ava-labs/subnet-evm/plugin/evm/message" "github.com/ava-labs/subnet-evm/rpc" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "golang.org/x/sync/errgroup" @@ -42,7 +43,8 @@ type blsSignatureBuf [bls.SignatureLen]byte const ( // Number of retries to collect signatures from validators maxRelayerQueryAttempts = 5 - // Maximum amount of time to spend waiting (in addition to network round trip time per attempt) during relayer signature query routine + // Maximum amount of time to spend waiting (in addition to network round trip time per attempt) + // during relayer signature query routine signatureRequestRetryWaitPeriodMs = 10_000 ) @@ -98,7 +100,8 @@ func NewApplicationRelayer( } var signingSubnet ids.ID if sourceBlockchain.GetSubnetID() == constants.PrimaryNetworkID { - // If the message originates from the primary subnet, then we instead "self sign" the message using the validators of the destination subnet. + // If the message originates from the primary subnet, then we instead "self sign" + // the message using the validators of the destination subnet. signingSubnet = cfg.GetSubnetID(relayerID.DestinationBlockchainID) } else { // Otherwise, the source subnet signs the message. @@ -107,12 +110,19 @@ func NewApplicationRelayer( sub := ticker.Subscribe() - checkpointManager := checkpoint.NewCheckpointManager(logger, db, sub, relayerID, startingHeight) + checkpointManager := checkpoint.NewCheckpointManager( + logger, + db, + sub, + relayerID, + startingHeight, + ) checkpointManager.Run() var warpClient *rpc.Client if !sourceBlockchain.UseAppRequestNetwork() { - // The subnet-evm Warp API client does not support query parameters or HTTP headers, and expects the URI to be in a specific form. + // The subnet-evm Warp API client does not support query parameters or HTTP headers + // and expects the URI to be in a specific form. // Instead, we invoke the Warp API directly via the RPC client. warpClient, err = utils.DialWithConfig( context.Background(), @@ -150,15 +160,21 @@ func NewApplicationRelayer( // Process [msgs] at height [height] by relaying each message to the destination chain. // Checkpoints the height with the checkpoint manager when all messages are relayed. -// ProcessHeight is expected to be called for every block greater than or equal to the [startingHeight] provided in the constructor -func (r *ApplicationRelayer) ProcessHeight(height uint64, handlers []messages.MessageHandler, errChan chan error) { +// ProcessHeight is expected to be called for every block greater than or equal to the +// [startingHeight] provided in the constructor. +func (r *ApplicationRelayer) ProcessHeight( + height uint64, + handlers []messages.MessageHandler, + errChan chan error, +) { var eg errgroup.Group for _, handler := range handlers { - // Copy the loop variable to a local variable to avoid the loop variable being captured by the goroutine - // Once we upgrade to Go 1.22, we can use the loop variable directly in the goroutine + // Copy the loop variable to a local variable to avoid the loop variable being captured by the + // goroutine. Once we upgrade to Go 1.22, we can use the loop variable directly in the goroutine. h := handler eg.Go(func() error { - return r.ProcessMessage(h) + _, err := r.ProcessMessage(h) + return err }) } if err := eg.Wait(); err != nil { @@ -182,29 +198,26 @@ func (r *ApplicationRelayer) ProcessHeight(height uint64, handlers []messages.Me } // Relays a message to the destination chain. Does not checkpoint the height. -func (r *ApplicationRelayer) ProcessMessage(handler messages.MessageHandler) error { +// returns the transaction hash if the message is successfully relayed. +func (r *ApplicationRelayer) ProcessMessage(handler messages.MessageHandler) (common.Hash, error) { // Increment the request ID. Make sure we don't hold the lock while we relay the message. r.lock.Lock() r.currentRequestID++ reqID := r.currentRequestID r.lock.Unlock() - err := r.relayMessage( - reqID, - handler, - ) - - return err + return r.relayMessage(reqID, handler) } func (r *ApplicationRelayer) RelayerID() database.RelayerID { return r.relayerID } +// returns the transaction hash if the message is successfully relayed. func (r *ApplicationRelayer) relayMessage( requestID uint32, handler messages.MessageHandler, -) error { +) (common.Hash, error) { r.logger.Debug( "Relaying message", zap.Uint32("requestID", requestID), @@ -218,11 +231,11 @@ func (r *ApplicationRelayer) relayMessage( zap.Error(err), ) r.incFailedRelayMessageCount("failed to check if message should be sent") - return err + return common.Hash{}, err } if !shouldSend { r.logger.Info("Message should not be sent") - return nil + return common.Hash{}, nil } unsignedMessage := handler.GetUnsignedMessage() @@ -240,7 +253,7 @@ func (r *ApplicationRelayer) relayMessage( zap.Error(err), ) r.incFailedRelayMessageCount("failed to create signed warp message via AppRequest network") - return err + return common.Hash{}, err } } else { r.incFetchSignatureRPCCount() @@ -251,35 +264,38 @@ func (r *ApplicationRelayer) relayMessage( zap.Error(err), ) r.incFailedRelayMessageCount("failed to create signed warp message via RPC") - return err + return common.Hash{}, err } } // create signed message latency (ms) r.setCreateSignedMessageLatencyMS(float64(time.Since(startCreateSignedMessageTime).Milliseconds())) - err = handler.SendMessage(signedMessage, r.destinationClient) + txHash, err := handler.SendMessage(signedMessage, r.destinationClient) if err != nil { r.logger.Error( "Failed to send warp message", zap.Error(err), ) r.incFailedRelayMessageCount("failed to send warp message") - return err + return common.Hash{}, err } r.logger.Info( "Finished relaying message to destination chain", zap.String("destinationBlockchainID", r.relayerID.DestinationBlockchainID.String()), + zap.String("txHash", txHash.Hex()), ) r.incSuccessfulRelayMessageCount() - return nil + return txHash, nil } // createSignedMessage fetches the signed Warp message from the source chain via RPC. // Each VM may implement their own RPC method to construct the aggregate signature, which // will need to be accounted for here. -func (r *ApplicationRelayer) createSignedMessage(unsignedMessage *avalancheWarp.UnsignedMessage) (*avalancheWarp.Message, error) { +func (r *ApplicationRelayer) createSignedMessage( + unsignedMessage *avalancheWarp.UnsignedMessage, +) (*avalancheWarp.Message, error) { r.logger.Info("Fetching aggregate signature from the source chain validators via API") var ( @@ -335,8 +351,12 @@ func (r *ApplicationRelayer) createSignedMessage(unsignedMessage *avalancheWarp. return nil, errFailedToGetAggSig } -// createSignedMessageAppRequest collects signatures from nodes by directly querying them via AppRequest, then aggregates the signatures, and constructs the signed warp message. -func (r *ApplicationRelayer) createSignedMessageAppRequest(unsignedMessage *avalancheWarp.UnsignedMessage, requestID uint32) (*avalancheWarp.Message, error) { +// createSignedMessageAppRequest collects signatures from nodes by directly querying them +// via AppRequest, then aggregates the signatures, and constructs the signed warp message. +func (r *ApplicationRelayer) createSignedMessageAppRequest( + unsignedMessage *avalancheWarp.UnsignedMessage, + requestID uint32, +) (*avalancheWarp.Message, error) { r.logger.Info( "Fetching aggregate signature from the source chain validators via AppRequest", zap.String("warpMessageID", unsignedMessage.ID().String()), @@ -390,7 +410,12 @@ func (r *ApplicationRelayer) createSignedMessageAppRequest(unsignedMessage *aval } // Construct the AppRequest - outMsg, err := r.messageCreator.AppRequest(unsignedMessage.SourceChainID, requestID, peers.DefaultAppRequestTimeout, reqBytes) + outMsg, err := r.messageCreator.AppRequest( + unsignedMessage.SourceChainID, + requestID, + peers.DefaultAppRequestTimeout, + reqBytes, + ) if err != nil { r.logger.Error( "Failed to create app request message", @@ -529,8 +554,9 @@ func (r *ApplicationRelayer) createSignedMessageAppRequest(unsignedMessage *aval // Attempts to create a signed warp message from the accumulated responses. // Returns a non-nil Warp message if [accumulatedSignatureWeight] exceeds the signature verification threshold. -// Returns false in the second return parameter if the app response is not relevant to the current signature aggregation request. -// Returns an error only if a non-recoverable error occurs, otherwise returns a nil error to continue processing responses. +// Returns false in the second return parameter if the app response is not relevant to the current signature +// aggregation request. Returns an error only if a non-recoverable error occurs, otherwise returns a nil error +// to continue processing responses. func (r *ApplicationRelayer) handleResponse( response message.InboundMessage, sentTo set.Set[ids.NodeID], @@ -608,10 +634,13 @@ func (r *ApplicationRelayer) handleResponse( return nil, true, err } - signedMsg, err := avalancheWarp.NewMessage(unsignedMessage, &avalancheWarp.BitSetSignature{ - Signers: vdrBitSet.Bytes(), - Signature: *(*[bls.SignatureLen]byte)(bls.SignatureToBytes(aggSig)), - }) + signedMsg, err := avalancheWarp.NewMessage( + unsignedMessage, + &avalancheWarp.BitSetSignature{ + Signers: vdrBitSet.Bytes(), + Signature: *(*[bls.SignatureLen]byte)(bls.SignatureToBytes(aggSig)), + }, + ) if err != nil { r.logger.Error( "Failed to create new signed message", @@ -628,8 +657,9 @@ func (r *ApplicationRelayer) handleResponse( return nil, true, nil } -// isValidSignatureResponse tries to generate a signature from the peer.AsyncResponse, then verifies the signature against the node's public key. -// If we are unable to generate the signature or verify correctly, false will be returned to indicate no valid signature was found in response. +// isValidSignatureResponse tries to generate a signature from the peer.AsyncResponse, then verifies +// the signature against the node's public key. If we are unable to generate the signature or verify +// correctly, false will be returned to indicate no valid signature was found in response. func (r *ApplicationRelayer) isValidSignatureResponse( unsignedMessage *avalancheWarp.UnsignedMessage, response message.InboundMessage, @@ -693,9 +723,12 @@ func (r *ApplicationRelayer) isValidSignatureResponse( return signature, true } -// aggregateSignatures constructs a BLS aggregate signature from the collected validator signatures. Also returns a bit set representing the -// validators that are represented in the aggregate signature. The bit set is in canonical validator order. -func (r *ApplicationRelayer) aggregateSignatures(signatureMap map[int]blsSignatureBuf) (*bls.Signature, set.Bits, error) { +// aggregateSignatures constructs a BLS aggregate signature from the collected validator signatures. Also +// returns a bit set representing the validators that are represented in the aggregate signature. The bit +// set is in canonical validator order. +func (r *ApplicationRelayer) aggregateSignatures( + signatureMap map[int]blsSignatureBuf, +) (*bls.Signature, set.Bits, error) { // Aggregate the signatures signatures := make([]*bls.Signature, 0, len(signatureMap)) vdrBitSet := set.NewBits() diff --git a/relayer/checkpoint/checkpoint.go b/relayer/checkpoint/checkpoint.go index 6d0e0e08..6df59338 100644 --- a/relayer/checkpoint/checkpoint.go +++ b/relayer/checkpoint/checkpoint.go @@ -81,7 +81,11 @@ func (cm *CheckpointManager) writeToDatabase() { zap.Uint64("height", cm.committedHeight), zap.String("relayerID", cm.relayerID.ID.String()), ) - err = cm.database.Put(cm.relayerID.ID, database.LatestProcessedBlockKey, []byte(strconv.FormatUint(cm.committedHeight, 10))) + err = cm.database.Put( + cm.relayerID.ID, + database.LatestProcessedBlockKey, + []byte(strconv.FormatUint(cm.committedHeight, 10)), + ) if err != nil { cm.logger.Error( "Failed to write latest processed block height", diff --git a/relayer/listener.go b/relayer/listener.go index 8cb3f594..42a832c3 100644 --- a/relayer/listener.go +++ b/relayer/listener.go @@ -8,20 +8,13 @@ import ( "fmt" "math/big" "math/rand" - "sync" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/awm-relayer/config" - "github.com/ava-labs/awm-relayer/database" - "github.com/ava-labs/awm-relayer/messages" - offchainregistry "github.com/ava-labs/awm-relayer/messages/off-chain-registry" - "github.com/ava-labs/awm-relayer/messages/teleporter" - relayerTypes "github.com/ava-labs/awm-relayer/types" "github.com/ava-labs/awm-relayer/utils" - vms "github.com/ava-labs/awm-relayer/vms" + "github.com/ava-labs/awm-relayer/vms" "github.com/ava-labs/subnet-evm/ethclient" - "github.com/ethereum/go-ethereum/common" "go.uber.org/atomic" "go.uber.org/zap" "google.golang.org/grpc" @@ -36,28 +29,63 @@ const ( // Listener handles all messages sent from a given source chain type Listener struct { - Subscriber vms.Subscriber - requestIDLock *sync.Mutex - currentRequestID uint32 - contractMessage vms.ContractMessage - messageHandlerFactories map[common.Address]messages.MessageHandlerFactory - logger logging.Logger - sourceBlockchain config.SourceBlockchain - catchUpResultChan chan bool - healthStatus *atomic.Bool - globalConfig *config.Config - applicationRelayers map[common.Hash]*ApplicationRelayer - ethClient ethclient.Client + Subscriber vms.Subscriber + currentRequestID uint32 + contractMessage vms.ContractMessage + logger logging.Logger + sourceBlockchain config.SourceBlockchain + catchUpResultChan chan bool + healthStatus *atomic.Bool + ethClient ethclient.Client + messageCoordinator *MessageCoordinator } -func NewListener( +// runListener creates a Listener instance and the ApplicationRelayers for a subnet. +// The Listener listens for warp messages on that subnet, and the ApplicationRelayers handle delivery to the destination +func RunListener( + ctx context.Context, logger logging.Logger, sourceBlockchain config.SourceBlockchain, + ethRPCClient ethclient.Client, relayerHealth *atomic.Bool, - globalConfig *config.Config, - applicationRelayers map[common.Hash]*ApplicationRelayer, + processMissedBlocks bool, + minHeight uint64, + messageCoordinator *MessageCoordinator, +) error { + // Create the Listener + listener, err := newListener( + ctx, + logger, + sourceBlockchain, + ethRPCClient, + relayerHealth, + processMissedBlocks, + minHeight, + messageCoordinator, + ) + if err != nil { + return fmt.Errorf("failed to create listener instance: %w", err) + } + + logger.Info( + "Listener initialized. Listening for messages to relay.", + zap.String("originBlockchainID", sourceBlockchain.BlockchainID), + ) + + // Wait for logs from the subscribed node + // Will only return on error or context cancellation + return listener.processLogs(ctx) +} + +func newListener( + ctx context.Context, + logger logging.Logger, + sourceBlockchain config.SourceBlockchain, + ethRPCClient ethclient.Client, + relayerHealth *atomic.Bool, + processMissedBlocks bool, startingHeight uint64, - ethClient ethclient.Client, + messageCoordinator *MessageCoordinator, grpcClient *grpc.ClientConn, ) (*Listener, error) { blockchainID, err := ids.FromString(sourceBlockchain.BlockchainID) @@ -68,8 +96,9 @@ func NewListener( ) return nil, err } + ethWSClient, err := utils.NewEthClientWithConfig( - context.Background(), + ctx, sourceBlockchain.WSEndpoint.BaseURL, sourceBlockchain.WSEndpoint.HTTPHeaders, sourceBlockchain.WSEndpoint.QueryParams, @@ -84,41 +113,6 @@ func NewListener( } sub := vms.NewSubscriber(logger, config.ParseVM(sourceBlockchain.VM), blockchainID, ethWSClient) - // Create message managers for each supported message protocol - messageHandlerFactories := make(map[common.Address]messages.MessageHandlerFactory) - for addressStr, cfg := range sourceBlockchain.MessageContracts { - address := common.HexToAddress(addressStr) - format := cfg.MessageFormat - var ( - m messages.MessageHandlerFactory - err error - ) - switch config.ParseMessageProtocol(format) { - case config.TELEPORTER: - m, err = teleporter.NewMessageHandlerFactory( - logger, - address, - cfg, - grpcClient, - ) - case config.OFF_CHAIN_REGISTRY: - m, err = offchainregistry.NewMessageHandlerFactory( - logger, - cfg, - ) - default: - m, err = nil, fmt.Errorf("invalid message format %s", format) - } - if err != nil { - logger.Error( - "Failed to create message manager", - zap.Error(err), - ) - return nil, err - } - messageHandlerFactories[address] = m - } - // Marks when the listener has finished the catch-up process on startup. // Until that time, we do not know the order in which messages are processed, // since the catch-up process occurs concurrently with normal message processing @@ -135,22 +129,19 @@ func NewListener( zap.String("blockchainIDHex", sourceBlockchain.GetBlockchainID().Hex()), ) lstnr := Listener{ - Subscriber: sub, - requestIDLock: &sync.Mutex{}, - currentRequestID: rand.Uint32(), // Initialize to a random value to mitigate requestID collision - contractMessage: vms.NewContractMessage(logger, sourceBlockchain), - messageHandlerFactories: messageHandlerFactories, - logger: logger, - sourceBlockchain: sourceBlockchain, - catchUpResultChan: catchUpResultChan, - healthStatus: relayerHealth, - globalConfig: globalConfig, - applicationRelayers: applicationRelayers, - ethClient: ethClient, - } - - // Open the subscription. We must do this before processing any missed messages, otherwise we may miss an incoming message - // in between fetching the latest block and subscribing. + Subscriber: sub, + currentRequestID: rand.Uint32(), // Initialize to a random value to mitigate requestID collision + contractMessage: vms.NewContractMessage(logger, sourceBlockchain), + logger: logger, + sourceBlockchain: sourceBlockchain, + catchUpResultChan: catchUpResultChan, + healthStatus: relayerHealth, + ethClient: ethRPCClient, + messageCoordinator: messageCoordinator, + } + + // Open the subscription. We must do this before processing any missed messages, otherwise we may + // miss an incoming message in between fetching the latest block and subscribing. err = lstnr.Subscriber.Subscribe(maxSubscribeAttempts) if err != nil { logger.Error( @@ -160,7 +151,7 @@ func NewListener( return nil, err } - if lstnr.globalConfig.ProcessMissedBlocks { + if processMissedBlocks { // Process historical blocks in a separate goroutine so that the main processing loop can // start processing new blocks as soon as possible. Otherwise, it's possible for // ProcessFromHeight to overload the message queue and cause a deadlock. @@ -179,7 +170,7 @@ func NewListener( // Listens to the Subscriber logs channel to process them. // On subscriber error, attempts to reconnect and errors if unable. // Exits if context is cancelled by another goroutine. -func (lstnr *Listener) ProcessLogs(ctx context.Context) error { +func (lstnr *Listener) processLogs(ctx context.Context) error { // Error channel for application relayer errors errChan := make(chan error) for { @@ -214,52 +205,7 @@ func (lstnr *Listener) ProcessLogs(ctx context.Context) error { return fmt.Errorf("failed to catch up on historical blocks") } case blockHeader := <-lstnr.Subscriber.Headers(): - // Parse the logs in the block, and group by application relayer - - block, err := relayerTypes.NewWarpBlockInfo(blockHeader, lstnr.ethClient) - if err != nil { - lstnr.logger.Error( - "Failed to create Warp block info", - zap.Error(err), - ) - continue - } - - // Relay the messages in the block to the destination chains. Continue on failure. - lstnr.logger.Debug( - "Processing block", - zap.String("sourceBlockchainID", lstnr.sourceBlockchain.GetBlockchainID().String()), - zap.Uint64("blockNumber", block.BlockNumber), - ) - - // Register each message in the block with the appropriate application relayer - messageHandlers := make(map[common.Hash][]messages.MessageHandler) - for _, warpLogInfo := range block.Messages { - appRelayer, handler, err := lstnr.GetAppRelayerMessageHandler(warpLogInfo) - if err != nil { - lstnr.logger.Error( - "Failed to parse message", - zap.String("blockchainID", lstnr.sourceBlockchain.GetBlockchainID().String()), - zap.Error(err), - ) - continue - } - if appRelayer == nil { - lstnr.logger.Debug("Application relayer not found. Skipping message relay") - continue - } - messageHandlers[appRelayer.relayerID.ID] = append(messageHandlers[appRelayer.relayerID.ID], handler) - } - // Initiate message relay of all registered messages - for _, appRelayer := range lstnr.applicationRelayers { - // Dispatch all messages in the block to the appropriate application relayer. - // An empty slice is still a valid argument to ProcessHeight; in this case the height is immediately committed. - handlers := messageHandlers[appRelayer.relayerID.ID] - - // Process the height async. This is safe because the ApplicationRelayer maintains the threadsafe - // invariant that heights are committed to the database one at a time, in order, with no gaps. - go appRelayer.ProcessHeight(block.BlockNumber, handlers, errChan) - } + go lstnr.messageCoordinator.ProcessBlock(blockHeader, lstnr.ethClient, errChan) case err := <-lstnr.Subscriber.Err(): lstnr.healthStatus.Store(false) lstnr.logger.Error( @@ -301,162 +247,3 @@ func (lstnr *Listener) reconnectToSubscriber() error { lstnr.healthStatus.Store(true) return nil } - -// Unpacks the Warp message and fetches the appropriate application relayer -// Checks for the following registered keys. At most one of these keys should be registered. -// 1. An exact match on sourceBlockchainID, destinationBlockchainID, originSenderAddress, and destinationAddress -// 2. A match on sourceBlockchainID and destinationBlockchainID, with a specific originSenderAddress and any destinationAddress -// 3. A match on sourceBlockchainID and destinationBlockchainID, with any originSenderAddress and a specific destinationAddress -// 4. A match on sourceBlockchainID and destinationBlockchainID, with any originSenderAddress and any destinationAddress -func (lstnr *Listener) getApplicationRelayer( - sourceBlockchainID ids.ID, - originSenderAddress common.Address, - destinationBlockchainID ids.ID, - destinationAddress common.Address, -) *ApplicationRelayer { - // Check for an exact match - applicationRelayerID := database.CalculateRelayerID( - sourceBlockchainID, - destinationBlockchainID, - originSenderAddress, - destinationAddress, - ) - if applicationRelayer, ok := lstnr.applicationRelayers[applicationRelayerID]; ok { - return applicationRelayer - } - - // Check for a match on sourceBlockchainID and destinationBlockchainID, with a specific originSenderAddress and any destinationAddress - applicationRelayerID = database.CalculateRelayerID( - sourceBlockchainID, - destinationBlockchainID, - originSenderAddress, - database.AllAllowedAddress, - ) - if applicationRelayer, ok := lstnr.applicationRelayers[applicationRelayerID]; ok { - return applicationRelayer - } - - // Check for a match on sourceBlockchainID and destinationBlockchainID, with any originSenderAddress and a specific destinationAddress - applicationRelayerID = database.CalculateRelayerID( - sourceBlockchainID, - destinationBlockchainID, - database.AllAllowedAddress, - destinationAddress, - ) - if applicationRelayer, ok := lstnr.applicationRelayers[applicationRelayerID]; ok { - return applicationRelayer - } - - // Check for a match on sourceBlockchainID and destinationBlockchainID, with any originSenderAddress and any destinationAddress - applicationRelayerID = database.CalculateRelayerID( - sourceBlockchainID, - destinationBlockchainID, - database.AllAllowedAddress, - database.AllAllowedAddress, - ) - if applicationRelayer, ok := lstnr.applicationRelayers[applicationRelayerID]; ok { - return applicationRelayer - } - lstnr.logger.Debug( - "Application relayer not found. Skipping message relay.", - zap.String("blockchainID", lstnr.sourceBlockchain.GetBlockchainID().String()), - zap.String("destinationBlockchainID", destinationBlockchainID.String()), - zap.String("originSenderAddress", originSenderAddress.String()), - zap.String("destinationAddress", destinationAddress.String()), - ) - return nil -} - -// Returns the ApplicationRelayer that is configured to handle this message, as well as a one-time MessageHandler -// instance that the ApplicationRelayer uses to relay this specific message. -// The MessageHandler and ApplicationRelayer are decoupled to support batch workflows in which a single ApplicationRelayer -// processes multiple messages (using their corresponding MessageHandlers) in a single shot. -func (lstnr *Listener) GetAppRelayerMessageHandler(warpMessageInfo *relayerTypes.WarpMessageInfo) ( - *ApplicationRelayer, - messages.MessageHandler, - error, -) { - // Check that the warp message is from a supported message protocol contract address. - messageHandlerFactory, supportedMessageProtocol := lstnr.messageHandlerFactories[warpMessageInfo.SourceAddress] - if !supportedMessageProtocol { - // Do not return an error here because it is expected for there to be messages from other contracts - // than just the ones supported by a single listener instance. - lstnr.logger.Debug( - "Warp message from unsupported message protocol address. Not relaying.", - zap.String("protocolAddress", warpMessageInfo.SourceAddress.Hex()), - ) - return nil, nil, nil - } - messageHandler, err := messageHandlerFactory.NewMessageHandler(warpMessageInfo.UnsignedMessage) - if err != nil { - lstnr.logger.Error( - "Failed to create message handler", - zap.Error(err), - ) - return nil, nil, err - } - - // Fetch the message delivery data - sourceBlockchainID, originSenderAddress, destinationBlockchainID, destinationAddress, err := messageHandler.GetMessageRoutingInfo() - if err != nil { - lstnr.logger.Error( - "Failed to get message routing information", - zap.Error(err), - ) - return nil, nil, err - } - - lstnr.logger.Info( - "Unpacked warp message", - zap.String("sourceBlockchainID", sourceBlockchainID.String()), - zap.String("originSenderAddress", originSenderAddress.String()), - zap.String("destinationBlockchainID", destinationBlockchainID.String()), - zap.String("destinationAddress", destinationAddress.String()), - zap.String("warpMessageID", warpMessageInfo.UnsignedMessage.ID().String()), - ) - - appRelayer := lstnr.getApplicationRelayer( - sourceBlockchainID, - originSenderAddress, - destinationBlockchainID, - destinationAddress, - ) - if appRelayer == nil { - return nil, nil, nil - } - return appRelayer, messageHandler, nil -} - -func (lstnr *Listener) ProcessManualWarpMessages( - logger logging.Logger, - manualWarpMessages []*relayerTypes.WarpMessageInfo, - sourceBlockchain config.SourceBlockchain, -) error { - // Send any messages that were specified in the configuration - for _, warpMessage := range manualWarpMessages { - logger.Info( - "Relaying manual Warp message", - zap.String("blockchainID", sourceBlockchain.BlockchainID), - zap.String("warpMessageID", warpMessage.UnsignedMessage.ID().String()), - ) - appRelayer, handler, err := lstnr.GetAppRelayerMessageHandler(warpMessage) - if err != nil { - logger.Error( - "Failed to parse manual Warp message.", - zap.Error(err), - zap.String("warpMessageID", warpMessage.UnsignedMessage.ID().String()), - ) - return err - } - err = appRelayer.ProcessMessage(handler) - if err != nil { - logger.Error( - "Failed to process manual Warp message", - zap.String("blockchainID", sourceBlockchain.BlockchainID), - zap.String("warpMessageID", warpMessage.UnsignedMessage.ID().String()), - ) - return err - } - } - return nil -} diff --git a/relayer/message_coordinator.go b/relayer/message_coordinator.go new file mode 100644 index 00000000..f9de1b1c --- /dev/null +++ b/relayer/message_coordinator.go @@ -0,0 +1,286 @@ +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package relayer + +import ( + "context" + "errors" + "fmt" + "math/big" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/awm-relayer/database" + "github.com/ava-labs/awm-relayer/messages" + relayerTypes "github.com/ava-labs/awm-relayer/types" + "github.com/ava-labs/subnet-evm/core/types" + "github.com/ava-labs/subnet-evm/ethclient" + "github.com/ava-labs/subnet-evm/interfaces" + "github.com/ava-labs/subnet-evm/precompile/contracts/warp" + "github.com/ethereum/go-ethereum/common" + "go.uber.org/zap" +) + +// MessageCoordinator contains all the logic required to process messages in the relayer. +// Other components such as the listeners or the API should pass messages to the MessageCoordinator +// so that it can parse the message(s) and pass them the the proper ApplicationRelayer. +type MessageCoordinator struct { + logger logging.Logger + // Maps Source blockchain ID and protocol address to a Message Handler Factory + messageHandlerFactories map[ids.ID]map[common.Address]messages.MessageHandlerFactory + applicationRelayers map[common.Hash]*ApplicationRelayer + sourceClients map[ids.ID]ethclient.Client +} + +func NewMessageCoordinator( + logger logging.Logger, + messageHandlerFactories map[ids.ID]map[common.Address]messages.MessageHandlerFactory, + applicationRelayers map[common.Hash]*ApplicationRelayer, + sourceClients map[ids.ID]ethclient.Client, +) *MessageCoordinator { + return &MessageCoordinator{ + logger: logger, + messageHandlerFactories: messageHandlerFactories, + applicationRelayers: applicationRelayers, + sourceClients: sourceClients, + } +} + +// getAppRelayerMessageHandler returns the ApplicationRelayer that is configured to handle this message, +// as well as a one-time MessageHandler instance that the ApplicationRelayer uses to relay this specific message. +// The MessageHandler and ApplicationRelayer are decoupled to support batch workflows in which a single +// ApplicationRelayer processes multiple messages (using their corresponding MessageHandlers) in a single shot. +func (mc *MessageCoordinator) getAppRelayerMessageHandler( + warpMessageInfo *relayerTypes.WarpMessageInfo, +) ( + *ApplicationRelayer, + messages.MessageHandler, + error, +) { + // Check that the warp message is from a supported message protocol contract address. + //nolint:lll + messageHandlerFactory, supportedMessageProtocol := mc.messageHandlerFactories[warpMessageInfo.UnsignedMessage.SourceChainID][warpMessageInfo.SourceAddress] + if !supportedMessageProtocol { + // Do not return an error here because it is expected for there to be messages from other contracts + // than just the ones supported by a single listener instance. + mc.logger.Debug( + "Warp message from unsupported message protocol address. Not relaying.", + zap.String("protocolAddress", warpMessageInfo.SourceAddress.Hex()), + ) + return nil, nil, nil + } + messageHandler, err := messageHandlerFactory.NewMessageHandler(warpMessageInfo.UnsignedMessage) + if err != nil { + mc.logger.Error("Failed to create message handler", zap.Error(err)) + return nil, nil, err + } + + // Fetch the message delivery data + //nolint:lll + sourceBlockchainID, originSenderAddress, destinationBlockchainID, destinationAddress, err := messageHandler.GetMessageRoutingInfo() + if err != nil { + mc.logger.Error("Failed to get message routing information", zap.Error(err)) + return nil, nil, err + } + + mc.logger.Info( + "Unpacked warp message", + zap.String("sourceBlockchainID", sourceBlockchainID.String()), + zap.String("originSenderAddress", originSenderAddress.String()), + zap.String("destinationBlockchainID", destinationBlockchainID.String()), + zap.String("destinationAddress", destinationAddress.String()), + zap.String("warpMessageID", warpMessageInfo.UnsignedMessage.ID().String()), + ) + + appRelayer := mc.getApplicationRelayer( + sourceBlockchainID, + originSenderAddress, + destinationBlockchainID, + destinationAddress, + ) + if appRelayer == nil { + return nil, nil, nil + } + return appRelayer, messageHandler, nil +} + +// Unpacks the Warp message and fetches the appropriate application relayer +// Checks for the following registered keys. At most one of these keys should be registered. +// 1. An exact match on sourceBlockchainID, destinationBlockchainID, originSenderAddress, and destinationAddress +// 2. A match on sourceBlockchainID and destinationBlockchainID, with a specific originSenderAddress and +// any destinationAddress +// 3. A match on sourceBlockchainID and destinationBlockchainID, with any originSenderAddress and a +// specific destinationAddress +// 4. A match on sourceBlockchainID and destinationBlockchainID, with any originSenderAddress and any +// destinationAddress +func (mc *MessageCoordinator) getApplicationRelayer( + sourceBlockchainID ids.ID, + originSenderAddress common.Address, + destinationBlockchainID ids.ID, + destinationAddress common.Address, +) *ApplicationRelayer { + // Check for an exact match + applicationRelayerID := database.CalculateRelayerID( + sourceBlockchainID, + destinationBlockchainID, + originSenderAddress, + destinationAddress, + ) + if applicationRelayer, ok := mc.applicationRelayers[applicationRelayerID]; ok { + return applicationRelayer + } + + // Check for a match on sourceBlockchainID and destinationBlockchainID, with a specific + // originSenderAddress and any destinationAddress. + applicationRelayerID = database.CalculateRelayerID( + sourceBlockchainID, + destinationBlockchainID, + originSenderAddress, + database.AllAllowedAddress, + ) + if applicationRelayer, ok := mc.applicationRelayers[applicationRelayerID]; ok { + return applicationRelayer + } + + // Check for a match on sourceBlockchainID and destinationBlockchainID, with any originSenderAddress + // and a specific destinationAddress. + applicationRelayerID = database.CalculateRelayerID( + sourceBlockchainID, + destinationBlockchainID, + database.AllAllowedAddress, + destinationAddress, + ) + if applicationRelayer, ok := mc.applicationRelayers[applicationRelayerID]; ok { + return applicationRelayer + } + + // Check for a match on sourceBlockchainID and destinationBlockchainID, with any originSenderAddress + // and any destinationAddress. + applicationRelayerID = database.CalculateRelayerID( + sourceBlockchainID, + destinationBlockchainID, + database.AllAllowedAddress, + database.AllAllowedAddress, + ) + if applicationRelayer, ok := mc.applicationRelayers[applicationRelayerID]; ok { + return applicationRelayer + } + mc.logger.Debug( + "Application relayer not found. Skipping message relay.", + zap.String("blockchainID", sourceBlockchainID.String()), + zap.String("destinationBlockchainID", destinationBlockchainID.String()), + zap.String("originSenderAddress", originSenderAddress.String()), + zap.String("destinationAddress", destinationAddress.String()), + ) + return nil +} + +func (mc *MessageCoordinator) ProcessWarpMessage(warpMessage *relayerTypes.WarpMessageInfo) (common.Hash, error) { + appRelayer, handler, err := mc.getAppRelayerMessageHandler(warpMessage) + if err != nil { + mc.logger.Error( + "Failed to parse Warp message.", + zap.Error(err), + zap.String("warpMessageID", warpMessage.UnsignedMessage.ID().String()), + ) + return common.Hash{}, err + } + if appRelayer == nil { + mc.logger.Error("Application relayer not found") + return common.Hash{}, errors.New("application relayer not found") + } + + return appRelayer.ProcessMessage(handler) +} + +func (mc *MessageCoordinator) ProcessMessageID( + blockchainID ids.ID, + messageID ids.ID, + blockNum *big.Int, +) (common.Hash, error) { + ethClient, ok := mc.sourceClients[blockchainID] + if !ok { + mc.logger.Error( + "Source client not found", + zap.String("blockchainID", blockchainID.String()), + ) + return common.Hash{}, fmt.Errorf("source client not set for blockchain: %s", blockchainID.String()) + } + + warpMessage, err := FetchWarpMessage(ethClient, messageID, blockNum) + if err != nil { + mc.logger.Error( + "Failed to fetch warp from blockchain", + zap.String("blockchainID", blockchainID.String()), + zap.Error(err), + ) + return common.Hash{}, fmt.Errorf("could not fetch warp message from ID: %w", err) + } + + return mc.ProcessWarpMessage(warpMessage) +} + +// Meant to be ran asynchronously. Errors should be sent to errChan. +func (mc *MessageCoordinator) ProcessBlock( + blockHeader *types.Header, + ethClient ethclient.Client, + errChan chan error, +) { + // Parse the logs in the block, and group by application relayer + block, err := relayerTypes.NewWarpBlockInfo(blockHeader, ethClient) + if err != nil { + mc.logger.Error("Failed to create Warp block info", zap.Error(err)) + errChan <- err + return + } + + // Register each message in the block with the appropriate application relayer + messageHandlers := make(map[common.Hash][]messages.MessageHandler) + for _, warpLogInfo := range block.Messages { + appRelayer, handler, err := mc.getAppRelayerMessageHandler(warpLogInfo) + if err != nil { + mc.logger.Error( + "Failed to parse message", + zap.String("blockchainID", warpLogInfo.UnsignedMessage.SourceChainID.String()), + zap.String("protocolAddress", warpLogInfo.SourceAddress.String()), + zap.Error(err), + ) + continue + } + if appRelayer == nil { + mc.logger.Debug("Application relayer not found. Skipping message relay") + continue + } + messageHandlers[appRelayer.relayerID.ID] = append(messageHandlers[appRelayer.relayerID.ID], handler) + } + // Initiate message relay of all registered messages + for _, appRelayer := range mc.applicationRelayers { + // Dispatch all messages in the block to the appropriate application relayer. + // An empty slice is still a valid argument to ProcessHeight; in this case the height is immediately committed. + handlers := messageHandlers[appRelayer.relayerID.ID] + + go appRelayer.ProcessHeight(block.BlockNumber, handlers, errChan) + } +} + +func FetchWarpMessage( + ethClient ethclient.Client, + warpID ids.ID, + blockNum *big.Int, +) (*relayerTypes.WarpMessageInfo, error) { + logs, err := ethClient.FilterLogs(context.Background(), interfaces.FilterQuery{ + Topics: [][]common.Hash{{relayerTypes.WarpPrecompileLogFilter}, nil, {common.Hash(warpID)}}, + Addresses: []common.Address{warp.ContractAddress}, + FromBlock: blockNum, + ToBlock: blockNum, + }) + if err != nil { + return nil, fmt.Errorf("could not fetch logs: %w", err) + } + if len(logs) != 1 { + return nil, fmt.Errorf("found more than 1 log: %d", len(logs)) + } + + return relayerTypes.NewWarpMessageInfo(logs[0]) +} diff --git a/tests/allowed_addresses.go b/tests/allowed_addresses.go index 4f5f2ca8..dbaa809a 100644 --- a/tests/allowed_addresses.go +++ b/tests/allowed_addresses.go @@ -28,12 +28,14 @@ const relayerCfgFname4 = "relayer-config-4.json" const numKeys = 4 // Tests allowed source and destination address functionality. -// First, relays messages using distinct relayer instances that all write to the same database. The instances are configured to: +// First, relays messages using distinct relayer instances that all write to the same database. +// The instances are configured to: // - Deliver from any source address to any destination address // - Deliver from a specific source address to any destination address // - Deliver from any source address to a specific destination address // - Deliver from a specific source address to a specific destination address -// Then, checks that each relayer instance is able to properly catch up on missed messages that match its particular configuration +// Then, checks that each relayer instance is able to properly catch up on missed messages that +// match its particular configuration. func AllowedAddresses(network interfaces.LocalNetwork) { subnetAInfo := network.GetPrimaryNetworkInfo() subnetBInfo, _ := utils.GetTwoSubnets(network) diff --git a/tests/basic_relay.go b/tests/basic_relay.go index 1c2a52df..713aef5c 100644 --- a/tests/basic_relay.go +++ b/tests/basic_relay.go @@ -52,6 +52,8 @@ func BasicRelay(network interfaces.LocalNetwork) { fundedAddress, relayerKey, ) + // The config needs to be validated in order to be passed to database.GetConfigRelayerIDs + relayerConfig.Validate() relayerConfigPath := testUtils.WriteRelayerConfig(relayerConfig, testUtils.DefaultRelayerCfgFname) @@ -107,15 +109,31 @@ func BasicRelay(network interfaces.LocalNetwork) { logging.JSON.ConsoleEncoder(), ), ) - jsonDB, err := database.NewJSONFileStorage(logger, relayerConfig.StorageLocation, database.GetConfigRelayerIDs(&relayerConfig)) + jsonDB, err := database.NewJSONFileStorage( + logger, + relayerConfig.StorageLocation, + database.GetConfigRelayerIDs(&relayerConfig), + ) Expect(err).Should(BeNil()) // Create relayer keys that allow all source and destination addresses - relayerIDA := database.CalculateRelayerID(subnetAInfo.BlockchainID, subnetBInfo.BlockchainID, database.AllAllowedAddress, database.AllAllowedAddress) - relayerIDB := database.CalculateRelayerID(subnetBInfo.BlockchainID, subnetAInfo.BlockchainID, database.AllAllowedAddress, database.AllAllowedAddress) + relayerIDA := database.CalculateRelayerID( + subnetAInfo.BlockchainID, + subnetBInfo.BlockchainID, + database.AllAllowedAddress, + database.AllAllowedAddress, + ) + relayerIDB := database.CalculateRelayerID( + subnetBInfo.BlockchainID, + subnetAInfo.BlockchainID, + database.AllAllowedAddress, + database.AllAllowedAddress, + ) // Modify the JSON database to force the relayer to re-process old blocks - jsonDB.Put(relayerIDA, database.LatestProcessedBlockKey, []byte("0")) - jsonDB.Put(relayerIDB, database.LatestProcessedBlockKey, []byte("0")) + err = jsonDB.Put(relayerIDA, database.LatestProcessedBlockKey, []byte("0")) + Expect(err).Should(BeNil()) + err = jsonDB.Put(relayerIDB, database.LatestProcessedBlockKey, []byte("0")) + Expect(err).Should(BeNil()) // Subscribe to the destination chain newHeadsB := make(chan *types.Header, 10) @@ -128,7 +146,8 @@ func BasicRelay(network interfaces.LocalNetwork) { relayerCleanup = testUtils.BuildAndRunRelayerExecutable(ctx, relayerConfigPath) defer relayerCleanup() - // We should not receive a new block on subnet B, since the relayer should have seen the Teleporter message was already delivered + // We should not receive a new block on subnet B, since the relayer should have + // seen the Teleporter message was already delivered. log.Info("Waiting for 10s to ensure no new block confirmations on destination chain") Consistently(newHeadsB, 10*time.Second, 500*time.Millisecond).ShouldNot(Receive()) diff --git a/tests/batch_relay.go b/tests/batch_relay.go index 808eb436..aebde3bb 100644 --- a/tests/batch_relay.go +++ b/tests/batch_relay.go @@ -119,7 +119,13 @@ func BatchRelay(network interfaces.LocalNetwork) { } currWait++ if currWait == maxWait { - Expect(false).Should(BeTrue(), fmt.Sprintf("did not receive all sent messages in time. received %d/%d", numMessages-sentMessages.Len(), numMessages)) + Expect(false).Should(BeTrue(), + fmt.Sprintf( + "did not receive all sent messages in time. received %d/%d", + numMessages-sentMessages.Len(), + numMessages, + ), + ) } time.Sleep(1 * time.Second) } diff --git a/tests/e2e_test.go b/tests/e2e_test.go index 8c1bd68f..c26e78fa 100644 --- a/tests/e2e_test.go +++ b/tests/e2e_test.go @@ -46,10 +46,18 @@ var _ = ginkgo.BeforeSuite(func() { localNetworkInstance = local.NewLocalNetwork(warpGenesisFile) // Generate the Teleporter deployment values - teleporterContractAddress := common.HexToAddress(testUtils.ReadHexTextFile("./tests/utils/UniversalTeleporterMessengerContractAddress.txt")) - teleporterDeployerAddress := common.HexToAddress(testUtils.ReadHexTextFile("./tests/utils/UniversalTeleporterDeployerAddress.txt")) - teleporterDeployerTransactionStr := testUtils.ReadHexTextFile("./tests/utils/UniversalTeleporterDeployerTransaction.txt") - teleporterDeployerTransaction, err := hex.DecodeString(utils.SanitizeHexString(teleporterDeployerTransactionStr)) + teleporterContractAddress := common.HexToAddress( + testUtils.ReadHexTextFile("./tests/utils/UniversalTeleporterMessengerContractAddress.txt"), + ) + teleporterDeployerAddress := common.HexToAddress( + testUtils.ReadHexTextFile("./tests/utils/UniversalTeleporterDeployerAddress.txt"), + ) + teleporterDeployerTransactionStr := testUtils.ReadHexTextFile( + "./tests/utils/UniversalTeleporterDeployerTransaction.txt", + ) + teleporterDeployerTransaction, err := hex.DecodeString( + utils.SanitizeHexString(teleporterDeployerTransactionStr), + ) Expect(err).Should(BeNil()) _, fundedKey := localNetworkInstance.GetFundedAccountInfo() @@ -61,7 +69,10 @@ var _ = ginkgo.BeforeSuite(func() { true, ) log.Info("Deployed Teleporter contracts") - localNetworkInstance.DeployTeleporterRegistryContracts(teleporterContractAddress, fundedKey) + localNetworkInstance.DeployTeleporterRegistryContracts( + teleporterContractAddress, + fundedKey, + ) var ctx context.Context ctx, cancelDecider = context.WithCancel(context.Background()) @@ -96,9 +107,6 @@ var _ = ginkgo.Describe("[AWM Relayer Integration Tests", func() { ginkgo.It("Basic Relay", func() { BasicRelay(localNetworkInstance) }) - ginkgo.It("Teleporter Registry", func() { - TeleporterRegistry(localNetworkInstance) - }) ginkgo.It("Shared Database", func() { SharedDatabaseAccess(localNetworkInstance) }) @@ -108,6 +116,9 @@ var _ = ginkgo.Describe("[AWM Relayer Integration Tests", func() { ginkgo.It("Batch Message", func() { BatchRelay(localNetworkInstance) }) + ginkgo.It("Relay Message API", func() { + RelayMessageAPI(localNetworkInstance) + }) ginkgo.It("Warp API", func() { WarpAPIRelay(localNetworkInstance) }) diff --git a/tests/manual_message.go b/tests/manual_message.go index 9a487d98..400eb5a0 100644 --- a/tests/manual_message.go +++ b/tests/manual_message.go @@ -4,35 +4,45 @@ package tests import ( + "bytes" "context" - "encoding/hex" + "encoding/json" + "fmt" + "math/big" + "net/http" "time" - avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" - "github.com/ava-labs/awm-relayer/config" + runner_sdk "github.com/ava-labs/avalanche-network-runner/client" + "github.com/ava-labs/awm-relayer/api" + offchainregistry "github.com/ava-labs/awm-relayer/messages/off-chain-registry" testUtils "github.com/ava-labs/awm-relayer/tests/utils" "github.com/ava-labs/subnet-evm/accounts/abi/bind" - "github.com/ava-labs/subnet-evm/core/types" - subnetEvmInterfaces "github.com/ava-labs/subnet-evm/interfaces" - "github.com/ava-labs/subnet-evm/precompile/contracts/warp" "github.com/ava-labs/teleporter/tests/interfaces" - "github.com/ava-labs/teleporter/tests/utils" + teleporterTestUtils "github.com/ava-labs/teleporter/tests/utils" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" - . "github.com/onsi/gomega" ) -// This tests relaying a message manually provided in the relayer config +// Tests relayer support for off-chain Teleporter Registry updates +// - Configures the relayer to send an off-chain message to the Teleporter Registry +// - Verifies that the Teleporter Registry is updated func ManualMessage(network interfaces.LocalNetwork) { - subnetAInfo := network.GetPrimaryNetworkInfo() - subnetBInfo, _ := utils.GetTwoSubnets(network) + cChainInfo := network.GetPrimaryNetworkInfo() + subnetAInfo, subnetBInfo := teleporterTestUtils.GetTwoSubnets(network) fundedAddress, fundedKey := network.GetFundedAccountInfo() teleporterContractAddress := network.GetTeleporterContractAddress() err := testUtils.ClearRelayerStorage() Expect(err).Should(BeNil()) + // + // Get the current Teleporter Registry version + // + currentVersion, err := cChainInfo.TeleporterRegistry.LatestVersion(&bind.CallOpts{}) + Expect(err).Should(BeNil()) + expectedNewVersion := currentVersion.Add(currentVersion, big.NewInt(1)) + // // Fund the relayer address on all subnets // @@ -41,89 +51,101 @@ func ManualMessage(network interfaces.LocalNetwork) { log.Info("Funding relayer address on all subnets") relayerKey, err := crypto.GenerateKey() Expect(err).Should(BeNil()) - testUtils.FundRelayers(ctx, []interfaces.SubnetTestInfo{subnetAInfo, subnetBInfo}, fundedKey, relayerKey) + testUtils.FundRelayers(ctx, []interfaces.SubnetTestInfo{cChainInfo}, fundedKey, relayerKey) + + // + // Define the off-chain Warp message + // + log.Info("Creating off-chain Warp message") + newProtocolAddress := common.HexToAddress("0x0123456789abcdef0123456789abcdef01234567") + networkID := network.GetNetworkID() // - // Send two Teleporter message on Subnet A, before the relayer is running + // Set up the nodes to accept the off-chain message // + // Create chain config file with off chain message for each chain + unsignedMessage, warpEnabledChainConfigC := teleporterTestUtils.InitOffChainMessageChainConfig( + networkID, + cChainInfo, + newProtocolAddress, + 2, + ) + _, warpEnabledChainConfigA := teleporterTestUtils.InitOffChainMessageChainConfig( + networkID, + subnetAInfo, + newProtocolAddress, + 2, + ) + _, warpEnabledChainConfigB := teleporterTestUtils.InitOffChainMessageChainConfig( + networkID, + subnetBInfo, + newProtocolAddress, + 2, + ) - log.Info("Sending two teleporter messages on subnet A") - // This message will be delivered by the relayer - receipt1, _, id1 := testUtils.SendBasicTeleporterMessage(ctx, subnetAInfo, subnetBInfo, fundedKey, fundedAddress) - msg1 := getWarpMessageFromLog(ctx, receipt1, subnetAInfo) + // Create chain config with off chain messages + chainConfigs := make(map[string]string) + teleporterTestUtils.SetChainConfig(chainConfigs, cChainInfo, warpEnabledChainConfigC) + teleporterTestUtils.SetChainConfig(chainConfigs, subnetBInfo, warpEnabledChainConfigB) + teleporterTestUtils.SetChainConfig(chainConfigs, subnetAInfo, warpEnabledChainConfigA) - // This message will not be delivered by the relayer - _, _, id2 := testUtils.SendBasicTeleporterMessage(ctx, subnetAInfo, subnetBInfo, fundedKey, fundedAddress) + // Restart nodes with new chain config + nodeNames := network.GetAllNodeNames() + log.Info("Restarting nodes with new chain config") + network.RestartNodes(ctx, nodeNames, runner_sdk.WithChainConfigs(chainConfigs)) + // Refresh the subnet info to get the new clients + cChainInfo = network.GetPrimaryNetworkInfo() // - // Set up relayer config to deliver one of the two previously sent messages + // Set up relayer config // relayerConfig := testUtils.CreateDefaultRelayerConfig( - []interfaces.SubnetTestInfo{subnetAInfo, subnetBInfo}, - []interfaces.SubnetTestInfo{subnetAInfo, subnetBInfo}, + []interfaces.SubnetTestInfo{cChainInfo}, + []interfaces.SubnetTestInfo{cChainInfo}, teleporterContractAddress, fundedAddress, relayerKey, ) - relayerConfig.ManualWarpMessages = []*config.ManualWarpMessage{ - { - UnsignedMessageBytes: hex.EncodeToString(msg1.Bytes()), - SourceBlockchainID: subnetAInfo.BlockchainID.String(), - DestinationBlockchainID: subnetBInfo.BlockchainID.String(), - SourceAddress: teleporterContractAddress.Hex(), - DestinationAddress: teleporterContractAddress.Hex(), - }, - } - relayerConfigPath := testUtils.WriteRelayerConfig(relayerConfig, testUtils.DefaultRelayerCfgFname) - // - // Run the Relayer. On startup, we should deliver the message provided in the config - // - - // Subscribe to the destination chain - newHeadsB := make(chan *types.Header, 10) - sub, err := subnetBInfo.WSClient.SubscribeNewHead(ctx, newHeadsB) - Expect(err).Should(BeNil()) - defer sub.Unsubscribe() - log.Info("Starting the relayer") relayerCleanup := testUtils.BuildAndRunRelayerExecutable(ctx, relayerConfigPath) defer relayerCleanup() - log.Info("Waiting for a new block confirmation on subnet B") - <-newHeadsB - delivered1, err := subnetBInfo.TeleporterMessenger.MessageReceived( - &bind.CallOpts{}, id1, - ) - Expect(err).Should(BeNil()) - Expect(delivered1).Should(BeTrue()) + // Sleep for some time to make sure relayer has started up and subscribed. + log.Info("Waiting for the relayer to start up") + time.Sleep(15 * time.Second) - log.Info("Waiting for 10s to ensure no new block confirmations on destination chain") - Consistently(newHeadsB, 10*time.Second, 500*time.Millisecond).ShouldNot(Receive()) + reqBody := api.ManualWarpMessageRequest{ + UnsignedMessageBytes: unsignedMessage.Bytes(), + SourceAddress: offchainregistry.OffChainRegistrySourceAddress.Hex(), + } - delivered2, err := subnetBInfo.TeleporterMessenger.MessageReceived( - &bind.CallOpts{}, id2, - ) - Expect(err).Should(BeNil()) - Expect(delivered2).Should(BeFalse()) -} + client := http.Client{ + Timeout: 30 * time.Second, + } -func getWarpMessageFromLog(ctx context.Context, receipt *types.Receipt, source interfaces.SubnetTestInfo) *avalancheWarp.UnsignedMessage { - log.Info("Fetching relevant warp logs from the newly produced block") - logs, err := source.RPCClient.FilterLogs(ctx, subnetEvmInterfaces.FilterQuery{ - BlockHash: &receipt.BlockHash, - Addresses: []common.Address{warp.Module.Address}, - }) - Expect(err).Should(BeNil()) - Expect(len(logs)).Should(Equal(1)) + requestURL := fmt.Sprintf("http://localhost:%d%s", relayerConfig.APIPort, api.RelayMessageAPIPath) - // Check for relevant warp log from subscription and ensure that it matches - // the log extracted from the last block. - txLog := logs[0] - log.Info("Parsing logData as unsigned warp message") - unsignedMsg, err := warp.UnpackSendWarpEventDataToMessage(txLog.Data) - Expect(err).Should(BeNil()) + // Send request to API + { + b, err := json.Marshal(reqBody) + Expect(err).Should(BeNil()) + bodyReader := bytes.NewReader(b) + + req, err := http.NewRequest(http.MethodPost, requestURL, bodyReader) + Expect(err).Should(BeNil()) + req.Header.Set("Content-Type", "application/json") - return unsignedMsg + res, err := client.Do(req) + Expect(err).Should(BeNil()) + Expect(res.Status).Should(Equal("200 OK")) + + // Wait for all nodes to see new transaction + time.Sleep(1 * time.Second) + + newVersion, err := cChainInfo.TeleporterRegistry.LatestVersion(&bind.CallOpts{}) + Expect(err).Should(BeNil()) + Expect(newVersion.Uint64()).Should(Equal(expectedNewVersion.Uint64())) + } } diff --git a/tests/relay_message_api.go b/tests/relay_message_api.go new file mode 100644 index 00000000..b737e4c7 --- /dev/null +++ b/tests/relay_message_api.go @@ -0,0 +1,171 @@ +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package tests + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "time" + + "github.com/ava-labs/avalanchego/ids" + avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" + "github.com/ava-labs/awm-relayer/api" + testUtils "github.com/ava-labs/awm-relayer/tests/utils" + "github.com/ava-labs/subnet-evm/core/types" + subnetEvmInterfaces "github.com/ava-labs/subnet-evm/interfaces" + "github.com/ava-labs/subnet-evm/precompile/contracts/warp" + "github.com/ava-labs/teleporter/tests/interfaces" + "github.com/ava-labs/teleporter/tests/utils" + teleporterTestUtils "github.com/ava-labs/teleporter/tests/utils" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/log" + . "github.com/onsi/gomega" +) + +func RelayMessageAPI(network interfaces.LocalNetwork) { + ctx := context.Background() + subnetAInfo := network.GetPrimaryNetworkInfo() + subnetBInfo, _ := utils.GetTwoSubnets(network) + fundedAddress, fundedKey := network.GetFundedAccountInfo() + teleporterContractAddress := network.GetTeleporterContractAddress() + err := testUtils.ClearRelayerStorage() + Expect(err).Should(BeNil()) + + log.Info("Funding relayer address on all subnets") + relayerKey, err := crypto.GenerateKey() + Expect(err).Should(BeNil()) + testUtils.FundRelayers(ctx, []interfaces.SubnetTestInfo{subnetAInfo, subnetBInfo}, fundedKey, relayerKey) + + log.Info("Sending teleporter message") + receipt, _, teleporterMessageID := testUtils.SendBasicTeleporterMessage( + ctx, + subnetAInfo, + subnetBInfo, + fundedKey, + fundedAddress, + ) + warpMessage := getWarpMessageFromLog(ctx, receipt, subnetAInfo) + + // Set up relayer config + relayerConfig := testUtils.CreateDefaultRelayerConfig( + []interfaces.SubnetTestInfo{subnetAInfo, subnetBInfo}, + []interfaces.SubnetTestInfo{subnetAInfo, subnetBInfo}, + teleporterContractAddress, + fundedAddress, + relayerKey, + ) + // Don't process missed blocks, so we can manually relay + relayerConfig.ProcessMissedBlocks = false + + relayerConfigPath := testUtils.WriteRelayerConfig(relayerConfig, testUtils.DefaultRelayerCfgFname) + + log.Info("Starting the relayer") + relayerCleanup := testUtils.BuildAndRunRelayerExecutable(ctx, relayerConfigPath) + defer relayerCleanup() + + // Sleep for some time to make sure relayer has started up and subscribed. + log.Info("Waiting for the relayer to start up") + time.Sleep(15 * time.Second) + + reqBody := api.RelayMessageRequest{ + BlockchainID: subnetAInfo.BlockchainID.String(), + MessageID: warpMessage.ID().String(), + BlockNum: receipt.BlockNumber.Uint64(), + } + + client := http.Client{ + Timeout: 30 * time.Second, + } + + requestURL := fmt.Sprintf("http://localhost:%d%s", relayerConfig.APIPort, api.RelayAPIPath) + + // Send request to API + { + b, err := json.Marshal(reqBody) + Expect(err).Should(BeNil()) + bodyReader := bytes.NewReader(b) + + req, err := http.NewRequest(http.MethodPost, requestURL, bodyReader) + Expect(err).Should(BeNil()) + req.Header.Set("Content-Type", "application/json") + + res, err := client.Do(req) + Expect(err).Should(BeNil()) + Expect(res.Status).Should(Equal("200 OK")) + + defer res.Body.Close() + body, err := io.ReadAll(res.Body) + Expect(err).Should(BeNil()) + + var response api.RelayMessageResponse + err = json.Unmarshal(body, &response) + Expect(err).Should(BeNil()) + + receipt, err := subnetBInfo.RPCClient.TransactionReceipt(ctx, common.HexToHash(response.TransactionHash)) + Expect(err).Should(BeNil()) + receiveEvent, err := teleporterTestUtils.GetEventFromLogs( + receipt.Logs, + subnetBInfo.TeleporterMessenger.ParseReceiveCrossChainMessage, + ) + Expect(err).Should(BeNil()) + Expect(ids.ID(receiveEvent.MessageID)).Should(Equal(teleporterMessageID)) + } + + // Send the same request to ensure the correct response. + { + b, err := json.Marshal(reqBody) + Expect(err).Should(BeNil()) + bodyReader := bytes.NewReader(b) + + req, err := http.NewRequest(http.MethodPost, requestURL, bodyReader) + Expect(err).Should(BeNil()) + req.Header.Set("Content-Type", "application/json") + + res, err := client.Do(req) + Expect(err).Should(BeNil()) + Expect(res.Status).Should(Equal("200 OK")) + + defer res.Body.Close() + body, err := io.ReadAll(res.Body) + Expect(err).Should(BeNil()) + + var response api.RelayMessageResponse + err = json.Unmarshal(body, &response) + Expect(err).Should(BeNil()) + Expect(response.TransactionHash).Should(Equal( + "0x0000000000000000000000000000000000000000000000000000000000000000", + )) + } + + // Cancel the command and stop the relayer + relayerCleanup() +} + +func getWarpMessageFromLog( + ctx context.Context, + receipt *types.Receipt, + source interfaces.SubnetTestInfo, +) *avalancheWarp.UnsignedMessage { + log.Info("Fetching relevant warp logs from the newly produced block") + logs, err := source.RPCClient.FilterLogs(ctx, subnetEvmInterfaces.FilterQuery{ + BlockHash: &receipt.BlockHash, + Addresses: []common.Address{warp.Module.Address}, + }) + Expect(err).Should(BeNil()) + Expect(len(logs)).Should(Equal(1)) + + // Check for relevant warp log from subscription and ensure that it matches + // the log extracted from the last block. + txLog := logs[0] + log.Info("Parsing logData as unsigned warp message") + unsignedMsg, err := warp.UnpackSendWarpEventDataToMessage(txLog.Data) + Expect(err).Should(BeNil()) + + return unsignedMsg +} diff --git a/tests/teleporter_registry.go b/tests/teleporter_registry.go deleted file mode 100644 index 13c4698e..00000000 --- a/tests/teleporter_registry.go +++ /dev/null @@ -1,122 +0,0 @@ -// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package tests - -import ( - "context" - "encoding/hex" - "math/big" - - runner_sdk "github.com/ava-labs/avalanche-network-runner/client" - "github.com/ava-labs/awm-relayer/config" - offchainregistry "github.com/ava-labs/awm-relayer/messages/off-chain-registry" - testUtils "github.com/ava-labs/awm-relayer/tests/utils" - "github.com/ava-labs/subnet-evm/accounts/abi/bind" - "github.com/ava-labs/subnet-evm/core/types" - "github.com/ava-labs/teleporter/tests/interfaces" - teleporterTestUtils "github.com/ava-labs/teleporter/tests/utils" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/log" - . "github.com/onsi/gomega" -) - -// Tests relayer support for off-chain Teleporter Registry updates -// - Configures the relayer to send an off-chain message to the Teleporter Registry -// - Verifies that the Teleporter Registry is updated -func TeleporterRegistry(network interfaces.LocalNetwork) { - cChainInfo := network.GetPrimaryNetworkInfo() - subnetAInfo, subnetBInfo := teleporterTestUtils.GetTwoSubnets(network) - fundedAddress, fundedKey := network.GetFundedAccountInfo() - teleporterContractAddress := network.GetTeleporterContractAddress() - err := testUtils.ClearRelayerStorage() - Expect(err).Should(BeNil()) - - // - // Get the current Teleporter Registry version - // - currentVersion, err := cChainInfo.TeleporterRegistry.LatestVersion(&bind.CallOpts{}) - Expect(err).Should(BeNil()) - expectedNewVersion := currentVersion.Add(currentVersion, big.NewInt(1)) - - // - // Fund the relayer address on all subnets - // - ctx := context.Background() - - log.Info("Funding relayer address on all subnets") - relayerKey, err := crypto.GenerateKey() - Expect(err).Should(BeNil()) - testUtils.FundRelayers(ctx, []interfaces.SubnetTestInfo{cChainInfo}, fundedKey, relayerKey) - - // - // Define the off-chain Warp message - // - log.Info("Creating off-chain Warp message") - newProtocolAddress := common.HexToAddress("0x0123456789abcdef0123456789abcdef01234567") - networkID := network.GetNetworkID() - - // - // Set up the nodes to accept the off-chain message - // - // Create chain config file with off chain message for each chain - unsignedMessage, warpEnabledChainConfigC := teleporterTestUtils.InitOffChainMessageChainConfig(networkID, cChainInfo, newProtocolAddress, 2) - _, warpEnabledChainConfigA := teleporterTestUtils.InitOffChainMessageChainConfig(networkID, subnetAInfo, newProtocolAddress, 2) - _, warpEnabledChainConfigB := teleporterTestUtils.InitOffChainMessageChainConfig(networkID, subnetBInfo, newProtocolAddress, 2) - - // Create chain config with off chain messages - chainConfigs := make(map[string]string) - teleporterTestUtils.SetChainConfig(chainConfigs, cChainInfo, warpEnabledChainConfigC) - teleporterTestUtils.SetChainConfig(chainConfigs, subnetBInfo, warpEnabledChainConfigB) - teleporterTestUtils.SetChainConfig(chainConfigs, subnetAInfo, warpEnabledChainConfigA) - - // Restart nodes with new chain config - nodeNames := network.GetAllNodeNames() - log.Info("Restarting nodes with new chain config") - network.RestartNodes(ctx, nodeNames, runner_sdk.WithChainConfigs(chainConfigs)) - // Refresh the subnet info to get the new clients - cChainInfo = network.GetPrimaryNetworkInfo() - - // - // Set up relayer config - // - relayerConfig := testUtils.CreateDefaultRelayerConfig( - []interfaces.SubnetTestInfo{cChainInfo}, - []interfaces.SubnetTestInfo{cChainInfo}, - teleporterContractAddress, - fundedAddress, - relayerKey, - ) - relayerConfig.ManualWarpMessages = []*config.ManualWarpMessage{ - { - UnsignedMessageBytes: hex.EncodeToString(unsignedMessage.Bytes()), - SourceBlockchainID: cChainInfo.BlockchainID.String(), - DestinationBlockchainID: cChainInfo.BlockchainID.String(), - SourceAddress: offchainregistry.OffChainRegistrySourceAddress.Hex(), - DestinationAddress: cChainInfo.TeleporterRegistryAddress.Hex(), - }, - } - relayerConfigPath := testUtils.WriteRelayerConfig(relayerConfig, testUtils.DefaultRelayerCfgFname) - // - // Run the Relayer. On startup, we should deliver the message provided in the config - // - - // Subscribe to the destination chain - newHeadsC := make(chan *types.Header, 10) - sub, err := cChainInfo.WSClient.SubscribeNewHead(ctx, newHeadsC) - Expect(err).Should(BeNil()) - defer sub.Unsubscribe() - - log.Info("Starting the relayer") - relayerCleanup := testUtils.BuildAndRunRelayerExecutable(ctx, relayerConfigPath) - defer relayerCleanup() - - log.Info("Waiting for a new block confirmation on the C-Chain") - <-newHeadsC - - log.Info("Verifying that the Teleporter Registry was updated") - newVersion, err := cChainInfo.TeleporterRegistry.LatestVersion(&bind.CallOpts{}) - Expect(err).Should(BeNil()) - Expect(newVersion.Cmp(expectedNewVersion)).Should(Equal(0)) -} diff --git a/tests/utils/utils.go b/tests/utils/utils.go index b66f4fd2..5f19b4b6 100644 --- a/tests/utils/utils.go +++ b/tests/utils/utils.go @@ -209,6 +209,7 @@ func CreateDefaultRelayerConfig( DestinationBlockchains: destinations, DeciderHost: "localhost", DeciderPort: &deciderPort, + APIPort: 8080, } } @@ -301,7 +302,10 @@ func SendBasicTeleporterMessage( input, fundedKey, ) - sendEvent, err := teleporterTestUtils.GetEventFromLogs(receipt.Logs, source.TeleporterMessenger.ParseSendCrossChainMessage) + sendEvent, err := teleporterTestUtils.GetEventFromLogs( + receipt.Logs, + source.TeleporterMessenger.ParseSendCrossChainMessage, + ) Expect(err).Should(BeNil()) return receipt, sendEvent.Message, teleporterMessageID @@ -365,7 +369,10 @@ func RelayBasicMessage( Expect(receipt.Status).Should(Equal(types.ReceiptStatusSuccessful)) // Check that the transaction emits ReceiveCrossChainMessage - receiveEvent, err := teleporterTestUtils.GetEventFromLogs(receipt.Logs, destination.TeleporterMessenger.ParseReceiveCrossChainMessage) + receiveEvent, err := teleporterTestUtils.GetEventFromLogs( + receipt.Logs, + destination.TeleporterMessenger.ParseReceiveCrossChainMessage, + ) Expect(err).Should(BeNil()) Expect(receiveEvent.SourceBlockchainID[:]).Should(Equal(source.BlockchainID[:])) Expect(receiveEvent.MessageID[:]).Should(Equal(teleporterMessageID[:])) @@ -384,7 +391,12 @@ func RelayBasicMessage( receivedTeleporterMessage, err := teleportermessenger.UnpackTeleporterMessage(addressedPayload.Payload) Expect(err).Should(BeNil()) - receivedMessageID, err := teleporterUtils.CalculateMessageID(teleporterContractAddress, source.BlockchainID, destination.BlockchainID, teleporterMessage.MessageNonce) + receivedMessageID, err := teleporterUtils.CalculateMessageID( + teleporterContractAddress, + source.BlockchainID, + destination.BlockchainID, + teleporterMessage.MessageNonce, + ) Expect(err).Should(BeNil()) Expect(receivedMessageID).Should(Equal(teleporterMessageID)) Expect(receivedTeleporterMessage.OriginSenderAddress).Should(Equal(teleporterMessage.OriginSenderAddress)) diff --git a/types/types.go b/types/types.go index 735115d2..b71a6805 100644 --- a/types/types.go +++ b/types/types.go @@ -16,12 +16,13 @@ import ( "github.com/ethereum/go-ethereum/common" ) -var WarpPrecompileLogFilter = warp.WarpABI.Events["SendWarpMessage"].ID -var ErrInvalidLog = errors.New("invalid warp message log") +var ( + WarpPrecompileLogFilter = warp.WarpABI.Events["SendWarpMessage"].ID + ErrInvalidLog = errors.New("invalid warp message log") +) // WarpBlockInfo describes the block height and logs needed to process Warp messages. -// WarpBlockInfo instances are populated by the subscriber, and forwared to the -// Listener to process +// WarpBlockInfo instances are populated by the subscriber, and forwarded to the Listener to process. type WarpBlockInfo struct { BlockNumber uint64 Messages []*WarpMessageInfo diff --git a/utils/client_utils.go b/utils/client_utils.go index 051a5705..410fa0b9 100644 --- a/utils/client_utils.go +++ b/utils/client_utils.go @@ -16,7 +16,11 @@ import ( var ErrInvalidEndpoint = errors.New("invalid rpc endpoint") // NewEthClientWithConfig returns an ethclient.Client with the internal RPC client configured with the provided options. -func NewEthClientWithConfig(ctx context.Context, baseURL string, httpHeaders, queryParams map[string]string) (ethclient.Client, error) { +func NewEthClientWithConfig( + ctx context.Context, + baseURL string, httpHeaders, + queryParams map[string]string, +) (ethclient.Client, error) { client, err := DialWithConfig(ctx, baseURL, httpHeaders, queryParams) if err != nil { return nil, err @@ -25,7 +29,12 @@ func NewEthClientWithConfig(ctx context.Context, baseURL string, httpHeaders, qu } // DialWithConfig dials the provided baseURL with the provided httpHeaders and queryParams -func DialWithConfig(ctx context.Context, baseURL string, httpHeaders, queryParams map[string]string) (*rpc.Client, error) { +func DialWithConfig( + ctx context.Context, + baseURL string, + httpHeaders map[string]string, + queryParams map[string]string, +) (*rpc.Client, error) { url, err := addQueryParams(baseURL, queryParams) if err != nil { return nil, err diff --git a/utils/utils.go b/utils/utils.go index 7648c0ef..36b5cd2f 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -12,6 +12,7 @@ import ( "strings" "time" + "github.com/ava-labs/avalanchego/ids" "github.com/ethereum/go-ethereum/common" ) @@ -33,8 +34,14 @@ const ( // AWM Utils // -// CheckStakeWeightExceedsThreshold returns true if the accumulated signature weight is at least [quorumNum]/[quorumDen] of [totalWeight]. -func CheckStakeWeightExceedsThreshold(accumulatedSignatureWeight *big.Int, totalWeight uint64, quorumNumerator uint64, quorumDenominator uint64) bool { +// CheckStakeWeightExceedsThreshold returns true if the accumulated signature weight is at +// least [quorumNum]/[quorumDen] of [totalWeight]. +func CheckStakeWeightExceedsThreshold( + accumulatedSignatureWeight *big.Int, + totalWeight uint64, + quorumNumerator uint64, + quorumDenominator uint64, +) bool { if accumulatedSignatureWeight == nil { return false } @@ -117,3 +124,16 @@ func StripFromString(input, substring string) string { return strippedString } + +// Converts a '0x'-prefixed hex string or cb58-encoded string to an ID. +// Input length validation is handled by the ids package. +func HexOrCB58ToID(s string) (ids.ID, error) { + if strings.HasPrefix(s, "0x") { + bytes, err := hex.DecodeString(SanitizeHexString(s)) + if err != nil { + return ids.ID{}, err + } + return ids.ToID(bytes) + } + return ids.FromString(s) +} diff --git a/utils/utils_test.go b/utils/utils_test.go index 2349915b..0ace0515 100644 --- a/utils/utils_test.go +++ b/utils/utils_test.go @@ -10,6 +10,44 @@ import ( "github.com/stretchr/testify/require" ) +func TestHexOrCB58ToID(t *testing.T) { + testCases := []struct { + name string + encoding string + expectedResult string + errorExpected bool + }{ + { + name: "hex conversion", + encoding: "0x7fc93d85c6d62c5b2ac0b519c87010ea5294012d1e407030d6acd0021cac10d5", + expectedResult: "yH8D7ThNJkxmtkuv2jgBa4P1Rn3Qpr4pPr7QYNfcdoS6k6HWp", + errorExpected: false, + }, + { + name: "cb58 conversion", + encoding: "yH8D7ThNJkxmtkuv2jgBa4P1Rn3Qpr4pPr7QYNfcdoS6k6HWp", + expectedResult: "yH8D7ThNJkxmtkuv2jgBa4P1Rn3Qpr4pPr7QYNfcdoS6k6HWp", + errorExpected: false, + }, + { + name: "non-prefixed hex", + encoding: "7fc93d85c6d62c5b2ac0b519c87010ea5294012d1e407030d6acd0021cac10d5", + errorExpected: true, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + actualResult, err := HexOrCB58ToID(testCase.encoding) + if testCase.errorExpected { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, testCase.expectedResult, actualResult.String()) + } + }) + } +} + func TestSanitizeHexString(t *testing.T) { testCases := []struct { name string @@ -87,7 +125,12 @@ func TestCheckStakeWeightExceedsThreshold(t *testing.T) { } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - actualResult := CheckStakeWeightExceedsThreshold(new(big.Int).SetUint64(testCase.accumulatedSignatureWeight), testCase.totalWeight, testCase.quorumNumerator, testCase.quorumDenominator) + actualResult := CheckStakeWeightExceedsThreshold( + new(big.Int).SetUint64(testCase.accumulatedSignatureWeight), + testCase.totalWeight, + testCase.quorumNumerator, + testCase.quorumDenominator, + ) require.Equal(t, testCase.expectedResult, actualResult) }) } diff --git a/vms/destination_client.go b/vms/destination_client.go index e417a458..e1fbda02 100644 --- a/vms/destination_client.go +++ b/vms/destination_client.go @@ -17,10 +17,11 @@ import ( "go.uber.org/zap" ) -// DestinationClient is the interface for the destination chain client. Methods that interact with the destination chain -// should generally be implemented in a thread safe way, as they will be called concurrently by the application relayers. +// DestinationClient is the interface for the destination chain client. Methods that interact with +// the destination chain should generally be implemented in a thread safe way, as they will be called +// concurrently by the application relayers. type DestinationClient interface { - // SendTx contructs the transaction from warp primitives, and sends to the configured destination chain endpoint. + // SendTx constructs the transaction from warp primitives, and sends to the configured destination chain endpoint. // Returns the hash of the sent transaction. // TODO: Make generic for any VM. SendTx(signedMessage *warp.Message, toAddress string, gasLimit uint64, callData []byte) (common.Hash, error) @@ -45,14 +46,17 @@ func NewDestinationClient(logger logging.Logger, subnetInfo *config.DestinationB } // CreateDestinationClients creates destination clients for all subnets configured as destinations -func CreateDestinationClients(logger logging.Logger, relayerConfig config.Config) (map[ids.ID]DestinationClient, error) { +func CreateDestinationClients( + logger logging.Logger, + relayerConfig config.Config, +) (map[ids.ID]DestinationClient, error) { destinationClients := make(map[ids.ID]DestinationClient) for _, subnetInfo := range relayerConfig.DestinationBlockchains { blockchainID, err := ids.FromString(subnetInfo.BlockchainID) if err != nil { logger.Error( "Failed to decode base-58 encoded source chain ID", - zap.String("blockchainID", blockchainID.String()), + zap.String("blockchainID", subnetInfo.BlockchainID), zap.Error(err), ) return nil, err diff --git a/vms/evm/contract_message.go b/vms/evm/contract_message.go index 49ace169..5221d8a3 100644 --- a/vms/evm/contract_message.go +++ b/vms/evm/contract_message.go @@ -24,8 +24,8 @@ func NewContractMessage(logger logging.Logger, subnetInfo config.SourceBlockchai } func (m *contractMessage) UnpackWarpMessage(unsignedMsgBytes []byte) (*avalancheWarp.UnsignedMessage, error) { - // This function may be called with raw UnsignedMessage bytes or with ABI encoded bytes as emitted by the Warp precompile - // The latter case is the steady state behavior, so check that first. The former only occurs on startup. + // This function may be called with raw UnsignedMessage bytes or with ABI encoded bytes as emitted by the Warp + // precompile. The latter case is the steady state behavior, so check that first. The former only occurs on startup. unsignedMsg, err := warp.UnpackSendWarpEventDataToMessage(unsignedMsgBytes) if err != nil { m.logger.Debug( diff --git a/vms/evm/contract_message_test.go b/vms/evm/contract_message_test.go index cff0ec97..150f0961 100644 --- a/vms/evm/contract_message_test.go +++ b/vms/evm/contract_message_test.go @@ -61,24 +61,24 @@ func TestUnpack(t *testing.T) { }{ { name: "valid log data", - input: "0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000024c00000000053968786a235cbcfb6e57321b94378e95939b773a9626acf7a8cc440075c02c7268000002220000000000010000001452718d4ea91a6dd9a68940dbd687efa32315d11600000200000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000008db97c7cece249c2b98bdc0226cc4c2a57bf52fcb1d32d469938520383696931c26b9753662db74ad33c012f41e337aa828f1b74000000000000000000000000abcedf1234abcedf1234abcedf1234abcedf12340000000000000000000000000000000000000000000000000000000000002710000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a100ff48a37cab9f87c8b5da933da46ea1a5fb80000000000000000000000000000000000000000000000000000000000000002acafebabecafebabecafebabecafebabecafebabecafebabecafebabecafebabecafebabecafebabecafe000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + input: "0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000024c00000000053968786a235cbcfb6e57321b94378e95939b773a9626acf7a8cc440075c02c7268000002220000000000010000001452718d4ea91a6dd9a68940dbd687efa32315d11600000200000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000008db97c7cece249c2b98bdc0226cc4c2a57bf52fcb1d32d469938520383696931c26b9753662db74ad33c012f41e337aa828f1b74000000000000000000000000abcedf1234abcedf1234abcedf1234abcedf12340000000000000000000000000000000000000000000000000000000000002710000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a100ff48a37cab9f87c8b5da933da46ea1a5fb80000000000000000000000000000000000000000000000000000000000000002acafebabecafebabecafebabecafebabecafebabecafebabecafebabecafebabecafebabecafebabecafe000000000000000000000000000000000000000000000000000000000000000000000000000000000000", //nolint:lll networkID: constants.DefaultNetworkID, expectError: false, }, { name: "invalid log data", - input: "1000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000024c00000000053968786a235cbcfb6e57321b94378e95939b773a9626acf7a8cc440075c02c7268000002220000000000010000001452718d4ea91a6dd9a68940dbd687efa32315d11600000200000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000008db97c7cece249c2b98bdc0226cc4c2a57bf52fcb1d32d469938520383696931c26b9753662db74ad33c012f41e337aa828f1b74000000000000000000000000abcedf1234abcedf1234abcedf1234abcedf12340000000000000000000000000000000000000000000000000000000000002710000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a100ff48a37cab9f87c8b5da933da46ea1a5fb80000000000000000000000000000000000000000000000000000000000000002acafebabecafebabecafebabecafebabecafebabecafebabecafebabecafebabecafebabecafebabecafe000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + input: "1000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000024c00000000053968786a235cbcfb6e57321b94378e95939b773a9626acf7a8cc440075c02c7268000002220000000000010000001452718d4ea91a6dd9a68940dbd687efa32315d11600000200000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000008db97c7cece249c2b98bdc0226cc4c2a57bf52fcb1d32d469938520383696931c26b9753662db74ad33c012f41e337aa828f1b74000000000000000000000000abcedf1234abcedf1234abcedf1234abcedf12340000000000000000000000000000000000000000000000000000000000002710000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a100ff48a37cab9f87c8b5da933da46ea1a5fb80000000000000000000000000000000000000000000000000000000000000002acafebabecafebabecafebabecafebabecafebabecafebabecafebabecafebabecafebabecafebabecafe000000000000000000000000000000000000000000000000000000000000000000000000000000000000", //nolint:lll expectError: true, }, { name: "valid standalone message", - input: "00000000053968786a235cbcfb6e57321b94378e95939b773a9626acf7a8cc440075c02c7268000002220000000000010000001452718d4ea91a6dd9a68940dbd687efa32315d11600000200000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000008db97c7cece249c2b98bdc0226cc4c2a57bf52fcb1d32d469938520383696931c26b9753662db74ad33c012f41e337aa828f1b74000000000000000000000000abcedf1234abcedf1234abcedf1234abcedf12340000000000000000000000000000000000000000000000000000000000002710000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a100ff48a37cab9f87c8b5da933da46ea1a5fb80000000000000000000000000000000000000000000000000000000000000002acafebabecafebabecafebabecafebabecafebabecafebabecafebabecafebabecafebabecafebabecafe00000000000000000000000000000000000000000000", + input: "00000000053968786a235cbcfb6e57321b94378e95939b773a9626acf7a8cc440075c02c7268000002220000000000010000001452718d4ea91a6dd9a68940dbd687efa32315d11600000200000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000008db97c7cece249c2b98bdc0226cc4c2a57bf52fcb1d32d469938520383696931c26b9753662db74ad33c012f41e337aa828f1b74000000000000000000000000abcedf1234abcedf1234abcedf1234abcedf12340000000000000000000000000000000000000000000000000000000000002710000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a100ff48a37cab9f87c8b5da933da46ea1a5fb80000000000000000000000000000000000000000000000000000000000000002acafebabecafebabecafebabecafebabecafebabecafebabecafebabecafebabecafebabecafebabecafe00000000000000000000000000000000000000000000", //nolint:lll networkID: constants.DefaultNetworkID, expectError: false, }, { name: "invalid standalone message", - input: "ab000000053968786a235cbcfb6e57321b94378e95939b773a9626acf7a8cc440075c02c7268000002220000000000010000001452718d4ea91a6dd9a68940dbd687efa32315d11600000200000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000008db97c7cece249c2b98bdc0226cc4c2a57bf52fcb1d32d469938520383696931c26b9753662db74ad33c012f41e337aa828f1b74000000000000000000000000abcedf1234abcedf1234abcedf1234abcedf12340000000000000000000000000000000000000000000000000000000000002710000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a100ff48a37cab9f87c8b5da933da46ea1a5fb80000000000000000000000000000000000000000000000000000000000000002acafebabecafebabecafebabecafebabecafebabecafebabecafebabecafebabecafebabecafebabecafe00000000000000000000000000000000000000000000", + input: "ab000000053968786a235cbcfb6e57321b94378e95939b773a9626acf7a8cc440075c02c7268000002220000000000010000001452718d4ea91a6dd9a68940dbd687efa32315d11600000200000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000008db97c7cece249c2b98bdc0226cc4c2a57bf52fcb1d32d469938520383696931c26b9753662db74ad33c012f41e337aa828f1b74000000000000000000000000abcedf1234abcedf1234abcedf1234abcedf12340000000000000000000000000000000000000000000000000000000000002710000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a100ff48a37cab9f87c8b5da933da46ea1a5fb80000000000000000000000000000000000000000000000000000000000000002acafebabecafebabecafebabecafebabecafebabecafebabecafebabecafebabecafebabecafebabecafe00000000000000000000000000000000000000000000", //nolint:lll expectError: true, }, } diff --git a/vms/evm/destination_client_test.go b/vms/evm/destination_client_test.go index 2658545f..b23576b9 100644 --- a/vms/evm/destination_client_test.go +++ b/vms/evm/destination_client_test.go @@ -91,9 +91,17 @@ func TestSendTx(t *testing.T) { toAddress := "0x27aE10273D17Cd7e80de8580A51f476960626e5f" gomock.InOrder( - mockClient.EXPECT().EstimateBaseFee(gomock.Any()).Return(new(big.Int), test.estimateBaseFeeErr).Times(test.estimateBaseFeeTimes), - mockClient.EXPECT().SuggestGasTipCap(gomock.Any()).Return(new(big.Int), test.suggestGasTipCapErr).Times(test.suggestGasTipCapTimes), - mockClient.EXPECT().SendTransaction(gomock.Any(), gomock.Any()).Return(test.sendTransactionErr).Times(test.sendTransactionTimes), + mockClient.EXPECT().EstimateBaseFee(gomock.Any()).Return( + new(big.Int), + test.estimateBaseFeeErr, + ).Times(test.estimateBaseFeeTimes), + mockClient.EXPECT().SuggestGasTipCap(gomock.Any()).Return( + new(big.Int), + test.suggestGasTipCapErr, + ).Times(test.suggestGasTipCapTimes), + mockClient.EXPECT().SendTransaction(gomock.Any(), gomock.Any()).Return( + test.sendTransactionErr, + ).Times(test.sendTransactionTimes), ) _, err := destinationClient.SendTx(warpMsg, toAddress, 0, []byte{}) diff --git a/vms/evm/signer/kms_signer_test.go b/vms/evm/signer/kms_signer_test.go index 22ccfbf7..c67f1385 100644 --- a/vms/evm/signer/kms_signer_test.go +++ b/vms/evm/signer/kms_signer_test.go @@ -22,8 +22,8 @@ func TestRecoverEIP155Signature(t *testing.T) { txHash: "69963cf4839e149fea0e7b0969dd6834ea4b22fa7f9209a46683982320e5edfd", rValue: "00a94bfca53b42454acc43e7328ce7a8f244a629026095c36c0ee82607377cbee4", sValue: "5964b0dd9bf23723570b6a073cb945fd1ba853ebf5579c8d11bb4fdb305cd079", - pubKey: "04a3b664aa1f37bf6c46a3f2cdb209091e16070208b30244838e2cb9fe1465c4911ba2ab0c54bfc39972740ef7b24904f8740b0a69aca2bbfce0f2829429b9a5c5", - expectedSignature: "a94bfca53b42454acc43e7328ce7a8f244a629026095c36c0ee82607377cbee45964b0dd9bf23723570b6a073cb945fd1ba853ebf5579c8d11bb4fdb305cd07901", + pubKey: "04a3b664aa1f37bf6c46a3f2cdb209091e16070208b30244838e2cb9fe1465c4911ba2ab0c54bfc39972740ef7b24904f8740b0a69aca2bbfce0f2829429b9a5c5", //nolint:lll + expectedSignature: "a94bfca53b42454acc43e7328ce7a8f244a629026095c36c0ee82607377cbee45964b0dd9bf23723570b6a073cb945fd1ba853ebf5579c8d11bb4fdb305cd07901", //nolint:lll expectedError: false, }, { @@ -31,8 +31,8 @@ func TestRecoverEIP155Signature(t *testing.T) { txHash: "69963cf4839e149fea0e7b0969dd6834ea4b22fa7f9209a46683982320e5edfd", rValue: "10a94bfca53b42454acc43e7328ce7a8f244a629026095c36c0ee82607377cbee4", sValue: "5964b0dd9bf23723570b6a073cb945fd1ba853ebf5579c8d11bb4fdb305cd079", - pubKey: "04a3b664aa1f37bf6c46a3f2cdb209091e16070208b30244838e2cb9fe1465c4911ba2ab0c54bfc39972740ef7b24904f8740b0a69aca2bbfce0f2829429b9a5c5", - expectedSignature: "a94bfca53b42454acc43e7328ce7a8f244a629026095c36c0ee82607377cbee45964b0dd9bf23723570b6a073cb945fd1ba853ebf5579c8d11bb4fdb305cd07901", + pubKey: "04a3b664aa1f37bf6c46a3f2cdb209091e16070208b30244838e2cb9fe1465c4911ba2ab0c54bfc39972740ef7b24904f8740b0a69aca2bbfce0f2829429b9a5c5", //nolint:lll + expectedSignature: "a94bfca53b42454acc43e7328ce7a8f244a629026095c36c0ee82607377cbee45964b0dd9bf23723570b6a073cb945fd1ba853ebf5579c8d11bb4fdb305cd07901", //nolint:lll expectedError: true, }, { @@ -40,17 +40,18 @@ func TestRecoverEIP155Signature(t *testing.T) { txHash: "69963cf4839e149fea0e7b0969dd6834ea4b22fa7f9209a46683982320e5edfd", rValue: "00a94bfca53b42454acc43e7328ce7a8f244a629026095c36c0ee82607377cbee4", sValue: "1964b0dd9bf23723570b6a073cb945fd1ba853ebf5579c8d11bb4fdb305cd079", - pubKey: "04a3b664aa1f37bf6c46a3f2cdb209091e16070208b30244838e2cb9fe1465c4911ba2ab0c54bfc39972740ef7b24904f8740b0a69aca2bbfce0f2829429b9a5c5", - expectedSignature: "a94bfca53b42454acc43e7328ce7a8f244a629026095c36c0ee82607377cbee45964b0dd9bf23723570b6a073cb945fd1ba853ebf5579c8d11bb4fdb305cd07901", + pubKey: "04a3b664aa1f37bf6c46a3f2cdb209091e16070208b30244838e2cb9fe1465c4911ba2ab0c54bfc39972740ef7b24904f8740b0a69aca2bbfce0f2829429b9a5c5", //nolint:lll + expectedSignature: "a94bfca53b42454acc43e7328ce7a8f244a629026095c36c0ee82607377cbee45964b0dd9bf23723570b6a073cb945fd1ba853ebf5579c8d11bb4fdb305cd07901", //nolint:lll expectedError: true, }, { - name: "s-value too high", - txHash: "e29dc6b15950a5433e239155a2208156ba8fd81eeb81ade45329d4b2fb2e8421", - rValue: "00d993636ed097bf04b7c982e0394d572ead3c8f23ecc8baba0bbdfaa91aba4376", - sValue: "00d1ac23b517ae2569522626096fa1922681c87612cceac971e855d42103fea7c6", // This is > secp256k1n/2 - pubKey: "04a3b664aa1f37bf6c46a3f2cdb209091e16070208b30244838e2cb9fe1465c4911ba2ab0c54bfc39972740ef7b24904f8740b0a69aca2bbfce0f2829429b9a5c5", - expectedSignature: "d993636ed097bf04b7c982e0394d572ead3c8f23ecc8baba0bbdfaa91aba43762e53dc4ae851da96add9d9f6905e6dd838e666d3e25dd6c9d77c8a6bcc37997b00", + name: "s-value too high", + txHash: "e29dc6b15950a5433e239155a2208156ba8fd81eeb81ade45329d4b2fb2e8421", + rValue: "00d993636ed097bf04b7c982e0394d572ead3c8f23ecc8baba0bbdfaa91aba4376", + // This is > secp256k1n/2 + sValue: "00d1ac23b517ae2569522626096fa1922681c87612cceac971e855d42103fea7c6", + pubKey: "04a3b664aa1f37bf6c46a3f2cdb209091e16070208b30244838e2cb9fe1465c4911ba2ab0c54bfc39972740ef7b24904f8740b0a69aca2bbfce0f2829429b9a5c5", //nolint:lll + expectedSignature: "d993636ed097bf04b7c982e0394d572ead3c8f23ecc8baba0bbdfaa91aba43762e53dc4ae851da96add9d9f6905e6dd838e666d3e25dd6c9d77c8a6bcc37997b00", //nolint:lll expectedError: false, }, } diff --git a/vms/evm/subscriber.go b/vms/evm/subscriber.go index 95878ebd..8027baeb 100644 --- a/vms/evm/subscriber.go +++ b/vms/evm/subscriber.go @@ -76,6 +76,7 @@ func (s *subscriber) ProcessFromHeight(height *big.Int, done chan bool) { bigLatestBlockHeight := big.NewInt(0).SetUint64(latestBlockHeight) + //nolint:lll for fromBlock := big.NewInt(0).Set(height); fromBlock.Cmp(bigLatestBlockHeight) <= 0; fromBlock.Add(fromBlock, big.NewInt(MaxBlocksPerRequest)) { toBlock := big.NewInt(0).Add(fromBlock, big.NewInt(MaxBlocksPerRequest-1))