Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Configuration support fully specified source to destination mapping #59

Merged
merged 34 commits into from
Oct 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
9ab0d98
add allowed destination chain id list
gwen917 Oct 13, 2023
618a185
add unit tests
gwen917 Oct 13, 2023
fddb357
fix comments
gwen917 Oct 13, 2023
8b5eacd
fix typo and config validation
gwen917 Oct 16, 2023
62d4fac
fix PR comments
gwen917 Oct 16, 2023
4c0d019
fix PR comments
gwen917 Oct 16, 2023
4240369
fix
gwen917 Oct 16, 2023
199b3e7
add global cfg
cam-schultz Oct 17, 2023
6f56a22
add source chain ID to log
cam-schultz Oct 17, 2023
724178f
combine source subnet config validation
cam-schultz Oct 17, 2023
5b6a211
add config readme
cam-schultz Oct 17, 2023
89ab862
rename supported destinations
cam-schultz Oct 17, 2023
836bce8
calculare allowed destinations on startup
cam-schultz Oct 17, 2023
c22b7bd
clarify error msg
cam-schultz Oct 17, 2023
be3f30f
fix typo
cam-schultz Oct 19, 2023
b3318d6
Merge branch 'main' into p2p-config
cam-schultz Oct 19, 2023
5e75b5f
readme cleanup
cam-schultz Oct 20, 2023
6973bdb
cleanup readme
cam-schultz Oct 24, 2023
c80f97b
validate dst chains in source validation
cam-schultz Oct 24, 2023
947d5d7
clarify comment
cam-schultz Oct 24, 2023
9ae1307
correct json tag
cam-schultz Oct 24, 2023
5b5decb
Merge branch 'main' into p2p-config
cam-schultz Oct 24, 2023
50dd208
Merge branch 'main' into p2p-config
cam-schultz Oct 25, 2023
c05368e
remove default p-chain node
cam-schultz Oct 25, 2023
6c8a869
denote api methods in readme
cam-schultz Oct 25, 2023
77fe281
remove global config
cam-schultz Oct 25, 2023
cb82c07
move supported destination check to relayer
cam-schultz Oct 25, 2023
50548ac
check supported destination unit test
cam-schultz Oct 26, 2023
468fa3f
lazily initialize source id fields
cam-schultz Oct 26, 2023
03b790d
remove redundant validation step
cam-schultz Oct 26, 2023
5630533
Merge branch 'main' into p1p-config
cam-schultz Oct 26, 2023
87634cb
use require in test
cam-schultz Oct 26, 2023
0e5eee6
store source ids on initialization
cam-schultz Oct 26, 2023
8498771
combine duplicate iterations
cam-schultz Oct 27, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 92 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
# awm-relayer

Standalone relayer for cross-chain Avalanche Warp Message delivery.
Reference relayer implementation for cross-chain Avalanche Warp Message delivery.

## Usage

### Building

Build the relayer by running the included build script:
Build the relayer by running the script:

```bash
./scripts/build.sh
```

