Skip to content

Commit

Permalink
Merge branch 'main' into dependabot/go_modules/golang.org/x/net-0.23.0
Browse files Browse the repository at this point in the history
  • Loading branch information
joelsmith-2019 authored Jun 12, 2024
2 parents 568a3bf + 723eb7d commit d35ed41
Show file tree
Hide file tree
Showing 48 changed files with 927 additions and 463 deletions.
34 changes: 34 additions & 0 deletions .github/workflows/lint.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Lint
on:
push:
branches:
- main
paths:
- "**/*.go"
- "go.mod"
- "go.sum"
- "**/go.mod"
- "**/go.sum"
pull_request:
paths:
- "**/*.go"
- "go.mod"
- "go.sum"
- "**/go.mod"
- "**/go.sum"
merge_group:
permissions:
contents: read
jobs:
golangci:
name: golangci-lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.21"
check-latest: true
- name: run linting
run: |
make lint
65 changes: 65 additions & 0 deletions .golangci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
run:
timeout: 10m
tests: true

linters:
disable-all: true
enable:
- asciicheck
- bidichk
- bodyclose
- decorder
- dupl
- dupword
- errcheck
- errchkjson
- errname
- exhaustive
- exportloopref
- forbidigo
- gci
- goconst
- gocritic
- gofmt
- gosec
- gosimple
- gosmopolitan
- govet
- grouper
- ineffassign
- loggercheck
- misspell
- nilerr
- nilnil
- noctx
- stylecheck
- testifylint
- thelper
- tparallel
- typecheck
- unconvert
- unparam
- unused
- usestdlibvars
- wastedassign
- whitespace

linters-settings:
gci:
custom-order: true
sections:
- standard # Standard section: captures all standard packages.
- default # Default section: contains all imports that could not be matched to another section type.
- blank # blank imports
- dot # dot imports
- prefix(github.com/cometbft/cometbft)
- prefix(github.com/cosmos)
- prefix(github.com/cosmos/cosmos-sdk)
- prefix(cosmossdk.io)
- prefix(github.com/strangelove-ventures/noble-cctp-relayer)
gosec:
excludes:
- G404

issues:
max-issues-per-linter: 0
22 changes: 11 additions & 11 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,20 @@ GOBIN := $(GOPATH)/bin
###############################################################################
### Formatting & Linting ###
###############################################################################
.PHONY: format lint
.PHONY: lint lint-fix

gofumpt_cmd=mvdan.cc/gofumpt
golangci_lint_cmd=github.com/golangci/golangci-lint/cmd/golangci-lint

format:
@echo "🤖 Running formatter..."
@go run $(gofumpt_cmd) -l -w .
@echo "✅ Completed formatting!"
golangci_lint_cmd=golangci-lint
golangci_version=v1.57.2

lint:
@echo "🤖 Running linter..."
@go run $(golangci_lint_cmd) run --timeout=10m
@echo "✅ Completed linting!"
@echo "--> Running linter"
@go install github.com/golangci/golangci-lint/cmd/golangci-lint@$(golangci_version)
@$(golangci_lint_cmd) run ./... --timeout 15m

lint-fix:
@echo "--> Running linter and fixing issues"
@go install github.com/golangci/golangci-lint/cmd/golangci-lint@$(golangci_version)
@$(golangci_lint_cmd) run ./... --fix --timeout 15m


###############################################################################
Expand Down
65 changes: 46 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,28 +20,55 @@ Sample configs can be found in [config](config).

### Flush Interval

Using the `--flush-interval` flag will run a flush on all paths every `duration`; ex `--flush-interval 5m`
Using the `--flush-interval` flag will run a flush on all chains every `duration`; ex `--flush-interval 5m`

The relayer will keep track of the latest flushed block. The first time the flush is run, the flush will start at the chains latest height - lookback period and flush up until height of the chain when the flush started. It will then store the height the flush ended on.
The first time the flush is run per chain, the flush will start at the chains `latest height - (2 * lookback period)`. The flush will always finish at the `latest chain height - lookback period`. This allows the flush to lag behind the chain so that the flush does not compete for transactions that are actively being processed. For subsequent flushes, each chain will reference its last flushed block, start from there and flush to the `latest chain height - lookback period` again. The flushing process will continue as long as the relayer is running.

After that, it will flush from the last stored height - lookback period up until the latest height of the chain.
For best results and coverage, the lookback period in blocks should correspond to the flush interval. If a chain produces 1 block a second and the flush interval is set to 30 minutes (1800 seconds), the lookback period should be at least 1800 blocks. When in doubt, round up and add a small buffer.

#### Examples

Consider a 30 minute flush interval (1800 seconds)
- Ethereum: 12 second blocks = (1800 / 12) = `150 blocks`
- Polygon: 2 second blocks = (1800 / 2) = `900 blocks`
- Arbitrum: 0.26 second blocks = (1800 / 0.26) = `~6950 blocks`

### Flush Only Mode

