From 2060ab680cbd1a2501a87ac3ec01d6cd9da79f37 Mon Sep 17 00:00:00 2001 From: Michael Kaplan Date: Fri, 25 Aug 2023 11:24:35 -0400 Subject: [PATCH] Initial Commit Co-authored-by: Michael Kaplan Co-authored-by: Cam Schultz Co-authored-by: Matthew Lam Co-authored-by: Linguan Yang Co-authored-by: Anatoliy Dutchak Co-authored-by: Geoff Stuart --- .github/CODEOWNERS | 3 + .github/dependabot.yml | 22 + .github/workflows/codeql.yml | 59 ++ .github/workflows/release.yaml | 36 + .github/workflows/snyk.yml | 33 + .github/workflows/test.yml | 36 + .gitignore | 4 + CONTRIBUTING.md | 49 ++ Dockerfile | 17 + LICENSE | 66 ++ README.md | 39 + SECURITY.md | 17 + config/config.go | 328 +++++++++ config/config_test.go | 377 ++++++++++ config/flags.go | 12 + config/keys.go | 16 + config/types.go | 58 ++ config/viper.go | 42 ++ go.mod | 112 +++ go.sum | 972 +++++++++++++++++++++++++ main/main.go | 290 ++++++++ messages/message_manager.go | 48 ++ messages/teleporter/abi.go | 42 ++ messages/teleporter/config.go | 21 + messages/teleporter/message.go | 62 ++ messages/teleporter/message_manager.go | 180 +++++ messages/teleporter/message_test.go | 83 +++ messages/teleporter/utils.go | 49 ++ peers/app_request_network.go | 239 ++++++ peers/external_handler.go | 175 +++++ relayer/canonical_validator_client.go | 43 ++ relayer/message_relayer.go | 542 ++++++++++++++ relayer/message_relayer_metrics.go | 47 ++ relayer/relayer.go | 202 +++++ relayer/relayer_test.go | 24 + resources/relayer-diagram.png | Bin 0 -> 184867 bytes scripts/build.sh | 52 ++ scripts/build_local_image.sh | 22 + scripts/constants.sh | 24 + scripts/versions.sh | 9 + utils/utils.go | 101 +++ utils/utils_test.go | 88 +++ vms/contract_message.go | 25 + vms/destination_client.go | 70 ++ vms/evm/contract_message.go | 57 ++ vms/evm/contract_message_test.go | 75 ++ vms/evm/destination_client.go | 224 ++++++ vms/evm/subscriber.go | 165 +++++ vms/subscriber.go | 36 + vms/vmtypes/message_info.go | 20 + vms/vmtypes/types.go | 18 + 51 files changed, 5331 insertions(+) create mode 100644 .github/CODEOWNERS create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/codeql.yml create mode 100644 .github/workflows/release.yaml create mode 100644 .github/workflows/snyk.yml create mode 100644 .github/workflows/test.yml create mode 100644 .gitignore create mode 100644 CONTRIBUTING.md create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 README.md create mode 100644 SECURITY.md create mode 100644 config/config.go create mode 100644 config/config_test.go create mode 100644 config/flags.go create mode 100644 config/keys.go create mode 100644 config/types.go create mode 100644 config/viper.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main/main.go create mode 100644 messages/message_manager.go create mode 100644 messages/teleporter/abi.go create mode 100644 messages/teleporter/config.go create mode 100644 messages/teleporter/message.go create mode 100644 messages/teleporter/message_manager.go create mode 100644 messages/teleporter/message_test.go create mode 100644 messages/teleporter/utils.go create mode 100644 peers/app_request_network.go create mode 100644 peers/external_handler.go create mode 100644 relayer/canonical_validator_client.go create mode 100644 relayer/message_relayer.go create mode 100644 relayer/message_relayer_metrics.go create mode 100644 relayer/relayer.go create mode 100644 relayer/relayer_test.go create mode 100644 resources/relayer-diagram.png create mode 100755 scripts/build.sh create mode 100755 scripts/build_local_image.sh create mode 100755 scripts/constants.sh create mode 100755 scripts/versions.sh create mode 100644 utils/utils.go create mode 100644 utils/utils_test.go create mode 100644 vms/contract_message.go create mode 100644 vms/destination_client.go create mode 100644 vms/evm/contract_message.go create mode 100644 vms/evm/contract_message_test.go create mode 100644 vms/evm/destination_client.go create mode 100644 vms/evm/subscriber.go create mode 100644 vms/subscriber.go create mode 100644 vms/vmtypes/message_info.go create mode 100644 vms/vmtypes/types.go diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..fb42fa6f --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +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 \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..56b8094c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,22 @@ +# Copyright (C) 2023, Ava Labs, Inc. All rights reserved. +# See the file LICENSE for licensing terms. + +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "gomod" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "daily" + - package-ecosystem: "gitsubmodule" + directory: "/" + schedule: + interval: "daily" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..768e58be --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,59 @@ +# Copyright (C) 2023, Ava Labs, Inc. All rights reserved. +# See the file LICENSE for licensing terms. + +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +name: "CodeQL" + +on: + push: + branches: + - main + + pull_request: + branches: + - '*' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'go' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + queries: security-extended,security-and-quality + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 00000000..47b7ce2e --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,36 @@ +# Copyright (C) 2023, Ava Labs, Inc. All rights reserved. +# See the file LICENSE for licensing terms. + +name: Prod - release Docker image + +on: + release: + types: [published] +jobs: + push_to_registry: + name: Push Docker image to Docker Hub + runs-on: ubuntu-latest + steps: + - name: Check out the repo + uses: actions/checkout@v3 + + # Needed for multi-platform builds + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + # Needed for multi-platform builds + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Log in to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build and Push to Docker Hub + uses: docker/build-push-action@v4 + with: + platforms: linux/amd64,linux/arm64 + push: true + tags: avaplatform/awm-relayer:${{ github.event.release.tag_name }}, avaplatform/awm-relayer:latest diff --git a/.github/workflows/snyk.yml b/.github/workflows/snyk.yml new file mode 100644 index 00000000..83bb705e --- /dev/null +++ b/.github/workflows/snyk.yml @@ -0,0 +1,33 @@ +# Copyright (C) 2023, Ava Labs, Inc. All rights reserved. +# See the file LICENSE for licensing terms. + +name: Snyk Golang +on: + push: + branches-ignore: + - "dependabot/**" + pull_request: + branches: + - "dependabot/**" + +jobs: + golang-snyk: + runs-on: ubuntu-20.04 + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + path: awm-relayer + + - name: Run Snyk + uses: snyk/actions/golang@master + continue-on-error: true # To make sure that SARIF upload gets called + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + with: + args: --sarif-file-output=snyk.sarif --all-projects --detection-depth=4 + + - name: Upload result to GitHub Code Scanning + uses: github/codeql-action/upload-sarif@v2 + with: + sarif_file: snyk.sarif diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..20a117a8 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,36 @@ +# Copyright (C) 2023, Ava Labs, Inc. All rights reserved. +# See the file LICENSE for licensing terms. + +name: Run Relayer Unit Tests + +on: + push: + branches: + - '*' + +jobs: + build-test-relayer: + runs-on: ubuntu-20.04 + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + path: awm-relayer + + - name: Setup Go + uses: actions/setup-go@v4 + with: + go-version: '1.20.7' + + - name: Build Relayer + run: | + cd awm-relayer + go mod tidy + ./scripts/build.sh + + - name: Run Relayer Unit Tests + run: | + cd awm-relayer + go test ./... + diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..e33a2c17 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +build/ +__debug_bin + +.vscode* \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..34126291 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,49 @@ +# How to Contribute to AWM Relayer + +## Setup + +To start developing on AWM Relayer, you'll need Golang >= v1.18.1. + +## Issues + +### Security + +- Do not open up a GitHub issue if it relates to a security vulnerability in AWM Relayer, and instead refer to our [security policy](./SECURITY.md). + +### Making an Issue + +- Check that the issue you're filing doesn't already exist by searching under [issues](https://github.com/ava-labs/awm-relayer/issues). +- If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/ava-labs/awm-relayer/issues/new/choose). Be sure to include a *title and clear description* with as much relevant information as possible. + +## Features + +- If you want to start a discussion about the development of a new feature or the modfiication of an existing one, start a thread under GitHub [discussions](https://github.com/ava-labs/awm-relayer/discussions/categories/ideas). +- Post a thread about your idea and why it should be added to AWM Relayer. +- Don't start working on a pull request until you've received positive feedback from the maintainers. + +## Pull Request Guidelines + +- Open a new GitHub pull request containing your changes. +- Ensure the PR description clearly describes the problem and solution, and how the change was tested. Include the relevant issue number if applicable. +- If your PR isn't ready to be reviewed just yet, you can open it as a draft to collect early feedback on your changes. +- Once the PR is ready for review, mark it as ready-for-review and request review from one of the maintainers. + +### Testing + +#### Local + +- Run the unit tests + +```sh +go test ./... +``` + +### Continuous Integration (CI) + +- Pull requests will generally not be approved or merged unless they pass CI. + +## Other + +### Do you have questions about the source code? + +- Ask any question about AWM Relayer under GitHub [discussions](https://github.com/ava-labs/teleporter/discussions/categories/q-a). diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..8094b197 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +### Build Stage ### +FROM golang:1.20.7-bullseye as build + +WORKDIR /go/src +# Copy the code into the container +COPY . . +RUN go mod tidy +# Build awm-relayer +RUN bash ./scripts/build.sh + +### RUN Stage ### +FROM golang:1.20.7 +COPY --from=build /go/src/build/awm-relayer /usr/bin/awm-relayer +EXPOSE 8080 +USER 1001 +CMD ["start"] +ENTRYPOINT ["/usr/bin/awm-relayer"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..fbc13b0d --- /dev/null +++ b/LICENSE @@ -0,0 +1,66 @@ +Copyright (C) 2023, Ava Labs, Inc. All rights reserved. + +Ecosystem License +Version: 1.1 + +Subject to the terms herein, Ava Labs, Inc. (**“Ava Labs”**) hereby grants you +a limited, royalty-free, worldwide, non-sublicensable, non-transferable, +non-exclusive license to use, copy, modify, create derivative works based on, +and redistribute the Software, in source code, binary, or any other form, +including any modifications or derivative works of the Software (collectively, +**“Licensed Software”**), in each case subject to this Ecosystem License +(**“License”**). + +This License applies to all copies, modifications, derivative works, and any +other form or usage of the Licensed Software. You will include and display +this License, without modification, with all uses of the Licensed Software, +regardless of form. + +You will use the Licensed Software solely (i) in connection with the Avalanche +Public Blockchain platform, having a NetworkID of 1 (Mainnet) or 5 (Fuji), and +associated blockchains, comprised exclusively of the Avalanche X-Chain, +C-Chain, P-Chain and any subnets linked to the P-Chain (“Avalanche Authorized +Platform”) or (ii) for non-production, testing or research purposes within the +Avalanche ecosystem, in each case, without any commercial application +(“Non-Commercial Use”); provided that this License does not permit use of the +Licensed Software in connection with (a) any forks of the Avalanche Authorized +Platform or (b) in any manner not operationally connected to the Avalanche +Authorized Platform other than, for the avoidance of doubt, the limited +exception for Non-Commercial Use. Ava Labs may publicly announce changes or +additions to the Avalanche Authorized Platform, which may expand or modify +usage of the Licensed Software. Upon such announcement, the Avalanche +Authorized Platform will be deemed to be the then-current iteration of such +platform. + +You hereby acknowledge and agree to the terms set forth at +www.avalabs.org/important-notice. + +If you use the Licensed Software in violation of this License, this License +will automatically terminate and Ava Labs reserves all rights to seek any +remedy for such violation. + +Except for uses explicitly permitted in this License, Ava Labs retains all +rights in the Licensed Software, including without limitation the ability to +modify it. + +Except as required or explicitly permitted by this License, you will not use +any Ava Labs names, logos, or trademarks without Ava Labs’ prior written +consent. + +You may use this License for software other than the “Licensed Software” +specified above, as long as the only change to this License is the definition +of the term “Licensed Software.” + +The Licensed Software may reference third party components. You acknowledge +and agree that these third party components may be governed by a separate +license or terms and that you will comply with them. + +**TO THE MAXIMUM EXTENT PERMITTED BY LAW, THE LICENSED SOFTWARE IS PROVIDED +ON AN “AS IS” BASIS, AND AVA LABS EXPRESSLY DISCLAIMS AND EXCLUDES ALL +REPRESENTATIONS, WARRANTIES AND OTHER TERMS AND CONDITIONS, WHETHER EXPRESS OR +IMPLIED, INCLUDING WITHOUT LIMITATION BY OPERATION OF LAW OR BY CUSTOM, +STATUTE OR OTHERWISE, AND INCLUDING, BUT NOT LIMITED TO, ANY IMPLIED WARRANTY, +TERM, OR CONDITION OF NON-INFRINGEMENT, MERCHANTABILITY, TITLE, OR FITNESS FOR +PARTICULAR PURPOSE. YOU USE THE LICENSED SOFTWARE AT YOUR OWN RISK. AVA LABS +EXPRESSLY DISCLAIMS ALL LIABILITY (INCLUDING FOR ALL DIRECT, CONSEQUENTIAL OR +OTHER DAMAGES OR LOSSES) RELATED TO ANY USE OF THE LICENSED SOFTWARE.** \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..5e2536b4 --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +# awm-relayer + +Standalone relayer for cross-chain Avalanche Warp Message delivery. + +## Usage +--- +### Building +Build the relayer by running the included build script: +``` +./scripts/build.sh +``` + +Build a Docker image by running the included build script: +``` +./scripts/build-local-image.sh +``` +### Running +The relayer binary accepts a path to a JSON configuration file as the sole argument. Command line configuration arguments are not currently supported. +``` +./build/awm-relayer --config-file path-to-config +``` + +## Architecture +--- +**Note:** The relayer in its current state supports Teleporter messages between `subnet-evm` instances. A handful of abstractions have been added to make the relayer extensible to other Warp message formats and VM types, but this work is ongoing. +### Components +The relayer consists of the following components: +- At the global level: + - *P2P App Network*: issues signature `AppRequests` + - *P-Chain client*: gets the validators for a subnet +- Per Source subnet + - *Subscriber*: listens for logs pertaining to cross-chain message transactions +- Per Destination subnet + - *Destination RPC client*: broadcasts transactions to the destination + +### Data flow +
+ +
diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..d29add8e --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,17 @@ +# Security Policy + +Avalanche takes the security of the platform and of its users very seriously. We and our community recognize the critical role of external security researchers and developers and welcome responsible disclosures. Valid reports will be eligible for a reward (terms and conditions apply). + +## Reporting a Vulnerability + +**Please do not file a public ticket** mentioning the vulnerability. To disclose a vulnerability submit it through our [Bug Bounty Program](https://hackenproof.com/avalanche). + +Vulnerabilities must be disclosed to us privately with reasonable time to respond, and avoid compromise of other users and accounts, or loss of funds that are not your own. We do not reward spam or social engineering vulnerabilities. + +Do not test for or validate any security issues in the live Avalanche networks (Mainnet and Fuji testnet), confirm all exploits in a local private testnet. + +Please refer to the [Bug Bounty Page](https://hackenproof.com/avalanche) for the most up-to-date program rules and scope. + +## Supported Versions + +Please use the most recently released version to perform testing and to validate security issues. \ No newline at end of file diff --git a/config/config.go b/config/config.go new file mode 100644 index 00000000..41a07cea --- /dev/null +++ b/config/config.go @@ -0,0 +1,328 @@ +// Copyright (C) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package config + +import ( + "crypto/ecdsa" + "encoding/hex" + "errors" + "fmt" + "math/big" + "net/url" + "os" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/set" + "github.com/ava-labs/awm-relayer/utils" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/spf13/viper" +) + +const ( + relayerPrivateKeyBytes = 32 + accountPrivateKeyEnvVarName = "ACCOUNT_PRIVATE_KEY" + cChainIdentifierString = "C" +) + +var ( + ErrInvalidPrivateKey = errors.New("failed to set private key string") +) + +type MessageProtocolConfig struct { + MessageFormat string `mapstructure:"message-format" json:"message-format"` + Settings map[string]interface{} `mapstructure:"settings" json:"settings"` +} +type SourceSubnet struct { + SubnetID string `mapstructure:"subnet-id" json:"subnet-id"` + ChainID string `mapstructure:"chain-id" json:"chain-id"` + VM string `mapstructure:"vm" json:"vm"` + APINodeHost string `mapstructure:"api-node-host" json:"api-node-host"` + APINodePort uint32 `mapstructure:"api-node-port" json:"api-node-port"` + EncryptConnection bool `mapstructure:"encrypt-connection" json:"encrypt-connection"` + WSEndpoint string `mapstructure:"ws-endpoint" json:"ws-endpoint"` + MessageContracts map[string]MessageProtocolConfig `mapstructure:"message-contracts" json:"message-contracts"` +} + +type DestinationSubnet struct { + SubnetID string `mapstructure:"subnet-id" json:"subnet-id"` + ChainID string `mapstructure:"chain-id" json:"chain-id"` + VM string `mapstructure:"vm" json:"vm"` + APINodeHost string `mapstructure:"api-node-host" json:"api-node-host"` + APINodePort uint32 `mapstructure:"api-node-port" json:"api-node-port"` + EncryptConnection bool `mapstructure:"encrypt-connection" json:"encrypt-connection"` + RPCEndpoint string `mapstructure:"rpc-endpoint" json:"rpc-endpoint"` + AccountPrivateKey string `mapstructure:"account-private-key" json:"account-private-key"` +} + +type Config struct { + LogLevel string `mapstructure:"log-level" json:"log-level"` + NetworkID uint32 `mapstructure:"network-id" json:"network-id"` + PChainAPIURL string `mapstructure:"p-chain-api-url" json:"p-chain-api-url"` + EncryptConnection bool `mapstructure:"encrypt-connection" json:"encrypt-connection"` + SourceSubnets []SourceSubnet `mapstructure:"source-subnets" json:"source-subnets"` + DestinationSubnets []DestinationSubnet `mapstructure:"destination-subnets" json:"destination-subnets"` +} + +func SetDefaultConfigValues(v *viper.Viper) { + v.SetDefault(LogLevelKey, "info") + v.SetDefault(NetworkIDKey, constants.MainnetID) + v.SetDefault(PChainAPIURLKey, "https://api.avax.network") + v.SetDefault(EncryptConnectionKey, true) +} + +// BuildConfig constructs the relayer config using Viper. +// The following precedence order is used. Each item takes precedence over the item below it: +// 1. Flags +// 2. Environment variables +// a. Global account-private-key +// b. Chain-specific account-private-key +// 3. Config file +// +// Returns the Config option and a bool indicating whether any options provided from one source +// were explicitly overridden by a higher precedence source. +// TODO: Improve the optionOverwritten return value to reflect the key that was modified. +func BuildConfig(v *viper.Viper) (Config, bool, error) { + // Set default values + SetDefaultConfigValues(v) + + // Build the config from Viper + var ( + cfg Config + err error + optionOverwritten bool = false + ) + + cfg.LogLevel = v.GetString(LogLevelKey) + cfg.NetworkID = v.GetUint32(NetworkIDKey) + cfg.PChainAPIURL = v.GetString(PChainAPIURLKey) + cfg.EncryptConnection = v.GetBool(EncryptConnectionKey) + if err := v.UnmarshalKey(DestinationSubnetsKey, &cfg.DestinationSubnets); err != nil { + return Config{}, false, fmt.Errorf("failed to unmarshal destination subnets: %v", err) + } + if err := v.UnmarshalKey(SourceSubnetsKey, &cfg.SourceSubnets); err != nil { + return Config{}, false, fmt.Errorf("failed to unmarshal source subnets: %v", err) + } + + // Explicitly overwrite the configured account private key + // If account-private-key is set as a flag or environment variable, + // overwrite all destination subnet configurations to use that key + // In all cases, sanitize the key before setting it in the config + accountPrivateKey := v.GetString(AccountPrivateKeyKey) + if accountPrivateKey != "" { + optionOverwritten = true + for i := range cfg.DestinationSubnets { + cfg.DestinationSubnets[i].AccountPrivateKey = utils.SanitizeHashString(accountPrivateKey) + } + } else { + // 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 + for i, subnet := range cfg.DestinationSubnets { + subnetAccountPrivateKey := os.Getenv(fmt.Sprintf("%s_%s", accountPrivateKeyEnvVarName, subnet.ChainID)) + if subnetAccountPrivateKey != "" { + optionOverwritten = true + cfg.DestinationSubnets[i].AccountPrivateKey = utils.SanitizeHashString(subnetAccountPrivateKey) + } else { + cfg.DestinationSubnets[i].AccountPrivateKey = utils.SanitizeHashString(cfg.DestinationSubnets[i].AccountPrivateKey) + } + } + } + + if err = cfg.Validate(); err != nil { + return Config{}, false, fmt.Errorf("failed to validate configuration: %v", err) + } + + var protocol string + if cfg.EncryptConnection { + protocol = "https" + } else { + protocol = "http" + } + + pChainapiUrl, err := utils.ConvertProtocol(cfg.PChainAPIURL, protocol) + if err != nil { + return Config{}, false, err + } + cfg.PChainAPIURL = pChainapiUrl + + return cfg, optionOverwritten, nil +} + +func (c *Config) Validate() error { + if len(c.SourceSubnets) == 0 { + return fmt.Errorf("relayer not configured to relay from any subnets. A list of source subnets must be provided in the configuration file") + } + if len(c.DestinationSubnets) == 0 { + return fmt.Errorf("relayer not configured to relay to any subnets. A list of destination subnets must be provided in the configuration file") + } + if _, err := url.ParseRequestURI(c.PChainAPIURL); err != nil { + return err + } + sourceChains := set.NewSet[string](len(c.SourceSubnets)) + for _, s := range c.SourceSubnets { + if err := s.Validate(); err != nil { + return err + } + if sourceChains.Contains(s.ChainID) { + return fmt.Errorf("configured source subnets must have unique chain IDs") + } + sourceChains.Add(s.ChainID) + } + + destinationChains := set.NewSet[string](len(c.DestinationSubnets)) + for _, s := range c.DestinationSubnets { + if err := s.Validate(); err != nil { + return err + } + if destinationChains.Contains(s.ChainID) { + return fmt.Errorf("configured destination subnets must have unique chain IDs") + } + destinationChains.Add(s.ChainID) + } + + return nil +} + +// GetSourceIDs returns the Subnet and Chain IDs of all subnets configured as a source +func (cfg *Config) GetSourceIDs() ([]ids.ID, []ids.ID, error) { + var sourceSubnetIDs []ids.ID + var sourceChainIDs []ids.ID + for _, s := range cfg.SourceSubnets { + subnetID, err := ids.FromString(s.SubnetID) + if err != nil { + return nil, nil, fmt.Errorf("invalid subnetID in configuration. error: %v", err) + } + sourceSubnetIDs = append(sourceSubnetIDs, subnetID) + + chainID, err := ids.FromString(s.ChainID) + if err != nil { + return nil, nil, fmt.Errorf("invalid subnetID in configuration. error: %v", err) + } + sourceChainIDs = append(sourceChainIDs, chainID) + } + return sourceSubnetIDs, sourceChainIDs, nil +} + +func (s *SourceSubnet) Validate() 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.ChainID); err != nil { + return fmt.Errorf("invalid chainID in source subnet configuration. Provided ID: %s", s.ChainID) + } + if _, err := url.ParseRequestURI(s.GetNodeWSEndpoint()); err != nil { + return fmt.Errorf("invalid relayer subscribe URL in source subnet configuration: %v", err) + } + + // Validate the VM specific settings + switch ParseVM(s.VM) { + case EVM: + for messageContractAddress := range s.MessageContracts { + if !common.IsHexAddress(messageContractAddress) { + return fmt.Errorf("invalid message contract address in EVM source subnet: %s", messageContractAddress) + } + } + default: + return fmt.Errorf("unsupported VM type for source subnet: %v", s.VM) + } + + // Validate message settings correspond to a supported message protocol + for _, messageConfig := range s.MessageContracts { + protocol := ParseMessageProtocol(messageConfig.MessageFormat) + if protocol == UNKNOWN_MESSAGE_PROTOCOL { + return fmt.Errorf("unsupported message protocol for source subnet: %s", messageConfig.MessageFormat) + } + } + + return nil +} + +func (s *DestinationSubnet) Validate() 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.ChainID); err != nil { + return fmt.Errorf("invalid chainID in source subnet configuration. Provided ID: %s", s.ChainID) + } + if _, err := url.ParseRequestURI(s.GetNodeRPCEndpoint()); err != nil { + return fmt.Errorf("invalid relayer broadcast URL: %v", err) + } + + if len(s.AccountPrivateKey) != relayerPrivateKeyBytes*2 { + return fmt.Errorf("invalid account private key hex string") + } + + if _, err := hex.DecodeString(s.AccountPrivateKey); err != nil { + return fmt.Errorf("invalid account private key hex string: %v", err) + } + + // Validate the VM specific settings + vm := ParseVM(s.VM) + if vm == UNKNOWN_VM { + return fmt.Errorf("unsupported VM type for source subnet: %s", s.VM) + } + + return nil +} + +func constructURL(protocol string, host string, port uint32, encrypt bool) string { + if encrypt { + protocol = protocol + "s" + } + portStr := "" + if port != 0 { + portStr = fmt.Sprintf(":%d", port) + } + return fmt.Sprintf("%s://%s%s", protocol, host, portStr) +} + +// Constructs an RPC endpoint for the subnet. +// If the RPCEndpoint field is set in the configuration, returns that directly. +// Otherwise, constructs the endpoint from the APINodeHost, APINodePort, and EncryptConnection fields, +// following the /ext/bc/{chainID}/rpc format. +func (s *DestinationSubnet) GetNodeRPCEndpoint() string { + if s.RPCEndpoint != "" { + return s.RPCEndpoint + } + baseUrl := constructURL("http", s.APINodeHost, s.APINodePort, s.EncryptConnection) + chainID := s.ChainID + subnetID, _ := ids.FromString(s.SubnetID) // already validated in Validate() + if subnetID == constants.PrimaryNetworkID { + chainID = cChainIdentifierString + } + return fmt.Sprintf("%s/ext/bc/%s/rpc", baseUrl, chainID) +} + +// Constructs a WS endpoint for the subnet. +// If the WSEndpoint field is set in the configuration, returns that directly. +// Otherwise, constructs the endpoint from the APINodeHost, APINodePort, and EncryptConnection fields, +// following the /ext/bc/{chainID}/ws format. +func (s *SourceSubnet) GetNodeWSEndpoint() string { + if s.WSEndpoint != "" { + return s.WSEndpoint + } + baseUrl := constructURL("ws", s.APINodeHost, s.APINodePort, s.EncryptConnection) + chainID := s.ChainID + subnetID, _ := ids.FromString(s.SubnetID) // already validated in Validate() + if subnetID == constants.PrimaryNetworkID { + chainID = cChainIdentifierString + } + return fmt.Sprintf("%s/ext/bc/%s/ws", baseUrl, chainID) +} + +// Get the private key and derive the wallet address from a relayer's configured private key for a given destination subnet. +func (s *DestinationSubnet) GetRelayerAccountInfo() (*ecdsa.PrivateKey, common.Address, error) { + var ok bool + pk := new(ecdsa.PrivateKey) + pk.D, ok = new(big.Int).SetString(s.AccountPrivateKey, 16) + if !ok { + return nil, common.Address{}, ErrInvalidPrivateKey + } + pk.PublicKey.Curve = crypto.S256() + pk.PublicKey.X, pk.PublicKey.Y = pk.PublicKey.Curve.ScalarBaseMult(pk.D.Bytes()) + pkBytes := pk.PublicKey.X.Bytes() + pkBytes = append(pkBytes, pk.PublicKey.Y.Bytes()...) + return pk, common.BytesToAddress(crypto.Keccak256(pkBytes)), nil +} diff --git a/config/config_test.go b/config/config_test.go new file mode 100644 index 00000000..5c99b1d5 --- /dev/null +++ b/config/config_test.go @@ -0,0 +1,377 @@ +// Copyright (C) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package config + +import ( + "crypto/ecdsa" + "encoding/json" + "fmt" + "math/big" + "os" + "path/filepath" + "testing" + + "github.com/ava-labs/awm-relayer/utils" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + testSubnetID string = "2TGBXcnwx5PqiXWiqxAKUaNSqDguXNh1mxnp82jui68hxJSZAx" + testChainID string = "S4mMqUXe7vHsGiRAma6bv3CKnyaLssyAxmQ2KvFpX1KEvfFCD" + testChainID2 string = "291etJW5EpagFY94v1JraFy8vLFYXcCnWKJ6Yz9vrjfPjCF4QL" + testAddress string = "0xd81545385803bCD83bd59f58Ba2d2c0562387F83" + primarySubnetID string = "11111111111111111111111111111111LpoYY" + testValidConfig = Config{ + LogLevel: "info", + NetworkID: 1337, + PChainAPIURL: "http://test.avax.network", + EncryptConnection: false, + SourceSubnets: []SourceSubnet{ + { + APINodeHost: "http://test.avax.network", + APINodePort: 0, + ChainID: testChainID, + SubnetID: testSubnetID, + VM: "evm", + EncryptConnection: false, + MessageContracts: map[string]MessageProtocolConfig{ + testAddress: { + MessageFormat: "teleporter", + }, + }, + }, + }, + DestinationSubnets: []DestinationSubnet{ + { + APINodeHost: "http://test.avax.network", + APINodePort: 0, + ChainID: testChainID, + SubnetID: testSubnetID, + VM: "evm", + EncryptConnection: false, + AccountPrivateKey: "0x56289e99c94b6912bfc12adc093c9b51124f0dc54ac7a766b2bc5ccf558d8027", + }, + }, + } + testPk1 string = "0xabc89e99c94b6912bfc12adc093c9b51124f0dc54ac7a766b2bc5ccf558d8abc" + testPk2 string = "0x12389e99c94b6912bfc12adc093c9b51124f0dc54ac7a766b2bc5ccf558d8123" +) + +func TestGetDestinationRPCEndpoint(t *testing.T) { + testCases := []struct { + s DestinationSubnet + expectedResult string + }{ + { + s: DestinationSubnet{ + EncryptConnection: false, + APINodeHost: "127.0.0.1", + APINodePort: 9650, + ChainID: testChainID, + SubnetID: testSubnetID, + }, + expectedResult: fmt.Sprintf("http://127.0.0.1:9650/ext/bc/%s/rpc", testChainID), + }, + { + s: DestinationSubnet{ + EncryptConnection: true, + APINodeHost: "127.0.0.1", + APINodePort: 9650, + ChainID: testChainID, + SubnetID: testSubnetID, + }, + expectedResult: fmt.Sprintf("https://127.0.0.1:9650/ext/bc/%s/rpc", testChainID), + }, + { + s: DestinationSubnet{ + EncryptConnection: false, + APINodeHost: "api.avax.network", + APINodePort: 0, + ChainID: testChainID, + SubnetID: testSubnetID, + }, + expectedResult: fmt.Sprintf("http://api.avax.network/ext/bc/%s/rpc", testChainID), + }, + { + s: DestinationSubnet{ + EncryptConnection: false, + APINodeHost: "127.0.0.1", + APINodePort: 9650, + ChainID: testChainID, + SubnetID: primarySubnetID, + }, + expectedResult: "http://127.0.0.1:9650/ext/bc/C/rpc", + }, + { + s: DestinationSubnet{ + EncryptConnection: false, + APINodeHost: "127.0.0.1", + APINodePort: 9650, + ChainID: testChainID, + SubnetID: testSubnetID, + RPCEndpoint: "https://subnets.avax.network/mysubnet/rpc", // overrides all other settings used to construct the endpoint + }, + expectedResult: "https://subnets.avax.network/mysubnet/rpc", + }, + } + + for i, testCase := range testCases { + res := testCase.s.GetNodeRPCEndpoint() + assert.Equal(t, testCase.expectedResult, res, fmt.Sprintf("test case %d failed", i)) + } +} + +func TestGetSourceSubnetWSEndpoint(t *testing.T) { + testCases := []struct { + s SourceSubnet + expectedResult string + }{ + { + s: SourceSubnet{ + EncryptConnection: false, + APINodeHost: "127.0.0.1", + APINodePort: 9650, + ChainID: testChainID, + SubnetID: testSubnetID, + }, + expectedResult: fmt.Sprintf("ws://127.0.0.1:9650/ext/bc/%s/ws", testChainID), + }, + { + s: SourceSubnet{ + EncryptConnection: true, + APINodeHost: "127.0.0.1", + APINodePort: 9650, + ChainID: testChainID, + SubnetID: testSubnetID, + }, + expectedResult: fmt.Sprintf("wss://127.0.0.1:9650/ext/bc/%s/ws", testChainID), + }, + { + s: SourceSubnet{ + EncryptConnection: false, + APINodeHost: "api.avax.network", + APINodePort: 0, + ChainID: testChainID, + SubnetID: testSubnetID, + }, + expectedResult: fmt.Sprintf("ws://api.avax.network/ext/bc/%s/ws", testChainID), + }, + { + s: SourceSubnet{ + EncryptConnection: false, + APINodeHost: "127.0.0.1", + APINodePort: 9650, + ChainID: testChainID, + SubnetID: primarySubnetID, + }, + expectedResult: "ws://127.0.0.1:9650/ext/bc/C/ws", + }, + { + s: SourceSubnet{ + EncryptConnection: false, + APINodeHost: "127.0.0.1", + APINodePort: 9650, + ChainID: testChainID, + SubnetID: testSubnetID, + WSEndpoint: "wss://subnets.avax.network/mysubnet/ws", // overrides all other settings used to construct the endpoint + }, + expectedResult: "wss://subnets.avax.network/mysubnet/ws", + }, + } + + for i, testCase := range testCases { + res := testCase.s.GetNodeWSEndpoint() + assert.Equal(t, testCase.expectedResult, res, fmt.Sprintf("test case %d failed", i)) + } +} + +func TestGetRelayerAccountInfo(t *testing.T) { + type retStruct struct { + pk *ecdsa.PrivateKey + addr common.Address + err error + } + + testCases := []struct { + s DestinationSubnet + expectedResult retStruct + }{ + // valid + { + s: DestinationSubnet{ + AccountPrivateKey: "56289e99c94b6912bfc12adc093c9b51124f0dc54ac7a766b2bc5ccf558d8027", + }, + expectedResult: retStruct{ + pk: &ecdsa.PrivateKey{ + D: big.NewInt(-5567472993773453273), + }, + addr: common.HexToAddress("0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC"), + err: nil, + }, + }, + // invalid, with 0x prefix. Should be sanitized before being set in DestinationSubnet + { + s: DestinationSubnet{ + AccountPrivateKey: "0x56289e99c94b6912bfc12adc093c9b51124f0dc54ac7a766b2bc5ccf558d8027", + }, + expectedResult: retStruct{ + pk: &ecdsa.PrivateKey{ + D: big.NewInt(-5567472993773453273), + }, + addr: common.HexToAddress("0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC"), + err: ErrInvalidPrivateKey, + }, + }, + // invalid + { + s: DestinationSubnet{ + AccountPrivateKey: "invalid56289e99c94b6912bfc12adc093c9b51124f0dc54ac7a766b2bc5ccf558d8027", + }, + expectedResult: retStruct{ + pk: &ecdsa.PrivateKey{ + D: big.NewInt(-5567472993773453273), + }, + addr: common.HexToAddress("0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC"), + err: ErrInvalidPrivateKey, + }, + }, + } + + for i, testCase := range testCases { + pk, addr, err := testCase.s.GetRelayerAccountInfo() + assert.Equal(t, testCase.expectedResult.err, err, fmt.Sprintf("test case %d had unexpected error", i)) + if err == nil { + assert.Equal(t, testCase.expectedResult.pk.D.Int64(), pk.D.Int64(), fmt.Sprintf("test case %d had mismatched pk", i)) + assert.Equal(t, testCase.expectedResult.addr, addr, fmt.Sprintf("test case %d had mismatched address", i)) + } + } +} + +// GetRelayerAccountPrivateKey tests. Individual cases must be run in their own functions +// because they modify the environment variables. +type getRelayerAccountPrivateKeyTestCase struct { + baseConfig Config + configModifier func(Config) Config + flags []string + envSetter func() + expectedOverwritten bool + resultVerifier func(Config) bool +} + +// Sets up the config file temporary environment and runs the test case. +func runGetRelayerAccountPrivateKeyTest(t *testing.T, testCase getRelayerAccountPrivateKeyTestCase) { + require := require.New(t) + root := t.TempDir() + + cfg := testCase.configModifier(testCase.baseConfig) + cfgBytes, err := json.Marshal(cfg) + require.NoError(err) + + configFile := setupConfigJSON(t, root, string(cfgBytes)) + + flags := append([]string{"--config-file", configFile}, testCase.flags...) + testCase.envSetter() + + fs := BuildFlagSet() + v, err := BuildViper(fs, flags) + require.NoError(err) + parsedCfg, optionOverwritten, err := BuildConfig(v) + require.NoError(err) + assert.Equal(t, optionOverwritten, testCase.expectedOverwritten) + if !testCase.resultVerifier(parsedCfg) { + t.Errorf("unexpected config.") + } +} + +func TestGetRelayerAccountPrivateKey_set_pk_in_config(t *testing.T) { + testCase := getRelayerAccountPrivateKeyTestCase{ + baseConfig: testValidConfig, + configModifier: func(c Config) Config { return c }, + envSetter: func() {}, + expectedOverwritten: false, + resultVerifier: func(c Config) bool { + // All destination subnets should have the default private key + for i, subnet := range c.DestinationSubnets { + if subnet.AccountPrivateKey != utils.SanitizeHashString(testValidConfig.DestinationSubnets[i].AccountPrivateKey) { + fmt.Printf("expected: %s, got: %s\n", utils.SanitizeHashString(testValidConfig.DestinationSubnets[i].AccountPrivateKey), subnet.AccountPrivateKey) + return false + } + } + return true + }, + } + runGetRelayerAccountPrivateKeyTest(t, testCase) +} + +func TestGetRelayerAccountPrivateKey_set_pk_with_subnet_env(t *testing.T) { + testCase := getRelayerAccountPrivateKeyTestCase{ + baseConfig: testValidConfig, + configModifier: func(c Config) Config { + // Add a second destination subnet. This PK should NOT be overwritten + newSubnet := c.DestinationSubnets[0] + newSubnet.ChainID = testChainID2 + newSubnet.AccountPrivateKey = testPk1 + c.DestinationSubnets = append(c.DestinationSubnets, newSubnet) + return c + }, + envSetter: func() { + // Overwrite the PK for the first subnet using an env var + varName := fmt.Sprintf("%s_%s", accountPrivateKeyEnvVarName, testValidConfig.DestinationSubnets[0].ChainID) + t.Setenv(varName, testPk2) + }, + expectedOverwritten: true, + resultVerifier: func(c Config) bool { + // All destination subnets should have testPk1 + if c.DestinationSubnets[0].AccountPrivateKey != utils.SanitizeHashString(testPk2) { + fmt.Printf("expected: %s, got: %s\n", utils.SanitizeHashString(testPk2), c.DestinationSubnets[0].AccountPrivateKey) + return false + } + if c.DestinationSubnets[1].AccountPrivateKey != utils.SanitizeHashString(testPk1) { + fmt.Printf("expected: %s, got: %s\n", utils.SanitizeHashString(testPk1), c.DestinationSubnets[1].AccountPrivateKey) + return false + } + return true + }, + } + runGetRelayerAccountPrivateKeyTest(t, testCase) +} +func TestGetRelayerAccountPrivateKey_set_pk_with_global_env(t *testing.T) { + testCase := getRelayerAccountPrivateKeyTestCase{ + baseConfig: testValidConfig, + configModifier: func(c Config) Config { + // Add a second destination subnet. This PK SHOULD be overwritten + newSubnet := c.DestinationSubnets[0] + newSubnet.ChainID = testChainID2 + newSubnet.AccountPrivateKey = testPk1 + c.DestinationSubnets = append(c.DestinationSubnets, newSubnet) + return c + }, + envSetter: func() { + // Overwrite the PK for the first subnet using an env var + varName := fmt.Sprintf("%s", accountPrivateKeyEnvVarName) + t.Setenv(varName, testPk2) + }, + expectedOverwritten: true, + resultVerifier: func(c Config) bool { + // All destination subnets should have testPk2 + for _, subnet := range c.DestinationSubnets { + if subnet.AccountPrivateKey != utils.SanitizeHashString(testPk2) { + fmt.Printf("expected: %s, got: %s\n", utils.SanitizeHashString(testPk2), subnet.AccountPrivateKey) + return false + } + } + return true + }, + } + runGetRelayerAccountPrivateKeyTest(t, testCase) +} + +// setups config json file and writes content +func setupConfigJSON(t *testing.T, rootPath string, value string) string { + configFilePath := filepath.Join(rootPath, "config.json") + require.NoError(t, os.WriteFile(configFilePath, []byte(value), 0o600)) + return configFilePath +} diff --git a/config/flags.go b/config/flags.go new file mode 100644 index 00000000..d1dcdc4e --- /dev/null +++ b/config/flags.go @@ -0,0 +1,12 @@ +// Copyright (C) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package config + +import "github.com/spf13/pflag" + +func BuildFlagSet() *pflag.FlagSet { + fs := pflag.NewFlagSet("awm-relayer", pflag.ContinueOnError) + fs.String(ConfigFileKey, "", "Specifies the relayer config file") + return fs +} diff --git a/config/keys.go b/config/keys.go new file mode 100644 index 00000000..a9beb607 --- /dev/null +++ b/config/keys.go @@ -0,0 +1,16 @@ +// Copyright (C) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package config + +// Top-level configuration keys +const ( + ConfigFileKey = "config-file" + LogLevelKey = "log-level" + NetworkIDKey = "network-id" + PChainAPIURLKey = "p-chain-api-url" + SourceSubnetsKey = "source-subnets" + DestinationSubnetsKey = "destination-subnets" + EncryptConnectionKey = "encrypt-connection" + AccountPrivateKeyKey = "account-private-key" +) diff --git a/config/types.go b/config/types.go new file mode 100644 index 00000000..1edd98ad --- /dev/null +++ b/config/types.go @@ -0,0 +1,58 @@ +// Copyright (C) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package config + +// Supported VMs +type VM int + +const ( + UNKNOWN_VM VM = iota + EVM +) + +func (vm VM) String() string { + switch vm { + case EVM: + return "evm" + default: + return "unknown" + } +} + +// ParseVM returns the VM corresponding to [vm] +func ParseVM(vm string) VM { + switch vm { + case "evm": + return EVM + default: + return UNKNOWN_VM + } +} + +// Supported Message Protocols +type MessageProtocol int + +const ( + UNKNOWN_MESSAGE_PROTOCOL MessageProtocol = iota + TELEPORTER +) + +func (msg MessageProtocol) String() string { + switch msg { + case TELEPORTER: + return "teleporter" + default: + return "unknown" + } +} + +// ParseMessageProtocol returns the MessageProtocol corresponding to [msg] +func ParseMessageProtocol(msg string) MessageProtocol { + switch msg { + case "teleporter": + return TELEPORTER + default: + return UNKNOWN_MESSAGE_PROTOCOL + } +} diff --git a/config/viper.go b/config/viper.go new file mode 100644 index 00000000..9818aadc --- /dev/null +++ b/config/viper.go @@ -0,0 +1,42 @@ +// Copyright (C) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package config + +import ( + "fmt" + "strings" + + "github.com/spf13/pflag" + "github.com/spf13/viper" +) + +// Build the viper instance. The config file must be provided via the command line flag or environment variable. +// All config keys may be provided via config file or environment variable. +func BuildViper(fs *pflag.FlagSet, args []string) (*viper.Viper, error) { + if err := fs.Parse(args); err != nil { + return nil, err + } + + v := viper.New() + v.AutomaticEnv() + // Map flag names to env var names. Flags are capitalized, and hyphens are replaced with underscores. + v.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) + if err := v.BindPFlags(fs); err != nil { + return nil, err + } + + // Verify required flags are set + if !v.IsSet(ConfigFileKey) { + return nil, fmt.Errorf("config file not set") + } + + filename := v.GetString(ConfigFileKey) + v.SetConfigFile(filename) + v.SetConfigType("json") + if err := v.ReadInConfig(); err != nil { + return nil, err + } + + return v, nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 00000000..448b1f0d --- /dev/null +++ b/go.mod @@ -0,0 +1,112 @@ +module github.com/ava-labs/awm-relayer + +go 1.18 + +require ( + github.com/ava-labs/avalanchego v1.10.8 + github.com/ava-labs/subnet-evm v0.5.4-0.20230816140336-9f21235c17b3 + github.com/ethereum/go-ethereum v1.12.0 + github.com/golang/mock v1.6.0 + github.com/prometheus/client_golang v1.16.0 + github.com/spf13/pflag v1.0.5 + github.com/spf13/viper v1.16.0 + github.com/stretchr/testify v1.8.4 + go.uber.org/zap v1.25.0 +) + +require ( + github.com/cockroachdb/errors v1.9.1 // indirect + github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect + github.com/cockroachdb/pebble v0.0.0-20230209160836-829675f94811 // indirect + github.com/cockroachdb/redact v1.1.3 // indirect + github.com/deckarep/golang-set/v2 v2.1.0 // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/getsentry/sentry-go v0.18.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/klauspost/compress v1.15.15 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/rogpeppe/go-internal v1.9.0 // indirect + github.com/spf13/afero v1.9.5 // indirect + github.com/spf13/cast v1.5.1 // indirect + github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/subosito/gotenv v1.4.2 // indirect + golang.org/x/exp v0.0.0-20230206171751-46f607a40771 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect +) + +require ( + github.com/DataDog/zstd v1.5.2 // indirect + github.com/NYTimes/gziphandler v1.1.1 // indirect + github.com/VictoriaMetrics/fastcache v1.10.0 // indirect + github.com/alexliesenfeld/health v0.7.0 + github.com/beorn7/perks v1.0.1 // indirect + github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect + github.com/btcsuite/btcd/btcutil v1.1.3 // indirect + github.com/cenkalti/backoff/v4 v4.1.3 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect + github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 // indirect + github.com/go-logr/logr v1.2.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/go-stack/stack v1.8.1 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect + github.com/google/btree v1.1.2 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/gorilla/mux v1.8.0 // indirect + github.com/gorilla/rpc v1.2.0 // indirect + github.com/gorilla/websocket v1.4.2 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.2 // indirect + github.com/holiman/big v0.0.0-20221017200358-a027dc42d04e // indirect + github.com/holiman/bloomfilter/v2 v2.0.3 // indirect + github.com/holiman/uint256 v1.2.3 // indirect + github.com/mattn/go-runewidth v0.0.9 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/mr-tron/base58 v1.2.0 // indirect + github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/pires/go-proxyproto v0.6.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_model v0.3.0 // indirect + github.com/prometheus/common v0.42.0 // indirect + github.com/prometheus/procfs v0.10.1 // indirect + github.com/rs/cors v1.7.0 // indirect + github.com/shirou/gopsutil v3.21.11+incompatible // indirect + github.com/status-im/keycard-go v0.2.0 // indirect + github.com/supranational/blst v0.3.11 // indirect + github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a // indirect + github.com/tklauser/go-sysconf v0.3.5 // indirect + github.com/tklauser/numcpus v0.2.2 // indirect + github.com/tyler-smith/go-bip39 v1.1.0 // indirect + github.com/yusufpapurcu/wmi v1.2.2 // indirect + go.opentelemetry.io/otel v1.11.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.11.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.11.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.11.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.11.0 // indirect + go.opentelemetry.io/otel/sdk v1.11.0 // indirect + go.opentelemetry.io/otel/trace v1.11.0 // indirect + go.opentelemetry.io/proto/otlp v0.19.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/crypto v0.9.0 // indirect + golang.org/x/net v0.10.0 // indirect + golang.org/x/sync v0.3.0 // indirect + golang.org/x/sys v0.9.0 // indirect + golang.org/x/term v0.8.0 // indirect + golang.org/x/text v0.9.0 // indirect + golang.org/x/time v0.3.0 // indirect + gonum.org/v1/gonum v0.11.0 // indirect + google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect + google.golang.org/grpc v1.56.0-dev // indirect + google.golang.org/protobuf v1.30.0 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..0b4041af --- /dev/null +++ b/go.sum @@ -0,0 +1,972 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.2.0 h1:Rt8g24XnyGTyglgET/PRUNlrUeu9F5L+7FilkXfZgs0= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= +github.com/CloudyKit/jet/v3 v3.0.0/go.mod h1:HKQPgSJmdK8hdoAbKUUWajkHyHo4RaU5rMdUywE7VMo= +github.com/DataDog/zstd v1.5.2 h1:vUG4lAyuPCXO0TLbXvPv7EB7cNK1QV/luu55UHLrrn8= +github.com/DataDog/zstd v1.5.2/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= +github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= +github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= +github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0= +github.com/VictoriaMetrics/fastcache v1.10.0 h1:5hDJnLsKLpnUEToub7ETuRu8RCkb40woBZAUiKonXzY= +github.com/VictoriaMetrics/fastcache v1.10.0/go.mod h1:tjiYeEfYXCqacuvYw/7UoDIeJaNxq6132xHICNP77w8= +github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= +github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= +github.com/alexliesenfeld/health v0.7.0 h1:U3mSZ3ussRbGx+/rXBjNVxjLX5cKaNh8Ly9Hx3q+yfY= +github.com/alexliesenfeld/health v0.7.0/go.mod h1:6Nnjbu7vBYHoZqIuZeOnTpnW7OH14ulR+wIBE2QuJ8I= +github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8= +github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/ava-labs/avalanchego v1.10.8 h1:fUudA4J37y8wyNG3iiX0kpoZXunsWpCgvsGDgIsi0NY= +github.com/ava-labs/avalanchego v1.10.8/go.mod h1:2zuce+beHe25wQ5RlrEeDpa+SqY/sjEOjDky+Q1NxfU= +github.com/ava-labs/subnet-evm v0.5.4-0.20230816140336-9f21235c17b3 h1:7pxUN5Wb2ePYxKBeME8lAIxEk3J+fX+KYt0tGFNwpYM= +github.com/ava-labs/subnet-evm v0.5.4-0.20230816140336-9f21235c17b3/go.mod h1:QNFybGyhdnUC5k8j+Re5N//K+aXumfSd0B9gDOKC+x0= +github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= +github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= +github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= +github.com/btcsuite/btcd v0.23.0 h1:V2/ZgjfDFIygAX3ZapeigkVBoVUtOJKSwrhZdlpSvaA= +github.com/btcsuite/btcd v0.23.0/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY= +github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA= +github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= +github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U= +github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= +github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A= +github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE= +github.com/btcsuite/btcd/btcutil v1.1.3 h1:xfbtw8lwpp0G6NwSHb+UE67ryTFHJAiNuipusjXSohQ= +github.com/btcsuite/btcd/btcutil v1.1.3/go.mod h1:UR7dsSJzJUfMmFiiLlIrMq1lS9jh9EdCV7FStZSnpi0= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= +github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= +github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= +github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I= +github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= +github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= +github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4= +github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cockroachdb/datadriven v1.0.2 h1:H9MtNqVoVhvd9nCBwOyDjUEdZCREqbIdCJD93PBm/jA= +github.com/cockroachdb/datadriven v1.0.2/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= +github.com/cockroachdb/errors v1.9.1 h1:yFVvsI0VxmRShfawbt/laCIDy/mtTqqnvoNgiy5bEV8= +github.com/cockroachdb/errors v1.9.1/go.mod h1:2sxOtL2WIc096WSZqZ5h8fa17rdDq9HZOZLBCor4mBk= +github.com/cockroachdb/logtags v0.0.0-20211118104740-dabe8e521a4f/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= +github.com/cockroachdb/pebble v0.0.0-20230209160836-829675f94811 h1:ytcWPaNPhNoGMWEhDvS3zToKcDpRsLuRolQJBVGdozk= +github.com/cockroachdb/pebble v0.0.0-20230209160836-829675f94811/go.mod h1:Nb5lgvnQ2+oGlE/EyZy4+2/CxRh9KfvCXnag1vtpxVM= +github.com/cockroachdb/redact v1.1.3 h1:AKZds10rFSIj7qADf0g46UixK8NNLwWTNdCIGS5wfSQ= +github.com/cockroachdb/redact v1.1.3/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= +github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deckarep/golang-set/v2 v2.1.0 h1:g47V4Or+DUdzbs8FxCCmgb6VYd+ptPAngjM6dtGktsI= +github.com/deckarep/golang-set/v2 v2.1.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc= +github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= +github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= +github.com/ethereum/go-ethereum v1.12.0 h1:bdnhLPtqETd4m3mS8BGMNvBTf36bO5bx/hxE2zljOa0= +github.com/ethereum/go-ethereum v1.12.0/go.mod h1:/oo2X/dZLJjf2mJ6YT9wcWxa4nNJDBKDBU6sFIpx1Gs= +github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5 h1:FtmdgXiUlNeRsoNMFlKLDt+S+6hbjVMEW6RGQ7aUf7c= +github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= +github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 h1:f6D9Hr8xV8uYKlyuj8XIruxlh9WjVjdh1gIicAS7ays= +github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= +github.com/getsentry/sentry-go v0.12.0/go.mod h1:NSap0JBYWzHND8oMbyi0+XZhUalc1TBdRL1M71JZW2c= +github.com/getsentry/sentry-go v0.18.0 h1:MtBW5H9QgdcJabtZcuJG80BMOwaBpkRDZkxRkNC1sN0= +github.com/getsentry/sentry-go v0.18.0/go.mod h1:Kgon4Mby+FJ7ZWHFUAZgVaIa8sxHtnRJRLTXZr51aKQ= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= +github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= +github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= +github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= +github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/gogo/googleapis v0.0.0-20180223154316-0cd9801be74a/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= +github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4= +github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/gogo/status v1.1.0/go.mod h1:BFv9nrluPLmrS0EmGVvLaPNmRosr9KapBYd5/hpY1WM= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= +github.com/golang/glog v1.1.0 h1:/d3pCKDPWNnvIWe0vVUpNP32qc8U3PDVxySP/y360qE= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk= +github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= +github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/rpc v1.2.0 h1:WvvdC2lNeT1SP32zrIce5l0ECBfbAlmrmSBsuc57wfk= +github.com/gorilla/rpc v1.2.0/go.mod h1:V4h9r+4sF5HnzqbwIez0fKSpANP0zlYd3qR7p36jkTQ= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.2 h1:gDLXvp5S9izjldquuoAhDzccbskOL6tDC5jMSyx3zxE= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.2/go.mod h1:7pdNwVWBBHGiCxa9lAszqCJMbfTISJ7oMftp8+UGV08= +github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE= +github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d h1:dg1dEPuWpEqDnvIw251EVy4zlP8gWbsGj4BsUKCRpYs= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/holiman/big v0.0.0-20221017200358-a027dc42d04e h1:pIYdhNkDh+YENVNi3gto8n9hAmRxKxoar0iE6BLucjw= +github.com/holiman/big v0.0.0-20221017200358-a027dc42d04e/go.mod h1:j9cQbcqHQujT0oKJ38PylVfqohClLr3CvDC+Qcg+lhU= +github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= +github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= +github.com/holiman/uint256 v1.2.3 h1:K8UWO1HUJpRMXBxbmaY1Y8IAMZC/RsKB+ArEnnK4l5o= +github.com/holiman/uint256 v1.2.3/go.mod h1:SC8Ryt4n+UBbPbIBKaG9zbbDlp4jOru9xFZmPzLUTxw= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/hydrogen18/memlistener v0.0.0-20200120041712-dcc25e7acd91/go.mod h1:qEIFzExnS6016fRpRfxrExeVn2gbClQA99gQhnIcdhE= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/iris-contrib/blackfriday v2.0.0+incompatible/go.mod h1:UzZ2bDEoaSGPbkg6SAB4att1aAwTmVIx/5gCVqeyUdI= +github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0= +github.com/iris-contrib/jade v1.1.3/go.mod h1:H/geBymxJhShH5kecoiOCSssPX7QWYH7UaeZTSWddIk= +github.com/iris-contrib/pongo2 v0.0.1/go.mod h1:Ssh+00+3GAZqSQb30AvBRNxBx7rf0GqwkjqxNd0u65g= +github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw= +github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= +github.com/kataras/golog v0.0.10/go.mod h1:yJ8YKCmyL+nWjERB90Qwn+bdyBZsaQwU3bTVFgkFIp8= +github.com/kataras/iris/v12 v12.1.8/go.mod h1:LMYy4VlP67TQ3Zgriz8RE2h2kMZV2SgMYbq3UhfoFmE= +github.com/kataras/neffos v0.0.14/go.mod h1:8lqADm8PnbeFfL7CLXh1WHw53dG27MC3pgi2R1rmoTE= +github.com/kataras/pio v0.0.2/go.mod h1:hAoW0t9UmXi4R5Oyq5Z4irTbaTsOemSrDGUtaTl7Dro= +github.com/kataras/sitemap v0.0.5/go.mod h1:KY2eugMKiPwsJgx7+U103YZehfvNGOXURubcGyk0Bz8= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw= +github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4= +github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/labstack/echo/v4 v4.5.0/go.mod h1:czIriw4a0C1dFun+ObrXp7ok03xON0N1awStJ6ArI7Y= +github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= +github.com/leanovate/gopter v0.2.9 h1:fQjYxZaynp97ozCzfOyOuAGOU4aU/z37zf/tOujFk7c= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/mediocregopher/radix/v3 v3.4.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8= +github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= +github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= +github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= +github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= +github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d h1:AREM5mwr4u1ORQBMvzfzBgpsctsbQikCVpvC+tX285E= +github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= +github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= +github.com/onsi/gomega v1.26.0 h1:03cDLK28U6hWvCAns6NeydX3zIm4SF3ci69ulidS32Q= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= +github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= +github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pires/go-proxyproto v0.6.2 h1:KAZ7UteSOt6urjme6ZldyFm4wDe/z0ZUP0Yv0Dos0d8= +github.com/pires/go-proxyproto v0.6.2/go.mod h1:Odh9VFOZJCf9G8cLW5o435Xf1J95Jw9Gw5rnCjcwzAY= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= +github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= +github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= +github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= +github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= +github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= +github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sanity-io/litter v1.5.1 h1:dwnrSypP6q56o3lFxTU+t2fwQ9A+U5qrXVO4Qg9KwVU= +github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= +github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= +github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= +github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc= +github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg= +github.com/status-im/keycard-go v0.2.0 h1:QDLFswOQu1r5jsycloeQh3bVU8n/NatHHaZobtDnDzA= +github.com/status-im/keycard-go v0.2.0/go.mod h1:wlp8ZLbsmrF6g6WjugPAx+IzoLrkdf9+mHxBEeo3Hbg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= +github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/supranational/blst v0.3.11 h1:LyU6FolezeWAhvQk0k6O/d49jqgO52MSDDfYgbeoEm4= +github.com/supranational/blst v0.3.11/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a h1:1ur3QoCqvE5fl+nylMaIr9PVV1w343YRDtsy+Rwu7XI= +github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= +github.com/thepudds/fzgen v0.4.2 h1:HlEHl5hk2/cqEomf2uK5SA/FeJc12s/vIHmOG+FbACw= +github.com/tklauser/go-sysconf v0.3.5 h1:uu3Xl4nkLzQfXNsWn15rPc/HQCJKObbt1dKJeWp3vU4= +github.com/tklauser/go-sysconf v0.3.5/go.mod h1:MkWzOF4RMCshBAMXuhXJs64Rte09mITnppBXY/rYEFI= +github.com/tklauser/numcpus v0.2.2 h1:oyhllyrScuYI6g+h/zUvNXNp1wy7x8qQy3t/piefldA= +github.com/tklauser/numcpus v0.2.2/go.mod h1:x3qojaO3uyYt0i56EW/VUYs7uBvdl2fkfZFu0T9wgjM= +github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8= +github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/urfave/cli/v2 v2.17.2-0.20221006022127-8f469abc00aa h1:5SqCsI/2Qya2bCzK15ozrqo2sZxkh0FHynJZOTVoV6Q= +github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w= +github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= +github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= +github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= +github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg= +github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opentelemetry.io/otel v1.11.0 h1:kfToEGMDq6TrVrJ9Vht84Y8y9enykSZzDDZglV0kIEk= +go.opentelemetry.io/otel v1.11.0/go.mod h1:H2KtuEphyMvlhZ+F7tg9GRhAOe60moNx61Ex+WmiKkk= +go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.11.0 h1:0dly5et1i/6Th3WHn0M6kYiJfFNzhhxanrJ0bOfnjEo= +go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.11.0/go.mod h1:+Lq4/WkdCkjbGcBMVHHg2apTbv8oMBf29QCnyCCJjNQ= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.11.0 h1:eyJ6njZmH16h9dOKCi7lMswAnGsSOwgTqWzfxqcuNr8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.11.0/go.mod h1:FnDp7XemjN3oZ3xGunnfOUTVwd2XcvLbtRAuOSU3oc8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.11.0 h1:j2RFV0Qdt38XQ2Jvi4WIsQ56w8T7eSirYbMw19VXRDg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.11.0/go.mod h1:pILgiTEtrqvZpoiuGdblDgS5dbIaTgDrkIuKfEFkt+A= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.11.0 h1:v29I/NbVp7LXQYMFZhU6q17D0jSEbYOAVONlrO1oH5s= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.11.0/go.mod h1:/RpLsmbQLDO1XCbWAM4S6TSwj8FKwwgyKKyqtvVfAnw= +go.opentelemetry.io/otel/sdk v1.11.0 h1:ZnKIL9V9Ztaq+ME43IUi/eo22mNsb6a7tGfzaOWB5fo= +go.opentelemetry.io/otel/sdk v1.11.0/go.mod h1:REusa8RsyKaq0OlyangWXaw97t2VogoO4SSEeKkSTAk= +go.opentelemetry.io/otel/trace v1.11.0 h1:20U/Vj42SX+mASlXLmSGBg6jpI1jQtv682lZtTAOVFI= +go.opentelemetry.io/otel/trace v1.11.0/go.mod h1:nyYjis9jy0gytE9LXGU+/m1sHTKbRY0fX0hulNNDP1U= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.opentelemetry.io/proto/otlp v0.19.0 h1:IVN6GR+mhC4s5yfcTbmzHYODqvWAp3ZedA2SJPI1Nnw= +go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= +go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.25.0 h1:4Hvk6GtkucQ790dqmj7l1eEnRdKm3k3ZUrUMS2d5+5c= +go.uber.org/zap v1.25.0/go.mod h1:JIAUzQIH94IC4fOJQm7gMmBJP5k7wQfdcnYdPoEXJYk= +golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20230206171751-46f607a40771 h1:xP7rWLUr1e1n2xkK5YB4LI0hPEy3LJC6Wk+D4pGlOJg= +golang.org/x/exp v0.0.0-20230206171751-46f607a40771/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20211008194852-3b03d305991f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210316164454-77fc1eacc6aa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= +golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +gonum.org/v1/gonum v0.11.0 h1:f1IJhK4Km5tBJmaiJXtk/PkL4cdVX6J+tGiM187uT5E= +gonum.org/v1/gonum v0.11.0/go.mod h1:fSG4YDCxxUZQJ7rKsQrj0gMOg00Il0Z96/qMA4bVQhA= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180518175338-11a468237815/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= +google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A= +google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= +google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.56.0-dev h1:3XdSkn+E4E0OxKEID50paHDwVA7cqZVolkHtMFaoQJA= +google.golang.org/grpc v1.56.0-dev/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= +gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= +gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= +gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/main/main.go b/main/main.go new file mode 100644 index 00000000..2fe475df --- /dev/null +++ b/main/main.go @@ -0,0 +1,290 @@ +// Copyright (C) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package main + +import ( + "context" + "encoding/hex" + "fmt" + "log" + "net/http" + "os" + "sync" + "sync/atomic" + + "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/avalanchego/vms/platformvm" + "github.com/ava-labs/awm-relayer/config" + "github.com/ava-labs/awm-relayer/peers" + "github.com/ava-labs/awm-relayer/relayer" + "github.com/ava-labs/awm-relayer/vms" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" + "go.uber.org/zap" +) + +const ( + defaultErrorChanSize = 1000 + defaultApiPort = 8080 + defaultMetricsPort = 9090 +) + +func main() { + fs := config.BuildFlagSet() + v, err := config.BuildViper(fs, os.Args[1:]) + if err != nil { + fmt.Printf("couldn't configure flags: %s\n", err) + os.Exit(1) + } + + cfg, optionOverwritten, err := config.BuildConfig(v) + if err != nil { + fmt.Printf("couldn't build config: %s\n", err) + os.Exit(1) + } + + logLevel, err := logging.ToLevel(cfg.LogLevel) + if err != nil { + fmt.Printf("error with log level: %v", err) + } + + logger := logging.NewLogger( + "awm-relayer", + logging.NewWrappedCore( + logLevel, + os.Stdout, + logging.JSON.ConsoleEncoder(), + ), + ) + + logger.Info("Initializing awm-relayer") + overwrittenLog := "" + if optionOverwritten { + overwrittenLog = " Some options were overwritten" + } + logger.Info(fmt.Sprintf("Set config options.%s", overwrittenLog)) + + // Global P-Chain client used to get subnet validator sets + pChainClient := platformvm.NewClient(cfg.PChainAPIURL) + + // Initialize all destination clients + logger.Info("Initializing destination clients") + destinationClients, err := vms.CreateDestinationClients(logger, cfg) + if err != nil { + logger.Error( + "Failed to create destination clients", + zap.Error(err), + ) + return + } + + // Initialize the global app request network + logger.Info("Initializing app request network") + sourceSubnetIDs, sourceChainIDs, err := cfg.GetSourceIDs() + if err != nil { + logger.Error( + "Failed to get source IDs", + zap.Error(err), + ) + return + } + + // Initialize metrics gathered through prometheus + gatherer, registerer, err := initMetrics() + if err != nil { + logger.Fatal("failed to set up prometheus metrics", + zap.Error(err)) + panic(err) + } + + network, responseChans, err := peers.NewNetwork(logger, registerer, cfg.NetworkID, sourceSubnetIDs, sourceChainIDs, cfg.PChainAPIURL) + if err != nil { + logger.Error( + "Failed to create app request network", + zap.Error(err), + ) + return + } + + // Create a health check server that polls a single atomic bool, settable by any relayer goroutine on failure + healthy := atomic.Bool{} + healthy.Store(true) + + checker := health.NewChecker( + health.WithCheck(health.Check{ + Name: "relayers-all", + Check: func(context.Context) error { + if !healthy.Load() { + return fmt.Errorf("relayer is unhealthy") + } + return nil + }, + }), + ) + + http.Handle("/health", health.NewHandler(checker)) + + // start the health check server + go func() { + log.Fatalln(http.ListenAndServe(fmt.Sprintf(":%d", defaultApiPort), nil)) + }() + + startMetricsServer(logger, gatherer, defaultMetricsPort) + + metrics := relayer.NewMessageRelayerMetrics(registerer) + + // Initialize message creator passed down to relayers for creating app requests. + messageCreator, err := message.NewCreator(logger, registerer, "message_creator", constants.DefaultNetworkCompressionType, constants.DefaultNetworkMaximumInboundTimeout) + if err != nil { + logger.Error( + "Failed to create message creator", + zap.Error(err), + ) + return + } + + // Create relayers for each of the subnets configured as a source + var wg sync.WaitGroup + for _, s := range cfg.SourceSubnets { + chainID, err := ids.FromString(s.ChainID) + if err != nil { + logger.Error( + "Invalid subnetID in configuration", + zap.Error(err), + ) + return + } + wg.Add(1) + subnetInfo := s + go func() { + defer func() { + wg.Done() + healthy.Store(false) + }() + runRelayer(logger, metrics, subnetInfo, pChainClient, network, responseChans[chainID], destinationClients, messageCreator) + logger.Info( + "Relayer exiting.", + zap.String("chainID", chainID.String()), + ) + }() + } + wg.Wait() +} + +// runRelayer creates a relayer instance for a subnet. It listens for warp messages on that subnet, and handles delivery to the destination +func runRelayer(logger logging.Logger, + metrics *relayer.MessageRelayerMetrics, + sourceSubnetInfo config.SourceSubnet, + pChainClient platformvm.Client, + network *peers.AppRequestNetwork, + responseChan chan message.InboundMessage, + destinationClients map[ids.ID]vms.DestinationClient, + messageCreator message.Creator, +) { + logger.Info( + "Creating relayer", + zap.String("chainID", sourceSubnetInfo.ChainID), + ) + errorChan := make(chan error, defaultErrorChanSize) + + relayer, subscriber, err := relayer.NewRelayer( + logger, + sourceSubnetInfo, + errorChan, + pChainClient, + network, + responseChan, + destinationClients, + ) + if err != nil { + logger.Error( + "Failed to create relayer instance", + zap.Error(err), + ) + return + } + logger.Info( + "Created relayer. Listening for messages to relay.", + zap.String("chainID", sourceSubnetInfo.ChainID), + ) + + // Wait for logs from the subscribed node + for { + select { + case txLog := <-subscriber.Logs(): + logger.Info( + "Handling Teleporter submit message log.", + zap.String("txId", hex.EncodeToString(txLog.SourceTxID)), + zap.String("originChainId", sourceSubnetInfo.ChainID), + zap.String("destinationChainId", txLog.DestinationChainID.String()), + zap.String("destinationChainAddress", txLog.DestinationAddress.String()), + zap.String("sourceAddress", txLog.SourceAddress.String()), + ) + + // Relay the message to the destination chain. Continue on failure. + err = relayer.RelayMessage(&txLog, metrics, messageCreator) + if err != nil { + logger.Error( + "Error relaying message", + zap.String("originChainID", sourceSubnetInfo.ChainID), + zap.Error(err), + ) + continue + } + case err := <-subscriber.Err(): + logger.Error( + "Received error from subscribed node", + zap.String("originChainID", sourceSubnetInfo.ChainID), + zap.Error(err), + ) + err = subscriber.Subscribe() + if err != nil { + logger.Error( + "Failed to resubscribe to node. Relayer goroutine exiting.", + zap.String("originChainID", sourceSubnetInfo.ChainID), + zap.Error(err), + ) + return + } + case err := <-errorChan: + logger.Error( + "Relayer goroutine stopped with error. Relayer goroutine exiting.", + zap.String("originChainID", sourceSubnetInfo.ChainID), + zap.Error(err), + ) + return + } + } +} + +func startMetricsServer(logger logging.Logger, gatherer prometheus.Gatherer, port uint32) { + http.Handle("/metrics", promhttp.HandlerFor(gatherer, promhttp.HandlerOpts{})) + + go func() { + logger.Info("starting metrics server...", + zap.Uint32("port", port)) + err := http.ListenAndServe(fmt.Sprintf(":%d", port), nil) + if err != nil { + logger.Fatal("metrics server exited", + zap.Error(err), + zap.Uint32("port", port)) + panic(err) + } + }() +} + +func initMetrics() (prometheus.Gatherer, prometheus.Registerer, error) { + gatherer := metrics.NewMultiGatherer() + registry := prometheus.NewRegistry() + if err := gatherer.Register("app", registry); err != nil { + return nil, nil, err + + } + return gatherer, registry, nil +} diff --git a/messages/message_manager.go b/messages/message_manager.go new file mode 100644 index 00000000..3a7601e6 --- /dev/null +++ b/messages/message_manager.go @@ -0,0 +1,48 @@ +// Copyright (C) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package messages + +import ( + "fmt" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/avalanchego/vms/platformvm/warp" + "github.com/ava-labs/awm-relayer/config" + "github.com/ava-labs/awm-relayer/messages/teleporter" + "github.com/ava-labs/awm-relayer/vms" + "github.com/ava-labs/awm-relayer/vms/vmtypes" + "github.com/ethereum/go-ethereum/common" +) + +// MessageManager is specific to each message protocol. The interface handles choosing which messages to send +// for each message protocol, and performs the sending to the destination chain. +type MessageManager interface { + // ShouldSendMessage returns true if the message should be sent to the destination chain + ShouldSendMessage(warpMessageInfo *vmtypes.WarpMessageInfo, destinationChainID ids.ID) (bool, error) + // 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, parsedVmPayload []byte, destinationChainID ids.ID) error +} + +// NewMessageManager constructs a MessageManager for a particular message protocol, defined by the message protocol address and config +// Note that DestinationClients may be invoked concurrently by many MessageManagers, so it is assumed that they are implemented in a thread-safe way +func NewMessageManager( + logger logging.Logger, + messageProtocolAddress common.Hash, + messageProtocolConfig config.MessageProtocolConfig, + destinationClients map[ids.ID]vms.DestinationClient, +) (MessageManager, error) { + format := messageProtocolConfig.MessageFormat + switch config.ParseMessageProtocol(format) { + case config.TELEPORTER: + return teleporter.NewMessageManager(logger, + messageProtocolAddress, + messageProtocolConfig, + destinationClients, + ) + default: + return nil, fmt.Errorf("invalid message format %s", format) + } +} diff --git a/messages/teleporter/abi.go b/messages/teleporter/abi.go new file mode 100644 index 00000000..650dfafb --- /dev/null +++ b/messages/teleporter/abi.go @@ -0,0 +1,42 @@ +// Copyright (C) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package teleporter + +import ( + "fmt" + "strings" + + "github.com/ava-labs/subnet-evm/accounts/abi" +) + +// ABI for ITeleporterMessenger.sol in github.com/ava-labs/teleporter@0f76bf51d02027a3139680a880a0d4ddff782ec1 +const EVMTeleporterContractRawABI = "[{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"destinationChainID\",\"type\":\"bytes32\"},{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"messageID\",\"type\":\"uint256\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"contractAddress\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"indexed\":false,\"internalType\":\"struct TeleporterFeeInfo\",\"name\":\"updatedFeeInfo\",\"type\":\"tuple\"}],\"name\":\"AddFeeAmount\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"originChainID\",\"type\":\"bytes32\"},{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"messageID\",\"type\":\"uint256\"},{\"components\":[{\"internalType\":\"uint256\",\"name\":\"messageID\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"senderAddress\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"destinationAddress\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"requiredGasLimit\",\"type\":\"uint256\"},{\"internalType\":\"address[]\",\"name\":\"allowedRelayerAddresses\",\"type\":\"address[]\"},{\"components\":[{\"internalType\":\"uint256\",\"name\":\"receivedMessageID\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"relayerRewardAddress\",\"type\":\"address\"}],\"internalType\":\"struct TeleporterMessageReceipt[]\",\"name\":\"receipts\",\"type\":\"tuple[]\"},{\"internalType\":\"bytes\",\"name\":\"message\",\"type\":\"bytes\"}],\"indexed\":false,\"internalType\":\"struct TeleporterMessage\",\"name\":\"message\",\"type\":\"tuple\"}],\"name\":\"FailedMessageExecution\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"destinationChainID\",\"type\":\"bytes32\"},{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"messageID\",\"type\":\"uint256\"},{\"components\":[{\"internalType\":\"uint256\",\"name\":\"messageID\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"senderAddress\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"destinationAddress\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"requiredGasLimit\",\"type\":\"uint256\"},{\"internalType\":\"address[]\",\"name\":\"allowedRelayerAddresses\",\"type\":\"address[]\"},{\"components\":[{\"internalType\":\"uint256\",\"name\":\"receivedMessageID\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"relayerRewardAddress\",\"type\":\"address\"}],\"internalType\":\"struct TeleporterMessageReceipt[]\",\"name\":\"receipts\",\"type\":\"tuple[]\"},{\"internalType\":\"bytes\",\"name\":\"message\",\"type\":\"bytes\"}],\"indexed\":false,\"internalType\":\"struct TeleporterMessage\",\"name\":\"message\",\"type\":\"tuple\"}],\"name\":\"SendCrossChainMessage\",\"type\":\"event\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"destinationChainID\",\"type\":\"bytes32\"},{\"internalType\":\"uint256\",\"name\":\"messageID\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"feeContractAddress\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"additionalFeeAmount\",\"type\":\"uint256\"}],\"name\":\"addFeeAmount\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"relayer\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"feeAsset\",\"type\":\"address\"}],\"name\":\"checkRelayerRewardAmount\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"destinationChainID\",\"type\":\"bytes32\"},{\"internalType\":\"uint256\",\"name\":\"messageID\",\"type\":\"uint256\"}],\"name\":\"getFeeInfo\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"feeAsset\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"feeAmount\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"destinationChainID\",\"type\":\"bytes32\"},{\"internalType\":\"uint256\",\"name\":\"messageID\",\"type\":\"uint256\"}],\"name\":\"getMessageHash\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"messageHash\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"originChainID\",\"type\":\"bytes32\"},{\"internalType\":\"uint256\",\"name\":\"messageID\",\"type\":\"uint256\"}],\"name\":\"getRelayerRewardAddress\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"relayerRewardAddress\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"originChainID\",\"type\":\"bytes32\"},{\"internalType\":\"uint256\",\"name\":\"messageID\",\"type\":\"uint256\"}],\"name\":\"messageReceived\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"delivered\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"relayerRewardAddress\",\"type\":\"address\"}],\"name\":\"receiveCrossChainMessage\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"feeAsset\",\"type\":\"address\"}],\"name\":\"redeemRelayerRewards\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"originChainID\",\"type\":\"bytes32\"},{\"components\":[{\"internalType\":\"uint256\",\"name\":\"messageID\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"senderAddress\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"destinationAddress\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"requiredGasLimit\",\"type\":\"uint256\"},{\"internalType\":\"address[]\",\"name\":\"allowedRelayerAddresses\",\"type\":\"address[]\"},{\"components\":[{\"internalType\":\"uint256\",\"name\":\"receivedMessageID\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"relayerRewardAddress\",\"type\":\"address\"}],\"internalType\":\"struct TeleporterMessageReceipt[]\",\"name\":\"receipts\",\"type\":\"tuple[]\"},{\"internalType\":\"bytes\",\"name\":\"message\",\"type\":\"bytes\"}],\"internalType\":\"struct TeleporterMessage\",\"name\":\"message\",\"type\":\"tuple\"}],\"name\":\"retryMessageExecution\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"originChainID\",\"type\":\"bytes32\"},{\"internalType\":\"uint256[]\",\"name\":\"messageIDs\",\"type\":\"uint256[]\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"contractAddress\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"struct TeleporterFeeInfo\",\"name\":\"feeInfo\",\"type\":\"tuple\"},{\"internalType\":\"address[]\",\"name\":\"allowedRelayerAddresses\",\"type\":\"address[]\"}],\"name\":\"retryReceipts\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"messageID\",\"type\":\"uint256\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"destinationChainID\",\"type\":\"bytes32\"},{\"components\":[{\"internalType\":\"uint256\",\"name\":\"messageID\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"senderAddress\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"destinationAddress\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"requiredGasLimit\",\"type\":\"uint256\"},{\"internalType\":\"address[]\",\"name\":\"allowedRelayerAddresses\",\"type\":\"address[]\"},{\"components\":[{\"internalType\":\"uint256\",\"name\":\"receivedMessageID\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"relayerRewardAddress\",\"type\":\"address\"}],\"internalType\":\"struct TeleporterMessageReceipt[]\",\"name\":\"receipts\",\"type\":\"tuple[]\"},{\"internalType\":\"bytes\",\"name\":\"message\",\"type\":\"bytes\"}],\"internalType\":\"struct TeleporterMessage\",\"name\":\"message\",\"type\":\"tuple\"}],\"name\":\"retrySendCrossChainMessage\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"destinationChainID\",\"type\":\"bytes32\"},{\"internalType\":\"address\",\"name\":\"destinationAddress\",\"type\":\"address\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"contractAddress\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"struct TeleporterFeeInfo\",\"name\":\"feeInfo\",\"type\":\"tuple\"},{\"internalType\":\"uint256\",\"name\":\"requiredGasLimit\",\"type\":\"uint256\"},{\"internalType\":\"address[]\",\"name\":\"allowedRelayerAddresses\",\"type\":\"address[]\"},{\"internalType\":\"bytes\",\"name\":\"message\",\"type\":\"bytes\"}],\"internalType\":\"struct TeleporterMessageInput\",\"name\":\"messageInput\",\"type\":\"tuple\"}],\"name\":\"sendCrossChainMessage\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"messageID\",\"type\":\"uint256\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]" + +var EVMTeleporterContractABI abi.ABI + +var TeleporterMessageABI abi.Type + +func init() { + parsed, err := abi.JSON(strings.NewReader(EVMTeleporterContractRawABI)) + if err != nil { + panic(fmt.Sprintf("failed to create evm contract ABI: %v", err)) + } + EVMTeleporterContractABI = parsed + + TeleporterMessageABI, err = abi.NewType("tuple", "struct Overloader.F", []abi.ArgumentMarshaling{ + {Name: "messageID", Type: "uint256"}, + {Name: "senderAddress", Type: "address"}, + {Name: "destinationAddress", Type: "address"}, + {Name: "requiredGasLimit", Type: "uint256"}, + {Name: "allowedRelayerAddresses", Type: "address[]"}, + {Name: "receipts", Type: "tuple[]", Components: []abi.ArgumentMarshaling{ + {Name: "receivedMessageID", Type: "uint256"}, + {Name: "relayerRewardAddress", Type: "address"}, + }}, + {Name: "message", Type: "bytes"}, + }) + if err != nil { + panic(fmt.Sprintf("failed to create TeleporterMessage ABI type: %v", err)) + } +} diff --git a/messages/teleporter/config.go b/messages/teleporter/config.go new file mode 100644 index 00000000..24582fd4 --- /dev/null +++ b/messages/teleporter/config.go @@ -0,0 +1,21 @@ +// Copyright (C) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package teleporter + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/common" +) + +type Config struct { + RewardAddress string `json:"reward-address"` +} + +func (c *Config) Validate() error { + if !common.IsHexAddress(c.RewardAddress) { + return fmt.Errorf("invalid reward address for EVM source subnet: %s", c.RewardAddress) + } + return nil +} diff --git a/messages/teleporter/message.go b/messages/teleporter/message.go new file mode 100644 index 00000000..0d467bef --- /dev/null +++ b/messages/teleporter/message.go @@ -0,0 +1,62 @@ +// Copyright (C) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package teleporter + +import ( + "fmt" + "math/big" + + "github.com/ava-labs/subnet-evm/accounts/abi" + "github.com/ethereum/go-ethereum/common" +) + +// TeleporterMessage contains the Teleporter message, including +// the payload Message as a byte slice +type TeleporterMessage struct { + MessageID *big.Int `json:"messageID"` + SenderAddress common.Address `json:"senderAddress"` + DestinationAddress common.Address `json:"destinationAddress"` + RequiredGasLimit *big.Int `json:"requiredGasLimit"` + AllowedRelayerAddresses []common.Address `json:"allowedRelayerAddresses"` + Receipts []TeleporterMessageReceipt `json:"receipts"` + Message []byte `json:"message"` +} + +type TeleporterMessageReceipt struct { + ReceivedMessageID *big.Int `json:"receivedMessageID"` + RelayerRewardAddress common.Address `json:"relayerRewardAddress"` +} + +// ReceiveCrossChainMessageInput is the input to the ReceiveCrossChainMessage +// in the contract deployed on the receiving chain +type ReceiveCrossChainMessageInput struct { + RelayerRewardAddress common.Address `json:"relayerRewardAddress"` +} + +// unpack Teleporter message bytes according to EVM ABI encoding rules +func unpackTeleporterMessage(messageBytes []byte) (*TeleporterMessage, error) { + args := abi.Arguments{ + { + Name: "teleporterMessage", + Type: TeleporterMessageABI, + }, + } + unpacked, err := args.Unpack(messageBytes) + if err != nil { + return nil, fmt.Errorf("failed to unpack to teleporter message with err: %v", err) + } + type teleporterMessageArg struct { + TeleporterMessage TeleporterMessage `json:"teleporterMessage"` + } + var teleporterMessage teleporterMessageArg + err = args.Copy(&teleporterMessage, unpacked) + if err != nil { + return nil, err + } + return &teleporterMessage.TeleporterMessage, nil +} + +func packReceiverMessage(inputStruct ReceiveCrossChainMessageInput) ([]byte, error) { + return EVMTeleporterContractABI.Pack("receiveCrossChainMessage", inputStruct.RelayerRewardAddress) +} diff --git a/messages/teleporter/message_manager.go b/messages/teleporter/message_manager.go new file mode 100644 index 00000000..e7cca282 --- /dev/null +++ b/messages/teleporter/message_manager.go @@ -0,0 +1,180 @@ +// Copyright (C) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package teleporter + +import ( + "encoding/json" + "fmt" + + "github.com/ava-labs/avalanchego/cache" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/avalanchego/vms/platformvm/warp" + "github.com/ava-labs/awm-relayer/config" + "github.com/ava-labs/awm-relayer/vms" + "github.com/ava-labs/awm-relayer/vms/vmtypes" + "github.com/ethereum/go-ethereum/common" + "go.uber.org/zap" +) + +const ( + teleporterMessageCacheSize = 100 +) + +type messageManager struct { + messageConfig Config + protocolAddress common.Hash + + // We parse teleporter messages in ShouldSendMessage, cache them to be reused in SendMessage + // The cache is keyed by the Warp message ID, NOT the Teleporter message ID + teleporterMessageCache *cache.LRU[ids.ID, *TeleporterMessage] + destinationClients map[ids.ID]vms.DestinationClient + + logger logging.Logger +} + +func NewMessageManager( + logger logging.Logger, + messageProtocolAddress common.Hash, + messageProtocolConfig config.MessageProtocolConfig, + destinationClients map[ids.ID]vms.DestinationClient, +) (*messageManager, error) { + // Marshal the map and unmarshal into the Teleporter config + data, err := json.Marshal(messageProtocolConfig.Settings) + if err != nil { + logger.Error("Failed to marshal Teleporter config") + return nil, err + } + var messageConfig Config + if err := json.Unmarshal(data, &messageConfig); err != nil { + logger.Error("Failed to unmarshal Teleporter config") + return nil, err + } + + if err := messageConfig.Validate(); err != nil { + logger.Error( + "Invalid Teleporter config.", + zap.Error(err), + ) + return nil, err + } + teleporterMessageCache := &cache.LRU[ids.ID, *TeleporterMessage]{Size: teleporterMessageCacheSize} + + return &messageManager{ + messageConfig: messageConfig, + protocolAddress: messageProtocolAddress, + teleporterMessageCache: teleporterMessageCache, + destinationClients: destinationClients, + logger: logger, + }, nil +} + +// ShouldSendMessage returns true if the message should be sent to the destination chain +func (m *messageManager) ShouldSendMessage(warpMessageInfo *vmtypes.WarpMessageInfo, destinationChainID ids.ID) (bool, error) { + // Unpack the teleporter message and add it to the cache + teleporterMessage, err := unpackTeleporterMessage(warpMessageInfo.WarpPayload) + if err != nil { + m.logger.Error( + "Failed unpacking teleporter message.", + zap.String("warpMessageID", warpMessageInfo.WarpUnsignedMessage.ID().String()), + ) + return false, err + } + + // Get the correct destination client from the global map + destinationClient, ok := m.destinationClients[destinationChainID] + if !ok { + return false, fmt.Errorf("relayer not configured to deliver to destination. destinationChainID=%s", destinationChainID.String()) + } + if !destinationClient.Allowed(destinationChainID, teleporterMessage.AllowedRelayerAddresses) { + m.logger.Info( + "Relayer not allowed to deliver to chain.", + zap.String("destinationChainID", destinationChainID.String()), + ) + return false, nil + } + // Cache the message so it can be reused in SendMessage + m.teleporterMessageCache.Put(warpMessageInfo.WarpUnsignedMessage.ID(), teleporterMessage) + return true, 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 *messageManager) SendMessage(signedMessage *warp.Message, parsedVmPayload []byte, destinationChainID ids.ID) error { + var ( + teleporterMessage *TeleporterMessage + ok bool + ) + teleporterMessage, ok = m.teleporterMessageCache.Get(signedMessage.ID()) + if !ok { + m.logger.Debug( + "Teleporter message to send not in cache. Extracting from signed warp message.", + zap.String("warpMessageID", signedMessage.ID().String()), + ) + var err error + teleporterMessage, err = unpackTeleporterMessage(parsedVmPayload) + if err != nil { + m.logger.Error( + "Failed unpacking teleporter message.", + zap.String("warpMessageID", signedMessage.ID().String()), + ) + return err + } + } + + m.logger.Info( + "Sending message to destination chain", + zap.String("destinationChainID", destinationChainID.String()), + zap.String("teleporterMessageID", teleporterMessage.MessageID.String()), + ) + numSigners, err := signedMessage.Signature.NumSigners() + if err != nil { + m.logger.Error( + "Failed to get number of signers", + zap.String("warpMessageID", signedMessage.ID().String()), + ) + return err + } + gasLimit, err := CalculateReceiveMessageGasLimit(numSigners, teleporterMessage.RequiredGasLimit) + if err != nil { + m.logger.Error( + "Gas limit required overflowed uint64 max. not relaying message", + zap.String("warpMessageID", signedMessage.ID().String()), + ) + return err + } + // Construct the transaction call data to call the receive cross chain message method of the receiver precompile. + callData, err := packReceiverMessage(ReceiveCrossChainMessageInput{ + RelayerRewardAddress: common.HexToAddress(m.messageConfig.RewardAddress), + }) + if err != nil { + m.logger.Error( + "Failed packing receiveCrossChainMessage call data", + zap.String("warpMessageID", signedMessage.ID().String()), + ) + return err + } + + // Get the correct destination client from the global map + destinationClient, ok := m.destinationClients[destinationChainID] + if !ok { + return fmt.Errorf("relayer not configured to deliver to destination. destinationChainID=%s", destinationChainID) + } + err = destinationClient.SendTx(signedMessage, m.protocolAddress.Hex(), gasLimit, callData) + if err != nil { + m.logger.Error( + "Failed to send tx.", + zap.String("warpMessageID", signedMessage.ID().String()), + zap.String("destinationChainID", destinationChainID.String()), + zap.Error(err), + ) + return err + } + m.logger.Info( + "Sent message to destination chain", + zap.String("destinationChainID", destinationChainID.String()), + zap.String("teleporterMessageID", teleporterMessage.MessageID.String()), + ) + return nil +} diff --git a/messages/teleporter/message_test.go b/messages/teleporter/message_test.go new file mode 100644 index 00000000..62740217 --- /dev/null +++ b/messages/teleporter/message_test.go @@ -0,0 +1,83 @@ +// Copyright (C) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package teleporter + +import ( + "bytes" + "encoding/hex" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/assert" +) + +func testTeleporterMessage(messageID int64) TeleporterMessage { + m := TeleporterMessage{ + MessageID: big.NewInt(messageID), + SenderAddress: common.HexToAddress("0x0123456789abcdef0123456789abcdef01234567"), + DestinationAddress: common.HexToAddress("0x0123456789abcdef0123456789abcdef01234567"), + RequiredGasLimit: big.NewInt(2), + AllowedRelayerAddresses: []common.Address{ + common.HexToAddress("0x0123456789abcdef0123456789abcdef01234567"), + }, + Receipts: []TeleporterMessageReceipt{ + { + ReceivedMessageID: big.NewInt(1), + RelayerRewardAddress: common.HexToAddress("0x0123456789abcdef0123456789abcdef01234567"), + }, + }, + Message: []byte{1, 2, 3, 4}, + } + return m +} + +// Pack the SendCrossChainMessage event type. PackEvent is documented as not supporting struct types, so this should be used +// with caution. Here, we only use it for testing purposes. In a real setting, the Teleporter contract should pack the event. +func packTeleporterMessage(destinationChainID common.Hash, message TeleporterMessage) ([]byte, error) { + _, hashes, err := EVMTeleporterContractABI.PackEvent("SendCrossChainMessage", destinationChainID, message.MessageID, message) + return hashes, err +} + +func TestPackUnpackTeleporterMessage(t *testing.T) { + message := testTeleporterMessage(4) + + b, err := packTeleporterMessage(common.HexToHash("0x03"), message) + if err != nil { + t.Errorf("failed to pack teleporter message: %v", err) + t.FailNow() + } + + unpacked, err := unpackTeleporterMessage(b) + if err != nil { + t.Errorf("failed to unpack teleporter message: %v", err) + t.FailNow() + } + + if unpacked.MessageID.Cmp(message.MessageID) != 0 { + t.Errorf("message ids do not match. expected: %d actual: %d", message.MessageID.Uint64(), unpacked.MessageID.Uint64()) + } + if unpacked.SenderAddress != message.SenderAddress { + t.Errorf("sender addresses do not match. expected: %s actual: %s", message.SenderAddress.Hex(), unpacked.SenderAddress.Hex()) + } + if unpacked.DestinationAddress != message.DestinationAddress { + t.Errorf("destination addresses do not match. expected: %s actual: %s", message.DestinationAddress.Hex(), unpacked.DestinationAddress.Hex()) + } + if unpacked.RequiredGasLimit.Cmp(message.RequiredGasLimit) != 0 { + t.Errorf("required gas limits do not match. expected: %d actual: %d", message.RequiredGasLimit.Uint64(), unpacked.RequiredGasLimit.Uint64()) + } + for i := 0; i < len(message.AllowedRelayerAddresses); i++ { + if unpacked.AllowedRelayerAddresses[i] != message.AllowedRelayerAddresses[i] { + t.Errorf("allowed relayer addresses %d do not match. expected: %s actual: %s", i, message.AllowedRelayerAddresses[i].Hex(), unpacked.AllowedRelayerAddresses[i].Hex()) + } + } + for i := 0; i < len(message.Receipts); i++ { + assert.Equal(t, 0, unpacked.Receipts[i].ReceivedMessageID.Cmp(message.Receipts[i].ReceivedMessageID)) + assert.Equal(t, message.Receipts[i].RelayerRewardAddress, unpacked.Receipts[i].RelayerRewardAddress) + } + + if !bytes.Equal(unpacked.Message, message.Message) { + t.Errorf("messages do not match. expected: %s actual: %s", hex.EncodeToString(message.Message), hex.EncodeToString(unpacked.Message)) + } +} diff --git a/messages/teleporter/utils.go b/messages/teleporter/utils.go new file mode 100644 index 00000000..5b66648f --- /dev/null +++ b/messages/teleporter/utils.go @@ -0,0 +1,49 @@ +// Copyright (C) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package teleporter + +import ( + "errors" + "math/big" + + "github.com/ava-labs/avalanchego/utils/math" + "github.com/ava-labs/awm-relayer/utils" +) + +const ( + receiveMessageGasLimitBufferAmount = 100_000 +) + +var ( + errRequiredGasLimitTooHigh = errors.New("required gas limit too high") +) + +// CalculateReceiveMessageGasLimit calculates the estimated gas amount used by a single call +// to receiveCrossChainMessage for the given message and validator bit vector. The result amount +// depends on the required limit for the message execution, the number of validator signatures +// included in the aggregate signature, the static gas cost defined by the precompile, and an +// extra buffer amount defined here to ensure the call doesn't run out of gas. +func CalculateReceiveMessageGasLimit(numSigners int, executionRequiredGasLimit *big.Int) (uint64, error) { + if !executionRequiredGasLimit.IsUint64() { + return 0, errRequiredGasLimitTooHigh + } + + gasAmounts := []uint64{ + executionRequiredGasLimit.Uint64(), + utils.ReceiveCrossChainMessageStaticGasCost, + uint64(numSigners) * utils.ReceiveCrossChainMessageGasCostPerAggregatedKey, + receiveMessageGasLimitBufferAmount, + } + + res := gasAmounts[0] + var err error + for i := 1; i < len(gasAmounts); i++ { + res, err = math.Add64(res, gasAmounts[i]) + if err != nil { + return 0, err + } + } + + return res, nil +} diff --git a/peers/app_request_network.go b/peers/app_request_network.go new file mode 100644 index 00000000..069422e8 --- /dev/null +++ b/peers/app_request_network.go @@ -0,0 +1,239 @@ +// Copyright (C) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package peers + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/ava-labs/avalanchego/api/info" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/message" + "github.com/ava-labs/avalanchego/network" + "github.com/ava-labs/avalanchego/snow/validators" + "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/ips" + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/avalanchego/utils/sampler" + "github.com/ava-labs/avalanchego/utils/set" + "github.com/prometheus/client_golang/prometheus" + "go.uber.org/zap" +) + +const ( + LocalNetworkID = 1337 // ID used by avalanche-cli for local networks + InboundMessageChannelSize = 1000 + DefaultAppRequestTimeout = time.Second * 2 + + numInitialTestPeers = 5 +) + +type AppRequestNetwork struct { + Network network.Network + Handler *RelayerExternalHandler + infoClient info.Client + logger logging.Logger + lock *sync.Mutex +} + +// NewNetwork connects to a peers at the app request level. +func NewNetwork( + logger logging.Logger, + registerer prometheus.Registerer, + networkID uint32, + subnetIDs []ids.ID, + chainIDs []ids.ID, + APINodeURL string, +) (*AppRequestNetwork, map[ids.ID]chan message.InboundMessage, error) { + if networkID != constants.MainnetID && + networkID != constants.FujiID && + len(APINodeURL) == 0 { + return nil, nil, fmt.Errorf("must provide an API URL for local networks") + } + + // Create the test network for AppRequests + var trackedSubnets set.Set[ids.ID] + for _, subnetID := range subnetIDs { + trackedSubnets.Add(subnetID) + } + + // Construct a response chan for each chain. Inbound messages will be routed to the proper channel in the handler + responseChans := make(map[ids.ID]chan message.InboundMessage) + for _, chainID := range chainIDs { + responseChan := make(chan message.InboundMessage, InboundMessageChannelSize) + responseChans[chainID] = responseChan + } + responseChansLock := new(sync.RWMutex) + + handler, err := NewRelayerExternalHandler(logger, registerer, responseChans, responseChansLock) + if err != nil { + logger.Error( + "Failed to create p2p network handler", + zap.Error(err), + ) + return nil, nil, err + } + + network, err := network.NewTestNetwork(logger, networkID, validators.NewSet(), trackedSubnets, handler) + if err != nil { + logger.Error( + "Failed to create test network", + zap.Error(err), + ) + return nil, nil, err + } + + // We need to initially connect to some nodes in the network before peer + // gossip will enable connecting to all the remaining nodes in the network. + var ( + beaconIPs, beaconIDs []string + infoClient info.Client + ) + + // Create the info client + infoClient = info.NewClient(APINodeURL) + peers, err := infoClient.Peers(context.Background()) + if err != nil { + logger.Error( + "Failed to get peers", + zap.Error(err), + ) + return nil, nil, err + } + ip, err := infoClient.GetNodeIP(context.Background()) + if err != nil { + logger.Error( + "Failed to get ip", + zap.Error(err), + ) + return nil, nil, err + } + id, _, err := infoClient.GetNodeID(context.Background()) + if err != nil { + logger.Error( + "Failed to get node id", + zap.Error(err), + ) + return nil, nil, err + } + beaconIPs = append(beaconIPs, ip) + beaconIDs = append(beaconIDs, id.String()) + + var indices []uint64 + // If we have more peers than numInitialTestPeers, take a random sample of numInitialTestPeers peers + if len(peers) > numInitialTestPeers { + s := sampler.NewUniform() + s.Initialize(uint64(len(peers))) + indices, _ = s.Sample(numInitialTestPeers) + } else { + for i := range peers { + indices = append(indices, uint64(i)) + } + } + + for _, index := range indices { + // Do not attempt to connect to private peers + if len(peers[index].PublicIP) == 0 { + continue + } + beaconIPs = append(beaconIPs, peers[index].PublicIP) + beaconIDs = append(beaconIDs, peers[index].ID.String()) + } + + for i, beaconIDStr := range beaconIDs { + beaconID, err := ids.NodeIDFromString(beaconIDStr) + if err != nil { + logger.Error( + "Failed to parse beaconID", + zap.String("beaconID", beaconIDStr), + zap.Error(err), + ) + return nil, nil, err + } + + beaconIPStr := beaconIPs[i] + ipPort, err := ips.ToIPPort(beaconIPStr) + if err != nil { + logger.Error( + "Failed to parse beaconIP", + zap.String("beaconIP", beaconIPStr), + zap.Error(err), + ) + return nil, nil, err + } + + network.ManuallyTrack(beaconID, ipPort) + } + + go logger.RecoverAndPanic(func() { + network.Dispatch() + }) + + return &AppRequestNetwork{ + Network: network, + Handler: handler, + infoClient: infoClient, + logger: logger, + lock: new(sync.Mutex), + }, responseChans, nil +} + +// ConnectPeers connects the network to peers with the given nodeIDs. +// On success, returns the provided set of nodeIDs and a nil error. +// On failure, returns the set of nodeIDs that successfully connected and an error. +func (n *AppRequestNetwork) ConnectPeers(nodeIDs set.Set[ids.NodeID]) (set.Set[ids.NodeID], error) { + n.lock.Lock() + defer n.lock.Unlock() + + var ( + retErr error + trackedNodes set.Set[ids.NodeID] + ) + + // First, check if we are already connected to all the peers + connectedPeers := n.Network.PeerInfo(nodeIDs.List()) + if len(connectedPeers) == nodeIDs.Len() { + return nodeIDs, nil + } + + // If we are not connected to all the peers already, then we have to iterate + // through the full list of peers obtained from the info API. Rather than iterating + // through connectedPeers for already tracked peers, just iterate through the full list, + // re-adding connections to already tracked peers. + + // Get the list of peers + peers, err := n.infoClient.Peers(context.Background()) + if err != nil { + n.logger.Error( + "failed to get peers", + zap.Error(err), + ) + return nil, err + } + + // Attempt to connect to each peer + for _, peer := range peers { + if nodeIDs.Contains(peer.ID) { + ipPort, err := ips.ToIPPort(peer.PublicIP) + if err != nil { + n.logger.Error( + "Failed to parse peer IP", + zap.String("beaconIP", peer.PublicIP), + zap.Error(err), + ) + retErr = fmt.Errorf("failed to connect to peers: %v", err) + continue + } + trackedNodes.Add(peer.ID) + n.Network.ManuallyTrack(peer.ID, ipPort) + if len(trackedNodes) == nodeIDs.Len() { + break + } + } + } + + return trackedNodes, retErr +} diff --git a/peers/external_handler.go b/peers/external_handler.go new file mode 100644 index 00000000..718ad10f --- /dev/null +++ b/peers/external_handler.go @@ -0,0 +1,175 @@ +// Copyright (C) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package peers + +import ( + "context" + "sync" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/message" + "github.com/ava-labs/avalanchego/snow/networking/router" + "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/avalanchego/utils/timer" + "github.com/ava-labs/avalanchego/version" + "github.com/prometheus/client_golang/prometheus" + "go.uber.org/zap" +) + +var _ router.ExternalHandler = (*RelayerExternalHandler)(nil) + +// Note: all of the external handler's methods are called on peer goroutines. It +// is possible for multiple concurrent calls to happen with different NodeIDs. +// However, a given NodeID will only be performing one call at a time. +type RelayerExternalHandler struct { + log logging.Logger + responseChansLock *sync.RWMutex + responseChans map[ids.ID]chan message.InboundMessage + timeoutManager timer.AdaptiveTimeoutManager +} + +// Create a new RelayerExternalHandler to forward relevant inbound app messages to the respective Teleporter message relayer, as well as handle timeouts. +func NewRelayerExternalHandler( + logger logging.Logger, + registerer prometheus.Registerer, + responseChans map[ids.ID]chan message.InboundMessage, + responseChansLock *sync.RWMutex, +) (*RelayerExternalHandler, error) { + // TODO: Leaving this static for now, but we may want to have this as a config option + cfg := timer.AdaptiveTimeoutConfig{ + InitialTimeout: constants.DefaultNetworkInitialTimeout, + MinimumTimeout: constants.DefaultNetworkInitialTimeout, + MaximumTimeout: constants.DefaultNetworkMaximumTimeout, + TimeoutCoefficient: constants.DefaultNetworkTimeoutCoefficient, + TimeoutHalflife: constants.DefaultNetworkTimeoutHalflife, + } + + timeoutManager, err := timer.NewAdaptiveTimeoutManager(&cfg, "external_handler", registerer) + if err != nil { + logger.Error( + "Failed to create timeout manager", + zap.Error(err), + ) + return nil, err + } + + go timeoutManager.Dispatch() + + return &RelayerExternalHandler{ + log: logger, + responseChansLock: responseChansLock, + responseChans: responseChans, + timeoutManager: timeoutManager, + }, nil +} + +// HandleInbound handles all inbound app message traffic. For the relayer, we only care about App Responses to +// signature request App Requests, and App Request Fail messages sent by the timeout manager. +// 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 message relayer. +// On startup, one TeleporterRelayer 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 TeleporterRelayer, a teleporterMessageRelayer goroutine is created and closed +// once the cross-chain message is delivered. Messages are routed through the resulting tree structure: +// +// TeleporterRelayer/source subnet teleporterMessageRelayer/Teleporter message +// +// { TeleporterRelayer.responseChan --... { teleporterMessageRelayer.messageResponseChan (consumer) +// HandleInbound (producer) --->{ TeleporterRelayer.responseChan ------>{ teleporterMessageRelayer.messageResponseChan (consumer) +// { ... { ... +// { TeleporterRelayer.responseChan --... { teleporterMessageRelayer.messageResponseChan (consumer) + +func (h *RelayerExternalHandler) HandleInbound(_ context.Context, inboundMessage message.InboundMessage) { + h.log.Debug( + "receiving message", + zap.Stringer("op", inboundMessage.Op()), + ) + if inboundMessage.Op() == message.AppResponseOp || inboundMessage.Op() == message.AppRequestFailedOp { + h.log.Info("handling app response", zap.Stringer("from", inboundMessage.NodeID())) + + // Extract the message fields + m := inboundMessage.Message() + + // Get the ChainID from the message. + // Note: we should NOT call GetSourceChainID; this is for cross-chain messages using the vm2 interface + // For normal app requests messages, the calls result in the same value, but if the relayer handles an + // inbound cross-chain app message, then we would get the incorrect chain ID. + chainID, err := message.GetChainID(m) + if err != nil { + h.log.Error("could not get chainID from message") + inboundMessage.OnFinishedHandling() + return + } + sourceChainID, err := message.GetSourceChainID(m) + if err != nil { + h.log.Error("could not get sourceChainID from message") + inboundMessage.OnFinishedHandling() + return + } + requestID, ok := message.GetRequestID(m) + if !ok { + h.log.Error("could not get requestID from message") + inboundMessage.OnFinishedHandling() + return + } + + reqID := ids.RequestID{ + NodeID: inboundMessage.NodeID(), + SourceChainID: sourceChainID, + DestinationChainID: chainID, + RequestID: requestID, + Op: byte(inboundMessage.Op()), + } + h.RegisterResponse(reqID) + + // Route to the appropriate response channel. Do not block on this call, otherwise incoming message handling may be blocked + // OnFinishedHandling is called by the consumer of the response channel + go func(message.InboundMessage, ids.ID) { + h.responseChansLock.RLock() + defer h.responseChansLock.RUnlock() + + h.responseChans[chainID] <- inboundMessage + }(inboundMessage, chainID) + + } else { + inboundMessage.OnFinishedHandling() + } +} + +func (h *RelayerExternalHandler) Connected(nodeID ids.NodeID, version *version.Application, subnetID ids.ID) { + h.log.Info( + "connected", + zap.Stringer("nodeID", nodeID), + zap.Stringer("version", version), + zap.Stringer("subnetID", subnetID), + ) +} + +func (h *RelayerExternalHandler) Disconnected(nodeID ids.NodeID) { + h.log.Info( + "disconnected", + zap.Stringer("nodeID", nodeID), + ) +} + +// RegisterRequest registers an AppRequest with the timeout manager. +// If RegisterResponse is not called before the timeout, HandleInbound is called with +// an internally created AppRequestFailed message. +func (h *RelayerExternalHandler) RegisterRequest(reqID ids.RequestID) { + inMsg := message.InternalAppRequestFailed( + reqID.NodeID, + reqID.SourceChainID, + reqID.RequestID, + ) + h.timeoutManager.Put(reqID, false, func() { + h.HandleInbound(context.Background(), inMsg) + }) +} + +// RegisterResponse registers an AppResponse with the timeout manager +func (h *RelayerExternalHandler) RegisterResponse(reqID ids.RequestID) { + h.timeoutManager.Remove(reqID) +} diff --git a/relayer/canonical_validator_client.go b/relayer/canonical_validator_client.go new file mode 100644 index 00000000..5a2e369d --- /dev/null +++ b/relayer/canonical_validator_client.go @@ -0,0 +1,43 @@ +// Copyright (C) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package relayer + +import ( + "context" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow/validators" + "github.com/ava-labs/avalanchego/vms/platformvm" +) + +// CanonicalValidatorClient wraps platformvm.Client and implements validators.State +type CanonicalValidatorClient struct { + client platformvm.Client +} + +func NewCanonicalValidatorClient(client platformvm.Client) *CanonicalValidatorClient { + return &CanonicalValidatorClient{ + client: client, + } +} + +func (v *CanonicalValidatorClient) GetMinimumHeight(ctx context.Context) (uint64, error) { + return v.client.GetHeight(ctx) +} + +func (v *CanonicalValidatorClient) GetCurrentHeight(ctx context.Context) (uint64, error) { + return v.client.GetHeight(ctx) +} + +func (v *CanonicalValidatorClient) GetSubnetID(ctx context.Context, chainID ids.ID) (ids.ID, error) { + return v.client.ValidatedBy(ctx, chainID) +} + +func (v *CanonicalValidatorClient) GetValidatorSet( + ctx context.Context, + height uint64, + subnetID ids.ID, +) (map[ids.NodeID]*validators.GetValidatorOutput, error) { + return v.client.GetValidatorsAt(ctx, subnetID, height) +} diff --git a/relayer/message_relayer.go b/relayer/message_relayer.go new file mode 100644 index 00000000..4d6dc0af --- /dev/null +++ b/relayer/message_relayer.go @@ -0,0 +1,542 @@ +// Copyright (C) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package relayer + +import ( + "bytes" + "context" + "fmt" + "math/big" + "time" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/message" + "github.com/ava-labs/avalanchego/proto/pb/p2p" + "github.com/ava-labs/avalanchego/subnets" + "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/crypto/bls" + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/avalanchego/utils/set" + "github.com/ava-labs/avalanchego/vms/platformvm/warp" + "github.com/ava-labs/awm-relayer/messages" + "github.com/ava-labs/awm-relayer/peers" + "github.com/ava-labs/awm-relayer/utils" + "github.com/ava-labs/awm-relayer/vms/vmtypes" + msg "github.com/ava-labs/subnet-evm/plugin/evm/message" + "go.uber.org/zap" +) + +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 + signatureRequestRetryWaitPeriodMs = 10_000 +) + +var ( + codec = msg.Codec + // Errors + errNotEnoughSignatures = fmt.Errorf("failed to collect a threshold of signatures") +) + +// messageRelayers are created for each warp message to be relayed. +// They collect signatures from validators, aggregate them, +// and send the signed warp message to the destination chain. +// Each messageRelayer runs in its own goroutine. +type messageRelayer struct { + relayer *Relayer + warpMessage *warp.UnsignedMessage + destinationChainID ids.ID + messageResponseChan chan message.InboundMessage + logger logging.Logger + messageCreator message.Creator + metrics *MessageRelayerMetrics +} + +func newMessageRelayer( + logger logging.Logger, + metrics *MessageRelayerMetrics, + relayer *Relayer, + warpMessage *warp.UnsignedMessage, + destinationChainID ids.ID, + messageResponseChan chan message.InboundMessage, + messageCreator message.Creator, +) *messageRelayer { + return &messageRelayer{ + relayer: relayer, + warpMessage: warpMessage, + destinationChainID: destinationChainID, + messageResponseChan: messageResponseChan, + logger: logger, + metrics: metrics, + messageCreator: messageCreator, + } +} + +func (r *messageRelayer) run(warpMessageInfo *vmtypes.WarpMessageInfo, requestID uint32, messageManager messages.MessageManager) { + shouldSend, err := messageManager.ShouldSendMessage(warpMessageInfo, r.destinationChainID) + if err != nil { + r.logger.Error( + "Failed to check if message should be sent", + zap.Error(err), + ) + + r.incFailedRelayMessageCount("failed to check if message should be sent") + + r.relayer.errorChan <- err + return + } + if !shouldSend { + r.logger.Info("Message should not be sent") + return + } + + startCreateSignedMessageTime := time.Now() + // Query nodes on the origin chain for signatures, and construct the signed warp message. + signedMessage, err := r.createSignedMessage(requestID) + if err != nil { + r.logger.Error( + "Failed to create signed warp message", + zap.Error(err), + ) + r.incFailedRelayMessageCount("failed to create signed warp message") + r.relayer.errorChan <- err + return + } + + // create signed message latency (ms) + r.setCreateSignedMessageLatencyMS(float64(time.Since(startCreateSignedMessageTime).Milliseconds())) + + err = messageManager.SendMessage(signedMessage, warpMessageInfo.WarpPayload, r.destinationChainID) + if err != nil { + r.logger.Error( + "Failed to send warp message", + zap.Error(err), + ) + r.incFailedRelayMessageCount("failed to send warp message") + r.relayer.errorChan <- err + return + } + r.logger.Info( + "Finished relaying message to destination chain", + zap.String("destinationChainID", r.destinationChainID.String()), + ) + r.incSuccessfulRelayMessageCount() +} + +// Run collects signatures from nodes by directly querying them via AppRequest, then aggregates the signatures, and constructs the signed warp message. +func (r *messageRelayer) createSignedMessage(requestID uint32) (*warp.Message, error) { + r.logger.Info( + "Starting relayer routine", + zap.String("destinationChainID", r.destinationChainID.String()), + ) + + // Get the current canonical validator set of the source subnet. + validatorSet, totalValidatorWeight, err := r.getCurrentCanonicalValidatorSet() + if err != nil { + r.logger.Error( + "Failed to get the canonical subnet validator set", + zap.String("subnetID", r.relayer.sourceSubnetID.String()), + zap.Error(err), + ) + return nil, err + } + + // We make queries to node IDs, not unique validators as represented by a BLS pubkey, so we need this map to track + // responses from nodes and populate the signatureMap with the corresponding validator signature + // This maps node IDs to the index in the canonical validator set + nodeValidatorIndexMap := make(map[ids.NodeID]int) + for i, vdr := range validatorSet { + for _, node := range vdr.NodeIDs { + nodeValidatorIndexMap[node] = i + } + } + + // Manually connect to all peers in the validator set + // If new peers are connected, AppRequests may fail while the handshake is in progress. + // In that case, AppRequests to those nodes will be retried in the next iteration of the retry loop. + nodeIDs := set.NewSet[ids.NodeID](len(nodeValidatorIndexMap)) + for node, _ := range nodeValidatorIndexMap { + nodeIDs.Add(node) + } + + // TODO: We may still be able to proceed with signature aggregation even if we fail to connect to some peers. + // We should check if the connected set represents sufficient stake, and continue if so. + _, err = r.relayer.network.ConnectPeers(nodeIDs) + if err != nil { + r.logger.Error( + "Failed to connect to peers", + zap.Error(err), + ) + return nil, err + } + + // Construct the request + req := msg.SignatureRequest{ + MessageID: r.warpMessage.ID(), + } + reqBytes, err := msg.RequestToBytes(codec, req) + if err != nil { + r.logger.Error( + "Failed to marshal request bytes", + zap.String("destinationChainID", r.destinationChainID.String()), + zap.Error(err), + ) + return nil, err + } + + outMsg, err := r.messageCreator.AppRequest(r.warpMessage.SourceChainID, requestID, peers.DefaultAppRequestTimeout, reqBytes) + if err != nil { + r.logger.Error( + "Failed to create app request message", + zap.Error(err), + ) + return nil, err + } + + // Query the validators with retries. On each retry, query one node per unique BLS pubkey + accumulatedSignatureWeight := big.NewInt(0) + + signatureMap := make(map[int]blsSignatureBuf) + + for attempt := 1; attempt <= maxRelayerQueryAttempts; attempt++ { + responsesExpected := len(validatorSet) - len(signatureMap) + r.logger.Debug( + "Relayer collecting signatures from peers.", + zap.Int("attempt", attempt), + zap.String("destinationChainID", r.destinationChainID.String()), + zap.Int("validatorSetSize", len(validatorSet)), + zap.Int("signatureMapSize", len(signatureMap)), + zap.Int("responsesExpected", responsesExpected), + ) + + vdrSet := set.NewSet[ids.NodeID](len(validatorSet)) + for i, vdr := range validatorSet { + // If we already have the signature for this validator, do not query any of the composite nodes again + if _, ok := signatureMap[i]; ok { + continue + } + + // TODO: Track failures and iterate through the validator's node list on subsequent query attempts + nodeID := vdr.NodeIDs[0] + vdrSet.Add(nodeID) + r.logger.Debug( + "Added node ID to query.", + zap.String("nodeID", nodeID.String()), + ) + + // Register a timeout response for each queried node + reqID := ids.RequestID{ + NodeID: nodeID, + SourceChainID: r.warpMessage.SourceChainID, + DestinationChainID: r.warpMessage.SourceChainID, + RequestID: requestID, + Op: byte(message.AppResponseOp), + } + r.relayer.network.Handler.RegisterRequest(reqID) + } + + sentTo := r.relayer.network.Network.Send(outMsg, vdrSet, r.relayer.sourceSubnetID, subnets.NoOpAllower) + r.logger.Debug( + "Sent signature request to network", + zap.String("messageID", req.MessageID.String()), + zap.Any("sentTo", sentTo), + ) + for nodeID := range vdrSet { + if !sentTo.Contains(nodeID) { + r.logger.Warn( + "Failed to make async request to node", + zap.String("nodeID", nodeID.String()), + zap.Error(err), + ) + responsesExpected-- + } + } + + responseCount := 0 + if responsesExpected > 0 { + // Handle the responses. For each response, we need to call response.OnFinishedHandling() exactly once. + // Wrap the loop body in an anonymous function so that we do so on each loop iteration + for response := range r.messageResponseChan { + r.logger.Debug( + "Processing response from node", + zap.String("nodeID", response.NodeID().String()), + ) + // This anonymous function attempts to create a signed warp message from the accumulated responses + // Returns an error only if a non-recoverable error occurs, otherwise returns (nil, nil) to continue processing responses + // When a non-nil signedMsg is returned, createSignedMessage itself returns + signedMsg, err := func() (*warp.Message, error) { + defer response.OnFinishedHandling() + + // Check if this is an expected response. + m := response.Message() + rcvReqID, ok := message.GetRequestID(m) + if !ok { + // This should never occur, since inbound message validity is already checked by the inbound handler + r.logger.Error("Could not get requestID from message") + return nil, nil + } + nodeID := response.NodeID() + if !sentTo.Contains(nodeID) || rcvReqID != requestID { + r.logger.Debug("Skipping irrelevant app response") + return nil, nil + } + + // Count the relevant app message + responseCount++ + + // If we receive an AppRequestFailed, then the request timed out. + // We still want to increment responseCount, since we are no longer expecting a response from that node. + if response.Op() == message.AppRequestFailedOp { + r.logger.Debug("Request timed out") + return nil, nil + } + + validator := validatorSet[nodeValidatorIndexMap[nodeID]] + signature, valid := r.isValidSignatureResponse(response, validator.PublicKey) + if valid { + r.logger.Debug( + "Got valid signature response", + zap.String("nodeID", nodeID.String()), + ) + signatureMap[nodeValidatorIndexMap[nodeID]] = signature + accumulatedSignatureWeight.Add(accumulatedSignatureWeight, new(big.Int).SetUint64(validator.Weight)) + } else { + r.logger.Debug( + "Got invalid signature response", + zap.String("nodeID", nodeID.String()), + ) + return nil, nil + } + + // As soon as the signatures exceed the stake weight threshold we try to aggregate and send the transaction. + if utils.CheckStakeWeightExceedsThreshold(accumulatedSignatureWeight, totalValidatorWeight) { + aggSig, vdrBitSet, err := r.aggregateSignatures(signatureMap) + if err != nil { + r.logger.Error( + "Failed to aggregate signature.", + zap.String("destinationChainID", r.destinationChainID.String()), + zap.Error(err), + ) + return nil, err + } + + signedMsg, err := warp.NewMessage(r.warpMessage, &warp.BitSetSignature{ + Signers: vdrBitSet.Bytes(), + Signature: *(*[bls.SignatureLen]byte)(bls.SignatureToBytes(aggSig)), + }) + if err != nil { + r.logger.Error( + "Failed to create new signed message", + zap.Error(err), + ) + return nil, err + } + return signedMsg, nil + } + // Not enough signatures, continue processing messages + return nil, nil + }() + if err != nil { + return nil, err + } + // If we have sufficient signatures, return here. + if signedMsg != nil { + r.logger.Info( + "Created signed message.", + zap.String("destinationChainID", r.destinationChainID.String()), + ) + return signedMsg, nil + } + // Break once we've had successful or unsuccessful responses from each requested node + if responseCount == responsesExpected { + break + } + } + } + if attempt != maxRelayerQueryAttempts { + // Sleep such that all retries are uniformly spread across totalRelayerQueryPeriodMs + // TODO: We may want to consider an exponential back off rather than a uniform sleep period. + time.Sleep(time.Duration(signatureRequestRetryWaitPeriodMs/maxRelayerQueryAttempts) * time.Millisecond) + } + } + + r.logger.Warn( + "Failed to collect a threshold of signatures", + zap.Int("attempts", maxRelayerQueryAttempts), + zap.String("destinationChainID", r.destinationChainID.String()), + ) + return nil, errNotEnoughSignatures +} + +func (r *messageRelayer) getCurrentCanonicalValidatorSet() ([]*warp.Validator, uint64, error) { + var ( + signingSubnet ids.ID + err error + ) + if r.relayer.sourceSubnetID == constants.PrimaryNetworkID { + // If the message originates from the primary subnet, then we instead "self sign" the message using the validators of the destination subnet. + signingSubnet, err = r.relayer.pChainClient.ValidatedBy(context.Background(), r.destinationChainID) + if err != nil { + r.logger.Error( + "failed to get validating subnet for destination chain", + zap.String("destinationChainID", r.destinationChainID.String()), + zap.Error(err), + ) + return nil, 0, err + } + } else { + // Otherwise, the source subnet signs the message. + signingSubnet = r.relayer.sourceSubnetID + } + + height, err := r.relayer.pChainClient.GetHeight(context.Background()) + if err != nil { + r.logger.Error( + "Failed to get P-Chain height", + zap.Error(err), + ) + return nil, 0, err + } + + // Get the current canonical validator set of the source subnet. + canonicalSubnetValidators, totalValidatorWeight, err := warp.GetCanonicalValidatorSet( + context.Background(), + r.relayer.canonicalValidatorClient, + height, + signingSubnet, + ) + if err != nil { + r.logger.Error( + "Failed to get the canonical subnet validator set", + zap.String("subnetID", r.relayer.sourceSubnetID.String()), + zap.Error(err), + ) + return nil, 0, err + } + + return canonicalSubnetValidators, totalValidatorWeight, 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. +func (r *messageRelayer) isValidSignatureResponse( + response message.InboundMessage, + pubKey *bls.PublicKey) (blsSignatureBuf, bool) { + + // If the handler returned an error response, count the response and continue + if response.Op() == message.AppRequestFailedOp { + r.logger.Debug( + "Relayer async response failed", + zap.String("nodeID", response.NodeID().String()), + ) + return blsSignatureBuf{}, false + } + + appResponse, ok := response.Message().(*p2p.AppResponse) + if !ok { + r.logger.Debug( + "Relayer async response was not an AppResponse", + zap.String("nodeID", response.NodeID().String()), + ) + return blsSignatureBuf{}, false + } + + var sigResponse msg.SignatureResponse + if _, err := msg.Codec.Unmarshal(appResponse.AppBytes, &sigResponse); err != nil { + r.logger.Error( + "Error unmarshaling signature response", + zap.Error(err), + ) + } + signature := sigResponse.Signature + + // If the node returned an empty signature, then it has not yet seen the warp message. Retry later. + emptySignature := blsSignatureBuf{} + if bytes.Equal(signature[:], emptySignature[:]) { + r.logger.Debug( + "Response contained an empty signature", + zap.String("nodeID", response.NodeID().String()), + zap.String("destinationChainID", r.destinationChainID.String()), + ) + return blsSignatureBuf{}, false + } + + sig, err := bls.SignatureFromBytes(signature[:]) + if err != nil { + r.logger.Debug( + "Failed to create signature from response", + zap.String("destinationChainID", r.destinationChainID.String()), + ) + return blsSignatureBuf{}, false + } + + if !bls.Verify(pubKey, sig, r.warpMessage.Bytes()) { + r.logger.Debug( + "Failed verification for signature", + zap.String("destinationChainID", r.destinationChainID.String()), + ) + return blsSignatureBuf{}, false + } + + 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 *messageRelayer) aggregateSignatures(signatureMap map[int]blsSignatureBuf) (*bls.Signature, set.Bits, error) { + // Aggregate the signatures + signatures := make([]*bls.Signature, 0, len(signatureMap)) + vdrBitSet := set.NewBits() + + for i, sigBytes := range signatureMap { + sig, err := bls.SignatureFromBytes(sigBytes[:]) + if err != nil { + r.logger.Error( + "Failed to unmarshal signature", + zap.Error(err), + ) + return nil, set.Bits{}, err + } + signatures = append(signatures, sig) + vdrBitSet.Add(i) + } + + aggSig, err := bls.AggregateSignatures(signatures) + if err != nil { + r.logger.Error( + "Failed to aggregate signatures", + zap.Error(err), + ) + return nil, set.Bits{}, err + } + return aggSig, vdrBitSet, nil +} + +func (r *messageRelayer) incSuccessfulRelayMessageCount() { + r.metrics.successfulRelayMessageCount. + WithLabelValues( + r.destinationChainID.String(), + r.relayer.sourceChainID.String(), + r.relayer.sourceSubnetID.String()).Inc() +} + +func (r *messageRelayer) incFailedRelayMessageCount(failureReason string) { + r.metrics.failedRelayMessageCount. + WithLabelValues( + r.destinationChainID.String(), + r.relayer.sourceChainID.String(), + r.relayer.sourceSubnetID.String(), + failureReason).Inc() +} + +func (r *messageRelayer) setCreateSignedMessageLatencyMS(latency float64) { + r.metrics.createSignedMessageLatencyMS. + WithLabelValues( + r.destinationChainID.String(), + r.relayer.sourceChainID.String(), + r.relayer.sourceSubnetID.String()).Set(latency) +} diff --git a/relayer/message_relayer_metrics.go b/relayer/message_relayer_metrics.go new file mode 100644 index 00000000..93a6fb89 --- /dev/null +++ b/relayer/message_relayer_metrics.go @@ -0,0 +1,47 @@ +// Copyright (C) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package relayer + +import "github.com/prometheus/client_golang/prometheus" + +type MessageRelayerMetrics struct { + successfulRelayMessageCount *prometheus.CounterVec + createSignedMessageLatencyMS *prometheus.GaugeVec + failedRelayMessageCount *prometheus.CounterVec +} + +func NewMessageRelayerMetrics(registerer prometheus.Registerer) *MessageRelayerMetrics { + successfulRelayMessageCount := prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "successful_relay_message_count", + Help: "Number of messages that relayed successfully", + }, + []string{"destination_chain_id", "source_chain_id", "source_subnet_id"}, + ) + registerer.MustRegister(successfulRelayMessageCount) + + createSignedMessageLatencyMS := prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "create_signed_message_latency_ms", + Help: "Latency of creating a signed message in milliseconds", + }, + []string{"destination_chain_id", "source_chain_id", "source_subnet_id"}, + ) + registerer.MustRegister(createSignedMessageLatencyMS) + + failedRelayMessageCount := prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "failed_relay_message_count", + Help: "Number of messages that failed to relay", + }, + []string{"destination_chain_id", "source_chain_id", "source_subnet_id", "failure_reason"}, + ) + registerer.MustRegister(failedRelayMessageCount) + + return &MessageRelayerMetrics{ + successfulRelayMessageCount: successfulRelayMessageCount, + createSignedMessageLatencyMS: createSignedMessageLatencyMS, + failedRelayMessageCount: failedRelayMessageCount, + } +} diff --git a/relayer/relayer.go b/relayer/relayer.go new file mode 100644 index 00000000..fb22b030 --- /dev/null +++ b/relayer/relayer.go @@ -0,0 +1,202 @@ +// Copyright (C) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package relayer + +import ( + "math/rand" + "sync" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/message" + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/avalanchego/vms/platformvm" + "github.com/ava-labs/awm-relayer/config" + "github.com/ava-labs/awm-relayer/messages" + "github.com/ava-labs/awm-relayer/peers" + vms "github.com/ava-labs/awm-relayer/vms" + "github.com/ava-labs/awm-relayer/vms/vmtypes" + "github.com/ethereum/go-ethereum/common" + "go.uber.org/zap" +) + +// Relayer handles all messages sent from a given source chain +type Relayer struct { + pChainClient platformvm.Client + canonicalValidatorClient *CanonicalValidatorClient + currentRequestID uint32 + network *peers.AppRequestNetwork + sourceSubnetID ids.ID + sourceChainID ids.ID + responseChan <-chan message.InboundMessage + messageChanLock *sync.RWMutex + messageChanMap map[uint32]chan message.InboundMessage + errorChan chan error + contractMessage vms.ContractMessage + messageManagers map[common.Hash]messages.MessageManager + logger logging.Logger +} + +func NewRelayer( + logger logging.Logger, + sourceSubnetInfo config.SourceSubnet, + errorChan chan error, + pChainClient platformvm.Client, + network *peers.AppRequestNetwork, + responseChan chan message.InboundMessage, + destinationClients map[ids.ID]vms.DestinationClient, +) (*Relayer, vms.Subscriber, error) { + + sub := vms.NewSubscriber(logger, sourceSubnetInfo) + + subnetID, err := ids.FromString(sourceSubnetInfo.SubnetID) + if err != nil { + logger.Error( + "Invalid subnetID in configuration", + zap.Error(err), + ) + return nil, nil, err + } + + chainID, err := ids.FromString(sourceSubnetInfo.ChainID) + if err != nil { + logger.Error( + "Failed to decode base-58 encoded source chain ID", + zap.Error(err), + ) + return nil, nil, err + } + + // Create message managers for each supported message protocol + messageManagers := make(map[common.Hash]messages.MessageManager) + for address, config := range sourceSubnetInfo.MessageContracts { + addressHash := common.HexToHash(address) + messageManager, err := messages.NewMessageManager(logger, addressHash, config, destinationClients) + if err != nil { + logger.Error( + "Failed to create message manager", + zap.Error(err), + ) + return nil, nil, err + } + messageManagers[addressHash] = messageManager + } + + logger.Info( + "Creating relayer", + zap.String("subnetID", subnetID.String()), + zap.String("subnetIDHex", subnetID.Hex()), + zap.String("chainID", chainID.String()), + zap.String("chainIDHex", chainID.Hex()), + ) + r := Relayer{ + pChainClient: pChainClient, + canonicalValidatorClient: NewCanonicalValidatorClient(pChainClient), + currentRequestID: rand.Uint32(), // Initialize to a random value to mitigate requestID collision + network: network, + sourceSubnetID: subnetID, + sourceChainID: chainID, + responseChan: responseChan, + messageChanLock: new(sync.RWMutex), + messageChanMap: make(map[uint32]chan message.InboundMessage), + errorChan: errorChan, + contractMessage: vms.NewContractMessage(logger, sourceSubnetInfo), + messageManagers: messageManagers, + logger: logger, + } + + // Start the message router. We must do this before Subscribing for the first time, otherwise we may miss an incoming message + go r.RouteToMessageChannel() + + err = sub.Subscribe() + if err != nil { + logger.Error( + "Failed to subscribe to node", + zap.Error(err), + ) + return nil, nil, err + } + return &r, sub, nil +} + +// RouteToMessageChannel forwards inbound app messages from the per-subnet responseChan to the per-message messageResponseChan +// The messageResponseChan to forward to is determined by the requestID, which is unique to each message relayer goroutine +// If the requestID has not been added to the map, then the message is dropped. +func (r *Relayer) RouteToMessageChannel() { + for response := range r.responseChan { + m := response.Message() + requestID, ok := message.GetRequestID(m) + if !ok { + response.OnFinishedHandling() + return + } + r.messageChanLock.RLock() + messageResponseChan, ok := r.messageChanMap[requestID] + r.messageChanLock.RUnlock() + if ok { + messageResponseChan <- response + } else { + response.OnFinishedHandling() + } + } +} + +// RelayMessage relays a single warp message to the destination chain. Warp message relay requests are concurrent with each other, +// and synchronized by relayer. +func (r *Relayer) RelayMessage(warpLogInfo *vmtypes.WarpLogInfo, metrics *MessageRelayerMetrics, messageCreator message.Creator) error { + // Unpack the VM message bytes into a Warp message + warpMessageInfo, err := r.contractMessage.UnpackWarpMessage(warpLogInfo.UnsignedMsgBytes) + if err != nil { + r.logger.Error( + "Failed to unpack sender message", + zap.Error(err), + ) + return err + } + + // Check that the warp message is from a support message protocol contract address. + messageManager, supportedMessageProtocol := r.messageManagers[warpLogInfo.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 relayer instance. + r.logger.Debug( + "Warp message from unsupported message protocol address. not relaying.", + zap.String("protocolAddress", warpLogInfo.SourceAddress.Hex()), + ) + return nil + } + + requestID := r.getAndIncrementRequestID() + + // Add an element to the inbound messages map, keyed by the requestID + // This allows RouteToMessageChannel to forward inbound messages to the correct channel + // Note: It's technically possible to block indefinitely here if the inbound handler is processing an flood of non-relevant app responses. This would occur if + // the RLocks are acquired more quickly than they are released in RouteToMessageChannel. However, given that inbound messages are rate limited and that the + // RLock is only held for a single map access, this is likely not something we need to worry about in practice. + messageResponseChan := make(chan message.InboundMessage, peers.InboundMessageChannelSize) + r.messageChanLock.Lock() + r.messageChanMap[requestID] = messageResponseChan + r.messageChanLock.Unlock() + + // Create and run the message relayer to attempt to deliver the message to the destination chain + messageRelayer := newMessageRelayer(r.logger, metrics, r, warpMessageInfo.WarpUnsignedMessage, warpLogInfo.DestinationChainID, messageResponseChan, messageCreator) + if err != nil { + r.logger.Error( + "Failed to create message relayer", + zap.Error(err), + ) + return err + } + + go messageRelayer.run(warpMessageInfo, requestID, messageManager) + + return nil +} + +// Get the current app request ID and increment it. Request IDs need to be unique to each teleporter message, but the specific values do not matter. +// This should only be called by the subnet-level TeleporterRelayer, and not by the goroutines that handle individual teleporter messages +func (r *Relayer) getAndIncrementRequestID() uint32 { + id := r.currentRequestID + r.currentRequestID++ + return id +} diff --git a/relayer/relayer_test.go b/relayer/relayer_test.go new file mode 100644 index 00000000..3407b77b --- /dev/null +++ b/relayer/relayer_test.go @@ -0,0 +1,24 @@ +// Copyright (C) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package relayer + +import ( + "testing" + + "github.com/ava-labs/awm-relayer/config" + "github.com/stretchr/testify/require" +) + +func TestGetRelayerAccountInfoSkipChainConfigCheckCompatible(t *testing.T) { + accountPrivateKey := "56289e99c94b6912bfc12adc093c9b51124f0dc54ac7a766b2bc5ccf558d8027" + expectedAddress := "0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC" + + info := config.DestinationSubnet{ + AccountPrivateKey: accountPrivateKey, + } + _, address, err := info.GetRelayerAccountInfo() + + require.NoError(t, err) + require.Equal(t, expectedAddress, address.String()) +} diff --git a/resources/relayer-diagram.png b/resources/relayer-diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..5199474b0c97c4e088cd904512c47b9cca5f7269 GIT binary patch literal 184867 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