Build a Docker image by running the included build script:
Build a Docker image by running the script:
```
./scripts/build-local-image.sh
```
Expand All @@ -25,19 +25,102 @@ The relayer binary accepts a path to a JSON configuration file as the sole argum
./build/awm-relayer --config-file path-to-config
```

## Architecture
### Configuration

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love this new README section 🙌


The relayer is configured via a JSON file, the path to which is passed in via the `--config-file` command line argument. The following configuration options are available:

`"log-level": "debug" | "info" | "warn" | "error" | "fatal" | "panic"`
- The log level for the relayer. Defaults to `info`.

`"network-id": integer`
- The ID of the Avalanche network to which the relayer will connect. Defaults to `1` (Mainnet).

`"p-chain-api-url": string`
- The URL of the Avalanche P-Chain API node to which the relayer will connect. This API node needs to have the following methods enabled:
- info.peers
- platform.getHeight
- platform.validatedBy
- platform.getValidatorsAt

`"encrypt-connection": boolean`
- Whether or not to encrypt the connection to the P-Chain API node. Defaults to `true`.

`"storage-location": string`
- The path to the directory in which the relayer will store its state. Defaults to `./awm-relayer-storage`.

`"source-subnets": []SourceSubnets`
- The list of source subnets to support. Each `SourceSubnet` has the following configuration:

`"subnet-id": string`
- cb58-encoded Subnet ID

`"blockchain-id": string`
- cb58-encoded Blockchain ID

`"vm": string`
- The VM type of the source subnet.

`"api-node-host": string`
- The host of the source subnet's API node.

`"api-node-port": integer`
- The port of the source subnet's API node.

`"encrypt-connection": boolean`
- Whether or not to encrypt the connection to the source subnet's API node.

**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.
`"rpc-endpoint": string`
- The RPC endpoint of the source subnet's API node. Used in favor of `api-node-host`, `api-node-port`, and `encrypt-connection` when constructing the endpoint

`"ws-endpoint": string`
- The WebSocket endpoint of the source subnet's API node. Used in favor of `api-node-host`, `api-node-port`, and `encrypt-connection` when constructing the endpoint

`"message-contracts": map[string]MessageProtocolConfig`
- Map of contract addresses to the config options of the protocol at that address. Each `MessageProtocolConfig` consists of a unique `message-format` name, and the raw JSON `settings`

`"supported-destinations": []string`
- List of destination subnet IDs that the source subnet supports. If empty, then all destinations are supported.

`"destination-subnets": []DestinationSubnets`
- The list of destination subnets to support. Each `DestinationSubnet` has the following configuration:

`"subnet-id": string`
- cb58-encoded Subnet ID

`"blockchain-id": string`
- cb58-encoded Blockchain ID

`"vm": string`
- The VM type of the source subnet.

`"api-node-host": string`
- The host of the source subnet's API node.

`"api-node-port": integer`
- The port of the source subnet's API node.

`"encrypt-connection": boolean`
- Whether or not to encrypt the connection to the source subnet's API node.

`"rpc-endpoint": string`
- The RPC endpoint of the destination subnet's API node. Used in favor of `api-node-host`, `api-node-port`, and `encrypt-connection` when constructing the endpoint

`"account-private-key": string`
- The hex-encoded private key to use for signing transactions on the destination subnet. May be provided by the environment variable `ACCOUNT_PRIVATE_KEY`. Each `destination-subnet` may use a separate private key by appending the blockchain ID to the private key environment variable name, e.g. `ACCOUNT_PRIVATE_KEY_11111111111111111111111111111111LpoYY`

## Architecture

### Components

The relayer consists of the following components:

- At the global level:
- *P2P App Network*: issues signature `AppRequests`
- *P2P app network*: issues signature `AppRequests`
- *P-Chain client*: gets the validators for a subnet
- *JSON database*: stores latest processed block for each source subnet
- Per Source subnet
- *Subscriber*: listens for logs pertaining to cross-chain message transactions
- *Source RPC client*: queries for missed blocks on startup
- Per Destination subnet
- *Destination RPC client*: broadcasts transactions to the destination

Expand All @@ -51,7 +134,7 @@ The relayer consists of the following components:

### Unit tests

Unit tests can be ran locally by running the command in root of the project:
Unit tests can be ran locally by running the command in the root of the project:

```bash
./scripts/test.sh
Expand All @@ -61,13 +144,13 @@ Unit tests can be ran locally by running the command in root of the project:

E2E tests are ran as part of CI, but can also be ran locally with the `--local` flag. To run the E2E tests locally, you'll need to install Gingko following the intructions [here](https://onsi.github.io/ginkgo/#installing-ginkgo)

Next, provide the path to the `subnet-evm` repository and the path to a writeable data directory (here we use the `~/subnet-evm` and `~/tmp/e2e-test`) to use for the tests:
Next, provide the path to the `subnet-evm` repository and the path to a writeable data directory (this example uses `~/subnet-evm` and `~/tmp/e2e-test`) to use for the tests:
```bash
./scripts/e2e_test.sh --local --subnet-evm ~/subnet-evm --data-dir ~/tmp/e2e-test
```
### Generate Mocks