This relayer also supports a `--flush-only-mode`. This mode will only flush the chain and not actively listen for new events as they occur. This is useful for running a secondary relayer which "lags" behind the primary relayer. It is only responsible for retrying failed transactions.

When the relayer is in flush only mode, the flush mechanism will start at `latest height - (4 * lookback period)` and finish at `latest height - (3 * lookback period)`. For all subsequent flushes, the relayer will start at the last flushed block and finish at `latest height - (3 * lookback period)`. Please see the notes above for configuring the flush interval and lookback period.

> Note: It is highly recommended to use the same configuration for both the primary and secondary relayer. This ensures that there is zero overlap between the relayers.
### Prometheus Metrics

By default, metrics are exported at on port :2112/metrics (`http://localhost:2112/metrics`). You can customize the port using the `--metrics-port` flag.

| **Exported Metric** | **Description** | **Type** |
|-------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------|----------|
| cctp_relayer_wallet_balance | Current balance of a relayer wallet in Wei.<br><br>Noble balances are not currently exported b/c `MsgReceiveMessage` is free to submit on Noble. | Gauge |
| cctp_relayer_chain_latest_height | Current height of the chain. | Gauge |
| cctp_relayer_broadcast_errors_total | The total number of failed broadcasts. Note: this is AFTER it retries `broadcast-retries` (config setting) number of times. | Counter |
| **Exported Metric** | **Description** | **Type** |
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | -------- |
| cctp_relayer_wallet_balance | Current balance of a relayer wallet in Wei.<br><br>Noble balances are not currently exported b/c `MsgReceiveMessage` is free to submit on Noble. | Gauge |
| cctp_relayer_chain_latest_height | Current height of the chain. | Gauge |
| cctp_relayer_broadcast_errors_total | The total number of failed broadcasts. Note: this is AFTER it retries `broadcast-retries` (config setting) number of times. | Counter |

### Noble Key
### Minter Private Keys
Minter private keys are required on a per chain basis to broadcast transactions to the target chain. These private keys can either be set in the `config.yaml` or via environment variables.

The noble private key you input into the config must be hex encoded. The easiest way to get this is via a chain binary:
#### Config Private Keys

`nobled keys export <KEY_NAME> --unarmored-hex --unsafe`
Please see `./config/sample-config.yaml` for setting minter private keys in configuration. Please note that this method is insecure as the private keys are stored in plain text.

#### Env Vars Private Keys

To pass in a private key via an environment variable, first identify the chain's name. A chain's name corresponds to the key under the `chains` section in the `config.yaml`. The sample config lists these chain names for example: `noble`, `ethereum`, `optimism`, etc. Now, take the chain name in all caps and append `_PRIV_KEY`.

An environment variable for `noble` would look like: `NOBLE_PRIV_KEY=<PRIVATE_KEY_HERE>`

#### Noble Private Key Format

The noble private key you input into the config or via enviroment variables must be hex encoded. The easiest way to get this is via a chain binary:

`nobled keys export <KEY_NAME> --unarmored-hex --unsafe`

### API
Simple API to query message state cache
Expand All @@ -54,14 +81,14 @@ localhost:8000/tx/<hash>?domain=0

### State

| IrisLookupId | Status | SourceDomain | DestDomain | SourceTxHash | DestTxHash | MsgSentBytes | Created | Updated |
|:-------------|:---------|:-------------|:-----------|:--------------|:-----------|:-------------|:--------|:--------|
| 0x123 | Created | 0 | 4 | 0x123 | ABC123 | bytes... | date | date |
| 0x123 | Pending | 0 | 4 | 0x123 | ABC123 | bytes... | date | date |
| 0x123 | Attested | 0 | 4 | 0x123 | ABC123 | bytes... | date | date |
| 0x123 | Complete | 0 | 4 | 0x123 | ABC123 | bytes... | date | date |
| 0x123 | Failed | 0 | 4 | 0x123 | ABC123 | bytes... | date | date |
| 0x123 | Filtered | 0 | 4 | 0x123 | ABC123 | bytes... | date | date |
| IrisLookupId | Status | SourceDomain | DestDomain | SourceTxHash | DestTxHash | MsgSentBytes | Created | Updated |
| :----------- | :------- | :----------- | :--------- | :----------- | :--------- | :----------- | :------ | :------ |
| 0x123 | Created | 0 | 4 | 0x123 | ABC123 | bytes... | date | date |
| 0x123 | Pending | 0 | 4 | 0x123 | ABC123 | bytes... | date | date |
| 0x123 | Attested | 0 | 4 | 0x123 | ABC123 | bytes... | date | date |
| 0x123 | Complete | 0 | 4 | 0x123 | ABC123 | bytes... | date | date |
| 0x123 | Failed | 0 | 4 | 0x123 | ABC123 | bytes... | date | date |
| 0x123 | Filtered | 0 | 4 | 0x123 | ABC123 | bytes... | date | date |

### Generating Go ABI bindings

Expand Down
34 changes: 29 additions & 5 deletions circle/attestation.go
Original file line number Diff line number Diff line change
@@ -1,31 +1,54 @@
package circle