%Uh*#>Ojj2%9lyH~_l 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 literal 0 HcmV?d00001 diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 00000000..322ff9d5 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# Copyright (C) 2023, Ava Labs, Inc. All rights reserved. +# See the file LICENSE for licensing terms. + +set -o errexit +set -o nounset +set -o pipefail + +go_version_minimum="1.18.1" + +go_version() { + go version | sed -nE -e 's/[^0-9.]+([0-9.]+).+/\1/p' +} + +version_lt() { + # Return true if $1 is a lower version than than $2, + local ver1=$1 + local ver2=$2 + # Reverse sort the versions, if the 1st item != ver1 then ver1 < ver2 + if [[ $(echo -e -n "$ver1\n$ver2\n" | sort -rV | head -n1) != "$ver1" ]]; then + return 0 + else + return 1 + fi +} + +if version_lt "$(go_version)" "$go_version_minimum"; then + echo "awm-relayer requires Go >= $go_version_minimum, Go $(go_version) found." >&2 + exit 1 +fi + +# Root directory +AWM_RELAYER_PATH=$( + cd "$(dirname "${BASH_SOURCE[0]}")" + cd .. && pwd +) + +# Load the versions +source "$AWM_RELAYER_PATH"/scripts/versions.sh + +if [[ $# -eq 1 ]]; then + binary_path=$1 +elif [[ $# -eq 0 ]]; then + binary_path="build/awm-relayer" +else + echo "Invalid arguments to build awm-relayer. Requires zero (default location) or one argument to specify binary location." + exit 1 +fi + +# Build AWM Relayer, which is run as a standalone process +echo "Building AWM Relayer Version: $awm_relayer_version at $binary_path" +go build -o "$binary_path" "main/"*.go diff --git a/scripts/build_local_image.sh b/scripts/build_local_image.sh new file mode 100755 index 00000000..71a8fa0f --- /dev/null +++ b/scripts/build_local_image.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# Copyright (C) 2023, Ava Labs, Inc. All rights reserved. +# See the file LICENSE for licensing terms. + +set -o errexit +set -o nounset +set -o pipefail + +# Directory above this script +RELAYER_PATH=$( cd "$( dirname "${BASH_SOURCE[0]}" )"; cd .. && pwd ) + +# Load the constants +source "$RELAYER_PATH"/scripts/constants.sh + +# WARNING: this will use the most recent commit even if there are un-committed changes present +full_commit_hash="$(git --git-dir="$RELAYER_PATH/.git" rev-parse HEAD)" +commit_hash="${full_commit_hash::8}" + +echo "Building Docker Image with tags: $relayer_dockerhub_repo:$commit_hash , $relayer_dockerhub_repo:$current_branch" +docker build -t "$relayer_dockerhub_repo:$commit_hash" \ + -t "$relayer_dockerhub_repo:$current_branch" \ + "$RELAYER_PATH" -f "$RELAYER_PATH/Dockerfile" diff --git a/scripts/constants.sh b/scripts/constants.sh new file mode 100755 index 00000000..14a0d5e6 --- /dev/null +++ b/scripts/constants.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# Copyright (C) 2023, Ava Labs, Inc. All rights reserved. +# See the file LICENSE for licensing terms. + +# Use lower_case variables in the scripts and UPPER_CASE variables for override +# Use the constants.sh for env overrides + +RELAYER_PATH=$( cd "$( dirname "${BASH_SOURCE[0]}" )"; cd .. && pwd ) # Directory above this script + +# Where AWM Relayer binary goes +relayer_path="$RELAYER_PATH/build/awm-relayer" + +# Set the PATHS +GOPATH="$(go env GOPATH)" + +# Avalabs docker hub repo is avaplatform/awm-relayer. +# Here we default to the local image (awm-relayer) as to avoid unintentional pushes +# You should probably set it - export DOCKER_REPO='avaplatform/awm-relayer' +relayer_dockerhub_repo=${DOCKER_REPO:-"awm-relayer"} + +# Current branch +current_branch=$(git symbolic-ref -q --short HEAD || git describe --tags --exact-match || true) + +git_commit=${RELAYER_COMMIT:-$( git rev-list -1 HEAD )} diff --git a/scripts/versions.sh b/scripts/versions.sh new file mode 100755 index 00000000..8ea7ce5f --- /dev/null +++ b/scripts/versions.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# Copyright (C) 2023, Ava Labs, Inc. All rights reserved. +# See the file LICENSE for licensing terms. + +# Set up the versions to be used +awm_relayer_version=${AWM_RELAYER_VERSION:-'v0.0.1'} +subnet_evm_version=${SUBNET_EVM_VERSION:-'v0.5.2-warp-rc.0'} +# Don't export them as they're used in the context of other calls +avalanche_version=${AVALANCHE_VERSION:-'v1.10.2'} diff --git a/utils/utils.go b/utils/utils.go new file mode 100644 index 00000000..59dc31ba --- /dev/null +++ b/utils/utils.go @@ -0,0 +1,101 @@ +// Copyright (C) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package utils + +import ( + "errors" + "fmt" + "math/big" + "net/url" + + "github.com/ethereum/go-ethereum/common" +) + +const ( + // Default stake threshold for aggregate signature verification. (67%) + // TODO: This should be made configuration on the VM level. + DefaultQuorumNumerator = 67 + DefaultQuorumDenominator = 100 + + // TODO: Revisit these constant values once we are using the subnet-evm branch with finalized + // Warp implementation. Should evaluate the maximum gas used by the Teleporter contract "receiveCrossChainMessage" + // method, excluding the call to execute the message payload. + ReceiveCrossChainMessageStaticGasCost uint64 = 2_000_000 + ReceiveCrossChainMessageGasCostPerAggregatedKey uint64 = 1_000 +) + +var ( + Uint256Max = (&big.Int{}).SetBytes(common.Hex2Bytes("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF")) + + // Errors + ErrNilInput = errors.New("nil input") + ErrTooLarge = errors.New("exceeds uint256 maximum value") +) + +// +// AWM Utils +// + +// CheckStakeWeightExceedsThreshold returns true if the accumulated signature weight is at least [quorumNum]/[quorumDen] of [totalWeight]. +func CheckStakeWeightExceedsThreshold(accumulatedSignatureWeight *big.Int, totalWeight uint64) bool { + if accumulatedSignatureWeight == nil { + return false + } + + // Verifies that quorumNum * totalWeight <= quorumDen * sigWeight + scaledTotalWeight := new(big.Int).SetUint64(totalWeight) + scaledTotalWeight.Mul(scaledTotalWeight, new(big.Int).SetUint64(DefaultQuorumNumerator)) + scaledSigWeight := new(big.Int).Mul(accumulatedSignatureWeight, new(big.Int).SetUint64(DefaultQuorumDenominator)) + + thresholdMet := scaledTotalWeight.Cmp(scaledSigWeight) != 1 + return thresholdMet +} + +// +// Generic Utils +// + +// BigToHashSafe ensures that a bignum value is able to fit into a 32 byte buffer before converting it to a common.Hash +// Returns an error if overflow/truncation would occur by trying to perfom this operation. +func BigToHashSafe(in *big.Int) (common.Hash, error) { + if in == nil { + return common.Hash{}, ErrNilInput + } + + if in.Cmp(Uint256Max) > 0 { + return common.Hash{}, ErrTooLarge + } + + return common.BigToHash(in), nil +} + +func ConvertProtocol(URLString, protocol string) (string, error) { + var ( + u *url.URL + err error + ) + if u, err = url.ParseRequestURI(URLString); err != nil { + return "", fmt.Errorf("invalid url") + } + + u.Scheme = protocol + if _, err = url.ParseRequestURI(u.String()); err != nil { + return "", fmt.Errorf("invalid protocol") + } + + return u.String(), nil +} + +// SanitizeHashString removes the "0x" prefix from a hex hash string if it exists. +// Otherwise, returns the original string. +func SanitizeHashString(hash string) string { + if len(hash)%2 != 0 || len(hash) < 2 { + return hash + } + + if hash[:2] == "0x" { + return hash[2:] + } + return hash +} diff --git a/utils/utils_test.go b/utils/utils_test.go new file mode 100644 index 00000000..9b3a9156 --- /dev/null +++ b/utils/utils_test.go @@ -0,0 +1,88 @@ +// Copyright (C) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package utils + +import "testing" + +func TestConvertProtocol(t *testing.T) { + testCases := []struct { + urlString string + protocol string + expectedUrl string + expectedError bool + }{ + { + urlString: "http://www.hello.com", + protocol: "https", + expectedUrl: "https://www.hello.com", + expectedError: false, + }, + { + urlString: "https://www.hello.com", + protocol: "http", + expectedUrl: "http://www.hello.com", + expectedError: false, + }, + { + urlString: "http://www.hello.com", + protocol: "http", + expectedUrl: "http://www.hello.com", + expectedError: false, + }, + { + urlString: "https://www.hello.com", + protocol: "https", + expectedUrl: "https://www.hello.com", + expectedError: false, + }, + { + urlString: "http://www.hello.com", + protocol: "\n", + expectedUrl: "", + expectedError: true, + }, + } + + for i, testCase := range testCases { + actualUrl, err := ConvertProtocol(testCase.urlString, testCase.protocol) + if err != nil && !testCase.expectedError { + t.Errorf("test case %d failed with unexpected error", i) + } + if err == nil && testCase.expectedError { + t.Errorf("test case %d did not produce expected error", i) + } + if actualUrl != testCase.expectedUrl { + t.Errorf("test case %d had unexpected URL. Actual: %s, Expected: %s", i, actualUrl, testCase.expectedUrl) + } + } +} + +func TestSanitizeHashString(t *testing.T) { + testCases := []struct { + hash string + expectedResult string + }{ + // Remove leading 0x from hex string + { + hash: "0x1234", + expectedResult: "1234", + }, + // Return original hex string + { + hash: "1234", + expectedResult: "1234", + }, + // Return original string, leading 0x is not hex + { + hash: "0x1234g", + expectedResult: "0x1234g", + }, + } + for i, testCase := range testCases { + actualResult := SanitizeHashString(testCase.hash) + if actualResult != testCase.expectedResult { + t.Errorf("test case %d had unexpected result. Actual: %s, Expected: %s", i, actualResult, testCase.expectedResult) + } + } +} diff --git a/vms/contract_message.go b/vms/contract_message.go new file mode 100644 index 00000000..3cb9fdb7 --- /dev/null +++ b/vms/contract_message.go @@ -0,0 +1,25 @@ +// Copyright (C) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package vms + +import ( + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/awm-relayer/config" + "github.com/ava-labs/awm-relayer/vms/evm" + "github.com/ava-labs/awm-relayer/vms/vmtypes" +) + +type ContractMessage interface { + // UnpackWarpMessage unpacks the warp message from the VM + UnpackWarpMessage(unsignedMsgBytes []byte) (*vmtypes.WarpMessageInfo, error) +} + +func NewContractMessage(logger logging.Logger, subnetInfo config.SourceSubnet) ContractMessage { + switch config.ParseVM(subnetInfo.VM) { + case config.EVM: + return evm.NewContractMessage(logger, subnetInfo) + default: + return nil + } +} diff --git a/vms/destination_client.go b/vms/destination_client.go new file mode 100644 index 00000000..cec3b06b --- /dev/null +++ b/vms/destination_client.go @@ -0,0 +1,70 @@ +// Copyright (C) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package vms + +import ( + "fmt" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/avalanchego/vms/platformvm/warp" + "github.com/ava-labs/awm-relayer/config" + "github.com/ava-labs/awm-relayer/vms/evm" + "github.com/ethereum/go-ethereum/common" + "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 message relayers. +type DestinationClient interface { + // SendTx contructs the transaction from warp primitives, and send to the configured destination chain endpoint + // TODO: Make generic for any VM. + SendTx(signedMessage *warp.Message, toAddress string, gasLimit uint64, callData []byte) error + + // Allowed checks if the relayer is allowed to relay the message according to the VM rules and the message metadata + Allowed(chainID ids.ID, allowedRelayers []common.Address) bool +} + +func NewDestinationClient(logger logging.Logger, subnetInfo config.DestinationSubnet) (DestinationClient, error) { + switch config.ParseVM(subnetInfo.VM) { + case config.EVM: + return evm.NewDestinationClient(logger, subnetInfo) + default: + return nil, fmt.Errorf("invalid vm") + } +} + +// CreateDestinationClients creates destination clients for all subnets configured as destinations +func CreateDestinationClients(logger logging.Logger, relayerConfig config.Config) (map[ids.ID]DestinationClient, error) { + destinationClients := make(map[ids.ID]DestinationClient) + for _, s := range relayerConfig.DestinationSubnets { + chainID, err := ids.FromString(s.ChainID) + if err != nil { + logger.Error( + "Failed to decode base-58 encoded source chain ID", + zap.Error(err), + ) + return nil, err + } + if _, ok := destinationClients[chainID]; ok { + logger.Info( + "Destination client already found for chainID. Continuing", + zap.String("chainID", chainID.String()), + ) + continue + } + + destinationClient, err := NewDestinationClient(logger, s) + if err != nil { + logger.Error( + "Could not create destination client", + zap.Error(err), + ) + return nil, err + } + + destinationClients[chainID] = destinationClient + } + return destinationClients, nil +} diff --git a/vms/evm/contract_message.go b/vms/evm/contract_message.go new file mode 100644 index 00000000..ed95e374 --- /dev/null +++ b/vms/evm/contract_message.go @@ -0,0 +1,57 @@ +// Copyright (C) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package evm + +import ( + "github.com/ava-labs/avalanchego/utils/logging" + avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" + "github.com/ava-labs/awm-relayer/config" + "github.com/ava-labs/awm-relayer/vms/vmtypes" + warpPayload "github.com/ava-labs/subnet-evm/warp/payload" + "go.uber.org/zap" +) + +type contractMessage struct { + logger logging.Logger +} + +func NewContractMessage(logger logging.Logger, subnetInfo config.SourceSubnet) *contractMessage { + return &contractMessage{ + logger: logger, + } +} + +func (m *contractMessage) UnpackWarpMessage(unsignedMsgBytes []byte) (*vmtypes.WarpMessageInfo, error) { + unsignedMsg, err := avalancheWarp.ParseUnsignedMessage(unsignedMsgBytes) + if err != nil { + m.logger.Error( + "Failed parsing unsigned message", + zap.Error(err), + ) + return nil, err + } + err = unsignedMsg.Initialize() + if err != nil { + m.logger.Error( + "Failed initializing unsigned message", + zap.Error(err), + ) + return nil, err + } + + warpPayload, err := warpPayload.ParseAddressedPayload(unsignedMsg.Payload) + if err != nil { + m.logger.Error( + "Failed parsing addressed payload", + zap.Error(err), + ) + return nil, err + } + + messageInfo := vmtypes.WarpMessageInfo{ + WarpUnsignedMessage: unsignedMsg, + WarpPayload: warpPayload.Payload, + } + return &messageInfo, err +} diff --git a/vms/evm/contract_message_test.go b/vms/evm/contract_message_test.go new file mode 100644 index 00000000..476a45af --- /dev/null +++ b/vms/evm/contract_message_test.go @@ -0,0 +1,75 @@ +// Copyright (C) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package evm + +import ( + "encoding/hex" + "testing" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/avalanchego/vms/platformvm/warp" + "github.com/ava-labs/awm-relayer/config" + warpPayload "github.com/ava-labs/subnet-evm/warp/payload" + "github.com/ethereum/go-ethereum/common" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" +) + +// Used to create a valid unsigned message for testing. Should not be used directly in tests. +func createUnsignedMessage() *warp.UnsignedMessage { + sourceChainID, err := ids.FromString("yH8D7ThNJkxmtkuv2jgBa4P1Rn3Qpr4pPr7QYNfcdoS6k6HWp") + if err != nil { + return nil + } + destinationChainID, err := ids.FromString("11111111111111111111111111111111LpoYY") + if err != nil { + return nil + } + + payload, err := warpPayload.NewAddressedPayload( + common.HexToAddress("27aE10273D17Cd7e80de8580A51f476960626e5f"), + common.Hash(destinationChainID), + common.HexToAddress("1234123412341234123412341234123412341234"), + []byte{}, + ) + if err != nil { + return nil + } + + unsignedMsg, err := warp.NewUnsignedMessage(0, sourceChainID, payload.Bytes()) + if err != nil { + return nil + } + return unsignedMsg +} + +func TestUnpack(t *testing.T) { + ctrl := gomock.NewController(t) + + m := NewContractMessage(logging.NewMockLogger(ctrl), config.SourceSubnet{}) + + testCases := []struct { + input string + networkID uint32 + }{ + { + input: "0000000000007fc93d85c6d62c5b2ac0b519c87010ea5294012d1e407030d6acd0021cac10d50000005200000000000027ae10273d17cd7e80de8580a51f476960626e5f0000000000000000000000000000000000000000000000000000000000000000123412341234123412341234123412341234123400000000", + networkID: 0, + }, + } + + for _, testCase := range testCases { + input, err := hex.DecodeString(testCase.input) + if err != nil { + t.Errorf("failed to decode test input: %v", err) + } + msg, err := m.UnpackWarpMessage(input) + if err != nil { + t.Errorf("failed to unpack message: %v", err) + } + + assert.Equal(t, testCase.networkID, msg.WarpUnsignedMessage.NetworkID) + } +} diff --git a/vms/evm/destination_client.go b/vms/evm/destination_client.go new file mode 100644 index 00000000..57ade4ce --- /dev/null +++ b/vms/evm/destination_client.go @@ -0,0 +1,224 @@ +// Copyright (C) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package evm + +import ( + "context" + "crypto/ecdsa" + "math/big" + "runtime" + "sync" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/logging" + avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" + "github.com/ava-labs/awm-relayer/config" + "github.com/ava-labs/subnet-evm/core/types" + "github.com/ava-labs/subnet-evm/ethclient" + predicateutils "github.com/ava-labs/subnet-evm/utils/predicate" + "github.com/ava-labs/subnet-evm/x/warp" + "github.com/ethereum/go-ethereum/common" + "go.uber.org/zap" +) + +const ( + // Set the max fee to twice the estimated base fee. + // TODO: Revisit this constant factor when we add profit determination, or make it configurable + BaseFeeFactor = 2 + MaxPriorityFeePerGas = 2500000000 // 2.5 gwei +) + +// Implements DestinationClient +type destinationClient struct { + client ethclient.Client + lock *sync.Mutex + + destinationChainID ids.ID + pk *ecdsa.PrivateKey + eoa common.Address + currentNonce uint64 + logger logging.Logger +} + +func NewDestinationClient(logger logging.Logger, subnetInfo config.DestinationSubnet) (*destinationClient, error) { + // Dial the destination RPC endpoint + client, err := ethclient.Dial(subnetInfo.GetNodeRPCEndpoint()) + if err != nil { + logger.Error( + "Failed to dial rpc endpoint", + zap.Error(err), + ) + return nil, err + } + + destinationID, err := ids.FromString(subnetInfo.ChainID) + if err != nil { + logger.Error( + "Could not decode destination chain ID from string", + zap.Error(err), + ) + return nil, err + } + + pk, eoa, err := subnetInfo.GetRelayerAccountInfo() + if err != nil { + logger.Error( + "Could not extract relayer account information from config", + zap.Error(err), + ) + return nil, err + } + // Explicitly zero the private key when it is gc'd + runtime.SetFinalizer(pk, func(pk *ecdsa.PrivateKey) { + pk.D.SetInt64(0) + pk = nil + }) + + nonce, err := client.NonceAt(context.Background(), eoa, nil) + if err != nil { + logger.Error( + "Failed to get nonce", + zap.Error(err), + ) + return nil, err + } + + return &destinationClient{ + client: client, + lock: new(sync.Mutex), + destinationChainID: destinationID, + pk: pk, + eoa: eoa, + currentNonce: nonce, + logger: logger, + }, nil +} + +func (tdc *destinationClient) SendTx(signedMessage *avalancheWarp.Message, + toAddress string, + gasLimit uint64, + callData []byte) error { + // Synchronize teleporter message requests to the same destination chain so that message ordering is preserved + tdc.lock.Lock() + defer tdc.lock.Unlock() + // We need the global 32-byte representation of the destination chain ID, as well as the destination's configured chainID + // Without the destination's configured chainID, transaction signature verification will fail + destinationChainIDBigInt, err := tdc.client.ChainID(context.Background()) + if err != nil { + tdc.logger.Error( + "Failed to get chain ID from destination chain endpoint", + zap.Error(err), + ) + return err + } + + // Get the current base fee estimation, which is based on the previous blocks gas usage. + baseFee, err := tdc.client.EstimateBaseFee(context.Background()) + if err != nil { + tdc.logger.Error( + "Failed to get base fee", + zap.Error(err), + ) + return err + } + + // Get the suggested gas tip cap of the network + // TODO: Add a configurable ceiling to this value + gasTipCap, err := tdc.client.SuggestGasTipCap(context.Background()) + if err != nil { + tdc.logger.Error( + "Failed to get gas tip cap", + zap.Error(err), + ) + return err + } + + // Pack the signed message to be delivered in the storage slots. + // The predicate bytes are packed with a delimiter of 0xff. + predicateBytes := predicateutils.PackPredicate(signedMessage.Bytes()) + + to := common.HexToAddress(toAddress) + + gasFeeCap := baseFee.Mul(baseFee, big.NewInt(BaseFeeFactor)) + gasFeeCap.Add(gasFeeCap, big.NewInt(MaxPriorityFeePerGas)) + + // Construct the actual transaction to broadcast on the destination chain + tx := types.NewTx(&types.DynamicFeeTx{ + ChainID: destinationChainIDBigInt, + Nonce: tdc.currentNonce, + To: &to, + Gas: gasLimit, + GasFeeCap: gasFeeCap, + GasTipCap: gasTipCap, + Value: big.NewInt(0), + Data: callData, + AccessList: types.AccessList{ + { + Address: warp.ContractAddress, + StorageKeys: predicateutils.BytesToHashSlice(predicateBytes), + }, + }, + }) + + // Sign and send the transaction on the destination chain + signer := types.LatestSignerForChainID(destinationChainIDBigInt) + signedTx, err := types.SignTx(tx, signer, tdc.pk) + if err != nil { + tdc.logger.Error( + "Failed to sign transaction", + zap.Error(err), + ) + return err + } + + if err := tdc.client.SendTransaction(context.Background(), signedTx); err != nil { + tdc.logger.Error( + "Failed to send transaction", + zap.Error(err), + ) + return err + } + + // Increment the nonce to use on the destination chain now that we've sent + // a transaction using the current value. + tdc.currentNonce++ + tdc.logger.Info( + "Sent transaction", + zap.String("txID", signedTx.Hash().String()), + ) + + return nil +} + +func (tdc *destinationClient) isDestination(chainID ids.ID) bool { + if chainID != tdc.destinationChainID { + tdc.logger.Info( + "Destination chain ID for message not supported by relayer.", + zap.String("chainID", chainID.String()), + ) + return false + } + return true +} + +func (tdc *destinationClient) isAllowedRelayer(allowedRelayers []common.Address) bool { + // If no allowed relayer addresses were set, then anyone can relay it. + if len(allowedRelayers) == 0 { + return true + } + + for _, addr := range allowedRelayers { + if addr == tdc.eoa { + return true + } + } + + tdc.logger.Info("Relayer EOA not allowed to deliver this message.") + return false +} + +func (tdc *destinationClient) Allowed(chainID ids.ID, allowedRelayers []common.Address) bool { + return tdc.isDestination(chainID) && + tdc.isAllowedRelayer(allowedRelayers) +} diff --git a/vms/evm/subscriber.go b/vms/evm/subscriber.go new file mode 100644 index 00000000..2e003bd2 --- /dev/null +++ b/vms/evm/subscriber.go @@ -0,0 +1,165 @@ +// Copyright (C) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package evm + +import ( + "context" + "errors" + "fmt" + "time" + + "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/vms/vmtypes" + "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/x/warp" + "github.com/ethereum/go-ethereum/common" + "go.uber.org/zap" +) + +const ( + // Max buffer size for ethereum subscription channels + maxClientSubscriptionBuffer = 20000 + subscribeRetryTimeout = 1 * time.Second + maxResubscribeAttempts = 10 +) + +var ( + // Errors + ErrInvalidLog = errors.New("invalid warp message log") +) + +// subscriber implements Subscriber +type subscriber struct { + nodeURL string + log chan vmtypes.WarpLogInfo + evmLog <-chan types.Log + sub interfaces.Subscription + + logger logging.Logger +} + +// NewSubscriber returns a subscriber +func NewSubscriber(logger logging.Logger, subnetInfo config.SourceSubnet) *subscriber { + return &subscriber{ + nodeURL: subnetInfo.GetNodeWSEndpoint(), + logger: logger, + } +} + +func NewWarpLogInfo(log types.Log) (*vmtypes.WarpLogInfo, error) { + if len(log.Topics) != 4 { + return nil, ErrInvalidLog + } + if log.Topics[0] != warp.WarpABI.Events["SendWarpMessage"].ID { + return nil, ErrInvalidLog + } + destinationChainID, err := ids.ToID(log.Topics[1].Bytes()) + if err != nil { + return nil, ErrInvalidLog + } + + return &vmtypes.WarpLogInfo{ + DestinationChainID: destinationChainID, + DestinationAddress: log.Topics[2], + SourceAddress: log.Topics[3], + SourceTxID: log.TxHash[:], + UnsignedMsgBytes: log.Data, + }, nil +} + +// forward logs from the concrete log channel to the interface channel +func (s *subscriber) forwardLogs() { + for msgLog := range s.evmLog { + messageInfo, err := NewWarpLogInfo(msgLog) + if err != nil { + s.logger.Info( + "Invalid log. Continuing.", + zap.Error(err), + ) + continue + } + s.log <- *messageInfo + } +} + +func (s *subscriber) Subscribe() error { + // Retry subscribing until successful. Attempt to resubscribe maxResubscribeAttempts times + for attempt := 0; attempt < maxResubscribeAttempts; attempt++ { + // Unsubscribe before resubscribing + // s.sub should only be nil on the first call to Subscribe + if s.sub != nil { + s.sub.Unsubscribe() + } + err := s.dialAndSubscribe() + if err == nil { + s.logger.Info("Successfully subscribed") + return nil + } + + s.logger.Warn( + "Failed to subscribe to node", + zap.Int("attempt", attempt), + zap.Error(err), + ) + + if attempt != maxResubscribeAttempts-1 { + time.Sleep(subscribeRetryTimeout) + } + } + + return fmt.Errorf("failed to subscribe to node with all %d attempts", maxResubscribeAttempts) +} + +func (s *subscriber) dialAndSubscribe() error { + // Dial the configured destination chain endpoint + // This needs to be a websocket + ethClient, err := ethclient.Dial(s.nodeURL) + if err != nil { + return err + } + + filterQuery := interfaces.FilterQuery{ + Topics: [][]common.Hash{ + {warp.WarpABI.Events["SendWarpMessage"].ID}, + {}, + {}, + }, + Addresses: []common.Address{ + warp.ContractAddress, + }, + } + evmLogs := make(chan types.Log, maxClientSubscriptionBuffer) + logs := make(chan vmtypes.WarpLogInfo, maxClientSubscriptionBuffer) + sub, err := ethClient.SubscribeFilterLogs(context.Background(), filterQuery, evmLogs) + if err != nil { + s.logger.Error( + "Failed to subscribe to logs", + zap.Error(err), + ) + return err + } + s.log = logs + s.evmLog = evmLogs + s.sub = sub + + // Forward logs to the interface channel. Closed when the subscription is cancelled + go s.forwardLogs() + return nil +} + +func (s *subscriber) Logs() <-chan vmtypes.WarpLogInfo { + return s.log +} + +func (s *subscriber) Err() <-chan error { + return s.sub.Err() +} + +func (s *subscriber) Cancel() { + // Nothing to do here, the ethclient manages both the log and err channels +} diff --git a/vms/subscriber.go b/vms/subscriber.go new file mode 100644 index 00000000..45c4345b --- /dev/null +++ b/vms/subscriber.go @@ -0,0 +1,36 @@ +// Copyright (C) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package vms + +import ( + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/awm-relayer/config" + "github.com/ava-labs/awm-relayer/vms/evm" + "github.com/ava-labs/awm-relayer/vms/vmtypes" +) + +// Subscriber subscribes to VM events containing Warp message data +type Subscriber interface { + // Subscribe registers a subscription. After Subscribe is called, + // log events that match [filter] are written to the channel returned + // by Logs + Subscribe() error + // Logs returns the channel that the subscription writes events to + Logs() <-chan vmtypes.WarpLogInfo + // Err returns the channel that the subscription writes errors to + // If an error is sent to this channel, the subscription should be closed + Err() <-chan error + // Cancel cancels the subscription + Cancel() +} + +// NewSubscriber returns a concrete Subscriber according to the VM specified by [subnetInfo] +func NewSubscriber(logger logging.Logger, subnetInfo config.SourceSubnet) Subscriber { + switch config.ParseVM(subnetInfo.VM) { + case config.EVM: + return evm.NewSubscriber(logger, subnetInfo) + default: + return nil + } +} diff --git a/vms/vmtypes/message_info.go b/vms/vmtypes/message_info.go new file mode 100644 index 00000000..e529df32 --- /dev/null +++ b/vms/vmtypes/message_info.go @@ -0,0 +1,20 @@ +// Copyright (C) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package vmtypes + +import ( + "github.com/ava-labs/avalanchego/ids" + "github.com/ethereum/go-ethereum/common" +) + +// WarpLogInfo is provided by the subscription and +// describes the transaction information emitted by the source chain, +// including the Warp Message payload bytes +type WarpLogInfo struct { + SourceAddress common.Hash + DestinationChainID ids.ID + DestinationAddress common.Hash + SourceTxID []byte + UnsignedMsgBytes []byte +} diff --git a/vms/vmtypes/types.go b/vms/vmtypes/types.go new file mode 100644 index 00000000..c10ddf05 --- /dev/null +++ b/vms/vmtypes/types.go @@ -0,0 +1,18 @@ +// Copyright (C) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package vmtypes + +import ( + "github.com/ava-labs/avalanchego/vms/platformvm/warp" +) + +// +// Types used in the vms interfaces +// + +// WarpMessageInfo is used internally to provide access to warp message info emitted by the sender +type WarpMessageInfo struct { + WarpUnsignedMessage *warp.UnsignedMessage + WarpPayload []byte +}