We use [gomock](https://pkg.go.dev/go.uber.org/mock/gomock) to generate mocks for testing. To generate mocks, run the following command at the root of the project:
[Gomock](https://pkg.go.dev/go.uber.org/mock/gomock) is used to generate mocks for testing. To generate mocks, run the following command at the root of the project:

```bash
go generate ./...
Expand Down
99 changes: 69 additions & 30 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,19 @@ type MessageProtocolConfig struct {
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"`
RPCEndpoint string `mapstructure:"rpc-endpoint" json:"rpc-endpoint"`
WSEndpoint string `mapstructure:"ws-endpoint" json:"ws-endpoint"`
MessageContracts map[string]MessageProtocolConfig `mapstructure:"message-contracts" json:"message-contracts"`
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"`
WSEndpoint string `mapstructure:"ws-endpoint" json:"ws-endpoint"`
MessageContracts map[string]MessageProtocolConfig `mapstructure:"message-contracts" json:"message-contracts"`
SupportedDestinations []string `mapstructure:"supported-destinations" json:"supported-destinations"`

// convenience field to access the supported destinations after initialization
supportedDestinations set.Set[ids.ID]
}

type DestinationSubnet struct {
Expand All @@ -67,12 +71,15 @@ type Config struct {
StorageLocation string `mapstructure:"storage-location" json:"storage-location"`
SourceSubnets []SourceSubnet `mapstructure:"source-subnets" json:"source-subnets"`
DestinationSubnets []DestinationSubnet `mapstructure:"destination-subnets" json:"destination-subnets"`

// convenience fields to access the source subnet and chain IDs after initialization
sourceSubnetIDs []ids.ID
sourceChainIDs []ids.ID
}

func SetDefaultConfigValues(v *viper.Viper) {
v.SetDefault(LogLevelKey, logging.Info.String())
v.SetDefault(NetworkIDKey, constants.MainnetID)
v.SetDefault(PChainAPIURLKey, "https://api.avax.network")
v.SetDefault(EncryptConnectionKey, true)
v.SetDefault(StorageLocationKey, "./.awm-relayer-storage")
}
Expand Down Expand Up @@ -165,17 +172,8 @@ func (c *Config) Validate() error {
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)
}

// Validate the destination chains
destinationChains := set.NewSet[string](len(c.DestinationSubnets))
for _, s := range c.DestinationSubnets {
if err := s.Validate(); err != nil {
Expand All @@ -187,30 +185,47 @@ func (c *Config) Validate() error {
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) {
// Validate the source chains and store the source subnet and chain IDs for future use
sourceChains := set.NewSet[string](len(c.SourceSubnets))
var sourceSubnetIDs []ids.ID
var sourceChainIDs []ids.ID
for _, s := range cfg.SourceSubnets {
for _, s := range c.SourceSubnets {
// Validate configuration
if err := s.Validate(&destinationChains); err != nil {
return err
}
// Verify uniqueness
if sourceChains.Contains(s.ChainID) {
return fmt.Errorf("configured source subnets must have unique chain IDs")
}
sourceChains.Add(s.ChainID)

// Save IDs for future use
subnetID, err := ids.FromString(s.SubnetID)
if err != nil {
return nil, nil, fmt.Errorf("invalid subnetID in configuration. error: %v", err)
return 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)
return fmt.Errorf("invalid subnetID in configuration. error: %v", err)
}
sourceChainIDs = append(sourceChainIDs, chainID)
}
return sourceSubnetIDs, sourceChainIDs, nil

c.sourceSubnetIDs = sourceSubnetIDs
c.sourceChainIDs = sourceChainIDs

return nil
}

func (s *SourceSubnet) Validate() error {
func (s *SourceSubnet) GetSupportedDestinations() set.Set[ids.ID] {
return s.supportedDestinations
}

// Validates the source subnet configuration, including verifying that the supported destinations are present in destinationChainIDs
func (s *SourceSubnet) Validate(destinationChainIDs *set.Set[string]) error {
if _, err := ids.FromString(s.SubnetID); err != nil {
return fmt.Errorf("invalid subnetID in source subnet configuration. Provided ID: %s", s.SubnetID)
}
Expand Down Expand Up @@ -244,6 +259,21 @@ func (s *SourceSubnet) Validate() error {
}
}

// Validate and store the allowed destinations for future use
s.supportedDestinations = set.Set[ids.ID]{}
for _, blockchainIDStr := range s.SupportedDestinations {
blockchainID, err := ids.FromString(blockchainIDStr)
if err != nil {
return fmt.Errorf("invalid chainID in configuration. error: %v", err)
}
if !destinationChainIDs.Contains(blockchainIDStr) {
return fmt.Errorf("configured source subnet %s has a supported destination blockchain ID %s that is not configured as a destination blockchain",
s.SubnetID,
blockchainID)
}
s.supportedDestinations.Add(blockchainID)
}

return nil
}

Expand Down Expand Up @@ -373,3 +403,12 @@ func (s *DestinationSubnet) GetRelayerAccountInfo() (*ecdsa.PrivateKey, common.A
pkBytes = append(pkBytes, pk.PublicKey.Y.Bytes()...)
return pk, common.BytesToAddress(crypto.Keccak256(pkBytes)), nil
}

//
// Top-level config getters
//

// GetSourceIDs returns the Subnet and Chain IDs of all subnets configured as a source
func (c *Config) GetSourceIDs() ([]ids.ID, []ids.ID) {
return c.sourceSubnetIDs, c.sourceChainIDs
}
9 changes: 1 addition & 8 deletions main/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,7 @@ func main() {

// 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
}
sourceSubnetIDs, sourceChainIDs := cfg.GetSourceIDs()

// Initialize metrics gathered through prometheus
gatherer, registerer, err := initMetrics()
Expand Down
1 change: 1 addition & 0 deletions messages/teleporter/message_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ func (m *messageManager) ShouldSendMessage(warpMessageInfo *vmtypes.WarpMessageI
if !ok {
return false, fmt.Errorf("relayer not configured to deliver to destination. destinationChainID=%s", destinationChainID.String())
}

senderAddress := destinationClient.SenderAddress()
if !isAllowedRelayer(teleporterMessage.AllowedRelayerAddresses, senderAddress) {
m.logger.Info(
Expand Down
1 change: 1 addition & 0 deletions messages/teleporter/message_manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ func TestShouldSendMessage(t *testing.T) {
if test.clientResult != nil {
test.clientResult.EXPECT().CallContract(gomock.Any(), gomock.Any(), gomock.Any()).Return(test.callContractResult, nil).Times(test.callContractTimes)
}

result, err := messageManager.ShouldSendMessage(test.warpMessageInfo, test.destinationChainID)
if test.expectedError {
require.Error(t, err)
Expand Down
Loading
Loading