import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"

"cosmossdk.io/log"

"github.com/strangelove-ventures/noble-cctp-relayer/types"
)

// CheckAttestation checks the iris api for attestation status and returns true if attestation is complete
func CheckAttestation(attestationURL string, logger log.Logger, irisLookupId string, txHash string, sourceDomain, destDomain types.Domain) *types.AttestationResponse {
logger.Debug(fmt.Sprintf("Checking attestation for %s%s%s for source tx %s from %d to %d", attestationURL, "0x", irisLookupId, txHash, sourceDomain, destDomain))
func CheckAttestation(attestationURL string, logger log.Logger, irisLookupID string, txHash string, sourceDomain, destDomain types.Domain) *types.AttestationResponse {
// append ending / if not present
if attestationURL[len(attestationURL)-1:] != "/" {
attestationURL += "/"
}

// add 0x prefix if not present
if len(irisLookupID) > 2 && irisLookupID[:2] != "0x" {
irisLookupID = "0x" + irisLookupID
}

logger.Debug(fmt.Sprintf("Checking attestation for %s%s for source tx %s from %d to %d", attestationURL, irisLookupID, txHash, sourceDomain, destDomain))

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

client := http.Client{Timeout: 2 * time.Second}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, attestationURL+irisLookupID, nil)
if err != nil {
logger.Debug("error creating request: " + err.Error())
return nil
}

rawResponse, err := client.Get(attestationURL + "0x" + irisLookupId)
client := http.Client{}
rawResponse, err := client.Do(req)
if err != nil {
logger.Debug("error during request: " + err.Error())
return nil
}

defer rawResponse.Body.Close()
if rawResponse.StatusCode != http.StatusOK {
logger.Debug("non 200 response received from Circles attestation API")
return nil
}

body, err := io.ReadAll(rawResponse.Body)
if err != nil {
logger.Debug("unable to parse message body")
Expand All @@ -38,7 +61,8 @@ func CheckAttestation(attestationURL string, logger log.Logger, irisLookupId str
logger.Debug("unable to unmarshal response")
return nil
}
logger.Info(fmt.Sprintf("Attestation found for %s%s%s", attestationURL, "0x", irisLookupId))

logger.Info(fmt.Sprintf("Attestation found for %s%s", attestationURL, irisLookupID))

return &response
}
29 changes: 24 additions & 5 deletions circle/attestation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,47 @@ import (
"os"
"testing"

"cosmossdk.io/log"
"github.com/rs/zerolog"
"github.com/stretchr/testify/require"

"cosmossdk.io/log"

"github.com/strangelove-ventures/noble-cctp-relayer/circle"
"github.com/strangelove-ventures/noble-cctp-relayer/types"
"github.com/stretchr/testify/require"
)

var cfg types.Config
var logger log.Logger

func init() {
cfg.Circle.AttestationBaseUrl = "https://iris-api-sandbox.circle.com/attestations/"
cfg.Circle.AttestationBaseURL = "https://iris-api-sandbox.circle.com/attestations/"
logger = log.NewLogger(os.Stdout, log.LevelOption(zerolog.ErrorLevel))
}

func TestAttestationIsReady(t *testing.T) {
resp := circle.CheckAttestation(cfg.Circle.AttestationBaseUrl, logger, "85bbf7e65a5992e6317a61f005e06d9972a033d71b514be183b179e1b47723fe", "", 0, 4)
resp := circle.CheckAttestation(cfg.Circle.AttestationBaseURL, logger, "85bbf7e65a5992e6317a61f005e06d9972a033d71b514be183b179e1b47723fe", "", 0, 4)
require.NotNil(t, resp)
require.Equal(t, "complete", resp.Status)
}

func TestAttestationNotFound(t *testing.T) {
resp := circle.CheckAttestation(cfg.Circle.AttestationBaseUrl, logger, "not an attestation", "", 0, 4)
resp := circle.CheckAttestation(cfg.Circle.AttestationBaseURL, logger, "not an attestation", "", 0, 4)
require.Nil(t, resp)
}

func TestAttestationWithoutEndingSlash(t *testing.T) {
startURL := cfg.Circle.AttestationBaseURL
cfg.Circle.AttestationBaseURL = startURL[:len(startURL)-1]

resp := circle.CheckAttestation(cfg.Circle.AttestationBaseURL, logger, "85bbf7e65a5992e6317a61f005e06d9972a033d71b514be183b179e1b47723fe", "", 0, 4)
require.NotNil(t, resp)
require.Equal(t, "complete", resp.Status)

cfg.Circle.AttestationBaseURL = startURL
}

func TestAttestationWithLeading0x(t *testing.T) {
resp := circle.CheckAttestation(cfg.Circle.AttestationBaseURL, logger, "0x85bbf7e65a5992e6317a61f005e06d9972a033d71b514be183b179e1b47723fe", "", 0, 4)
require.NotNil(t, resp)
require.Equal(t, "complete", resp.Status)
}
Loading

0 comments on commit d35ed41

Please sign in to comment.