├── .dockerignore
├── .githooks
└── pre-commit
├── .github
├── CODEOWNERS
├── ISSUE_TEMPLATE
│ ├── bug-report.yml
│ └── feature-request.yml
├── dependabot.yml
└── workflows
│ ├── ci-release.yml
│ ├── codeql.yml
│ ├── docker-build-publish.yml
│ ├── lables.yml
│ ├── lint.yml
│ ├── stale.yml
│ ├── tag.yml
│ └── test.yml
├── .gitignore
├── .golangci.yml
├── .goreleaser.yaml
├── .markdownlint.yaml
├── CHANGELOG.md
├── Dockerfile
├── LICENSE
├── Makefile
├── NOTICE
├── README.md
├── benchmark
└── main.go
├── cmd
└── blobstream
│ ├── base
│ └── config.go
│ ├── bootstrapper
│ ├── cmd.go
│ └── config.go
│ ├── common
│ └── helpers.go
│ ├── deploy
│ ├── cmd.go
│ ├── config.go
│ └── errors.go
│ ├── generate
│ └── cmd.go
│ ├── keys
│ ├── common
│ │ └── common.go
│ ├── evm
│ │ ├── config.go
│ │ ├── evm.go
│ │ └── evm_test.go
│ ├── keys.go
│ └── p2p
│ │ ├── config.go
│ │ ├── p2p.go
│ │ └── p2p_test.go
│ ├── main.go
│ ├── orchestrator
│ ├── cmd.go
│ └── config.go
│ ├── query
│ ├── cmd.go
│ └── config.go
│ ├── relayer
│ ├── cmd.go
│ └── config.go
│ ├── root
│ └── cmd.go
│ └── version
│ ├── build_info.go
│ └── version.go
├── docker
└── entrypoint.sh
├── docs
├── bootstrapper.md
├── deploy.md
├── keys.md
├── orchestrator.md
└── relayer.md
├── e2e
├── Dockerfile_e2e
├── Makefile
├── README.md
├── Service.go
├── celestia-app
│ ├── app.toml
│ ├── config.toml
│ ├── core0
│ │ ├── account-address-core0.txt
│ │ ├── config
│ │ │ ├── gentx
│ │ │ │ └── gentx-de74a671b839639ed638fd4140f797ddadb3c2f1.json
│ │ │ ├── node_key.json
│ │ │ └── priv_validator_key.json
│ │ └── keyring-test
│ │ │ ├── 0a90c0bbbe830cda2fcd50a587f3c885d548fa12.address
│ │ │ └── core0.info
│ ├── core1
│ │ ├── account-address-core1.txt
│ │ ├── config
│ │ │ ├── node_key.json
│ │ │ └── priv_validator_key.json
│ │ └── keyring-test
│ │ │ ├── a6dcb7467c034b90704d82179e62931fe82054a0.address
│ │ │ └── core1.info
│ ├── core2
│ │ ├── account-address-core2.txt
│ │ ├── config
│ │ │ ├── node_key.json
│ │ │ └── priv_validator_key.json
│ │ └── keyring-test
│ │ │ ├── 6e5a5f45cc5319a5d2b4529d545d377f59579ac9.address
│ │ │ └── core2.info
│ ├── core3
│ │ ├── account-address-core3.txt
│ │ ├── config
│ │ │ ├── node_key.json
│ │ │ └── priv_validator_key.json
│ │ └── keyring-test
│ │ │ ├── a4d227a9eb1b27c19b18038c659abfab91bd4a48.address
│ │ │ └── core3.info
│ ├── genesis.json
│ └── genesis_template.json
├── deployer_test.go
├── docker-compose.yml
├── errors.go
├── go.mod
├── go.sum
├── orchestrator_test.go
├── qgb_network.go
├── relayer_test.go
├── scripts
│ ├── cleanup.sh
│ ├── deploy_blobstream_contract.sh
│ ├── start_core0.sh
│ ├── start_node_and_create_validator.sh
│ ├── start_orchestrator_after_validator_created.sh
│ └── start_relayer.sh
├── telemetry
│ ├── grafana
│ │ └── datasources
│ │ │ └── config.yml
│ ├── otel-collector
│ │ └── config.yml
│ └── prometheus
│ │ └── prometheus.yml
└── test_commons.go
├── evm
├── errors.go
├── ethereum_signature.go
├── ethereum_signature_test.go
├── evm_client.go
├── evm_client_test.go
├── evm_transaction_opts.go
├── evm_transaction_opts_test.go
└── suite_test.go
├── go.mod
├── go.sum
├── go.work.sum
├── helpers
├── interrupt.go
├── parse.go
├── parse_test.go
├── retrier.go
├── retrier_test.go
├── ticker.go
└── ticker_test.go
├── orchestrator
├── broadcaster.go
├── broadcaster_test.go
├── errors.go
├── orchestrator.go
├── orchestrator_test.go
└── suite_test.go
├── p2p
├── dht.go
├── dht_test.go
├── errors.go
├── host.go
├── host_test.go
├── keys.go
├── keys_test.go
├── querier.go
├── querier_test.go
├── validators.go
└── validators_test.go
├── relayer
├── errors.go
├── historic_relayer_test.go
├── historic_suite_test.go
├── relayer.go
├── relayer_test.go
└── suite_test.go
├── rpc
├── app_historic_querier_test.go
├── app_querier.go
├── app_querier_test.go
├── errors.go
├── historic_suite_test.go
├── suite_test.go
├── tm_querier.go
└── tm_querier_test.go
├── scripts
└── test_cover.sh
├── store
├── badger.go
├── errors.go
├── fslock
│ ├── lock_unix.go
│ ├── locker.go
│ └── locker_test.go
├── init.go
├── init_test.go
├── store.go
└── store_test.go
├── telemetry
└── metrics.go
├── testing
├── blobstream.go
├── celestia_network.go
├── dht_network.go
├── errors.go
├── evm_chain.go
└── testnode.go
└── types
├── data_commitment_confirm.go
├── data_commitment_confirm_test.go
├── errors.go
├── latest_valset.go
├── latest_valset_test.go
├── valset_confirm.go
└── valset_confirm_test.go
/.dockerignore:
--------------------------------------------------------------------------------
1 | build/
2 | e2e/
3 | .vscode/
4 | .idea/
5 | tmp/
6 | *.md
7 | *.txt
8 | profile.out
9 | *.yml
10 | *.yaml
11 |
--------------------------------------------------------------------------------
/.githooks/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | set -eu -o pipefail
4 |
5 | STAGED_GO_FILES=$(git diff --cached --name-only -- '*.go')
6 | STAGED_MD_FILES=$(git diff --cached --name-only -- '*.md')
7 |
8 | if [[ $STAGED_GO_FILES == "" ]] && [[ $STAGED_MD_FILES == "" ]]; then
9 | echo "--> Found no go or markdown files, skipping linting"
10 | elif [[ $STAGED_GO_FILES == "" ]]; then
11 | echo "--> Found markdown files, linting"
12 | if ! command -v markdownlint &> /dev/null ; then
13 | echo "markdownlint is not installed of available in the PATH" >&2
14 | echo "please check https://github.com/igorshubovych/markdownlint-cli" >&2
15 | exit 1
16 | fi
17 | markdownlint --config .markdownlint.yaml '**/*.md'
18 | else
19 | echo "--> Found go files, running make lint"
20 | if ! command -v golangci-lint &> /dev/null ; then
21 | echo "golangci-lint not installed or available in the PATH" >&2
22 | echo "please check https://github.com/golangci/golangci-lint" >&2
23 | exit 1
24 | fi
25 | make lint
26 | fi
27 |
28 | if go mod tidy -v 2>&1 | grep -q 'updates to go.mod needed'; then
29 | exit 1
30 | fi
31 |
32 | git diff --exit-code go.* &> /dev/null
33 |
34 | if [ $? -eq 1 ]; then
35 | echo "go.mod or go.sum differs, please re-add it to your commit"
36 |
37 | exit 1
38 | fi
39 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # CODEOWNERS: https://help.github.com/articles/about-codeowners/
2 |
3 | # Everything goes through the following "global owners" by default.
4 | # Unless a later match takes precedence, these three will be
5 | # requested for review when someone opens a PR.
6 | # Note that the last matching pattern takes precedence, so
7 | # global owners are only requested if there isn't a more specific
8 | # codeowner specified below. For this reason, the global codeowners
9 | # are often repeated in package-level definitions.
10 |
11 | # global owners
12 | * @evan-forbes @rach-id
13 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug-report.yml:
--------------------------------------------------------------------------------
1 | name: Bug Report
2 | description: Create a report to help us squash bugs!
3 | title: "
"
4 | labels: ["bug"]
5 |
6 | body:
7 | - type: markdown
8 | attributes:
9 | value: |
10 | IMPORTANT: Prior to opening a bug report, check if it affects one of the
11 | core modules and if it's eligible for a bug bounty on `SECURITY.md`.
12 | Bugs that are not submitted through the appropriate channels won't
13 | receive any bounty.
14 |
15 | - type: textarea
16 | id: summary
17 | attributes:
18 | label: Summary of Bug
19 | description: Concisely describe the issue.
20 | validations:
21 | required: true
22 |
23 | - type: textarea
24 | id: version
25 | attributes:
26 | label: Version
27 | description: git commit hash or release version
28 | validations:
29 | required: true
30 |
31 | - type: textarea
32 | id: repro
33 | attributes:
34 | label: Steps to Reproduce
35 | description: >
36 | What commands in order should someone run to reproduce your problem?
37 | validations:
38 | required: true
39 |
40 | - type: checkboxes
41 | id: admin
42 | attributes:
43 | label: For Admin Use
44 | description: (do not edit)
45 | options:
46 | - label: Not duplicate issue
47 | - label: Appropriate labels applied
48 | - label: Appropriate contributors tagged
49 | - label: Contributor assigned/self-assigned
50 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature-request.yml:
--------------------------------------------------------------------------------
1 | name: Feature Request
2 | description: Create a proposal to request a feature
3 | title: ""
4 | labels: ["enhancement"]
5 |
6 | body:
7 | - type: markdown
8 | attributes:
9 | value: |
10 | ✰ Thanks for opening an issue! ✰
11 | Before smashing the submit button please fill in the template.
12 | Word of caution: poorly thought-out proposals may be rejected without
13 | deliberation.
14 |
15 | - type: textarea
16 | id: summary
17 | attributes:
18 | label: Summary
19 | description: Short, concise description of the proposed feature.
20 | validations:
21 | required: true
22 |
23 | - type: textarea
24 | id: problem
25 | attributes:
26 | label: Problem Definition
27 | description: |
28 | Why do we need this feature?
29 | What problems may be addressed by introducing this feature?
30 | What benefits does the SDK stand to gain by including this feature?
31 | Are there any disadvantages of including this feature?
32 | validations:
33 | required: true
34 |
35 | - type: textarea
36 | id: proposal
37 | attributes:
38 | label: Proposal
39 | description: Detailed description of requirements of implementation.
40 | validations:
41 | required: true
42 |
43 | - type: checkboxes
44 | id: admin
45 | attributes:
46 | label: For Admin Use
47 | description: (do not edit)
48 | options:
49 | - label: Not duplicate issue
50 | - label: Appropriate labels applied
51 | - label: Appropriate contributors tagged
52 | - label: Contributor assigned/self-assigned
53 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: docker
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | open-pull-requests-limit: 10
8 | reviewers:
9 | - "rach-id"
10 | - package-ecosystem: github-actions
11 | directory: "/"
12 | schedule:
13 | interval: daily
14 | open-pull-requests-limit: 10
15 | reviewers:
16 | - "rach-id"
17 | - package-ecosystem: gomod
18 | directory: "/"
19 | schedule:
20 | interval: daily
21 | open-pull-requests-limit: 10
22 | labels:
23 | - automerge
24 | - dependencies
25 | reviewers:
26 | - "rach-id"
27 | - package-ecosystem: gomod
28 | directory: "e2e"
29 | schedule:
30 | interval: daily
31 | open-pull-requests-limit: 10
32 | labels:
33 | - automerge
34 | - dependencies
35 | reviewers:
36 | - "rach-id"
37 |
--------------------------------------------------------------------------------
/.github/workflows/ci-release.yml:
--------------------------------------------------------------------------------
1 | name: CI and Release
2 |
3 | # Run this workflow on push events (i.e. PR merge) to main or release branches,
4 | # push events for new semantic version tags, all PRs, and manual triggers.
5 | on:
6 | push:
7 | branches:
8 | - main
9 | tags:
10 | - "v[0-9]+.[0-9]+.[0-9]+"
11 | - "v[0-9]+.[0-9]+.[0-9]+-alpha.[0-9]+"
12 | - "v[0-9]+.[0-9]+.[0-9]+-beta.[0-9]+"
13 | - "v[0-9]+.[0-9]+.[0-9]+-rc[0-9]+"
14 | pull_request:
15 | workflow_dispatch:
16 | # Inputs the workflow accepts.
17 | inputs:
18 | version:
19 | # Friendly description to be shown in the UI instead of 'name'
20 | description: "Semver type of new version (major / minor / patch)"
21 | # Input has to be provided for the workflow to run
22 | required: true
23 | type: choice
24 | options:
25 | - patch
26 | - minor
27 | - major
28 |
29 | jobs:
30 | lint:
31 | uses: ./.github/workflows/lint.yml
32 |
33 | test:
34 | uses: ./.github/workflows/test.yml
35 |
36 | goreleaser-check:
37 | runs-on: ubuntu-latest
38 | steps:
39 | - name: checkout
40 | uses: actions/checkout@v4
41 | - uses: goreleaser/goreleaser-action@v5
42 | with:
43 | version: latest
44 | args: check
45 |
46 | # branch_name trims ref/heads/ from github.ref to access a clean branch name
47 | branch_name:
48 | runs-on: ubuntu-latest
49 | outputs:
50 | branch: ${{ steps.trim_ref.outputs.branch }}
51 | steps:
52 | - name: Trim branch name
53 | id: trim_ref
54 | run: |
55 | echo "branch=$(${${{ github.ref }}:11})" >> $GITHUB_OUTPUT
56 |
57 | # If this was a workflow dispatch event, we need to generate and push a tag
58 | # for goreleaser to grab
59 | version_bump:
60 | needs: [lint, test, branch_name, goreleaser-check]
61 | runs-on: ubuntu-latest
62 | permissions: "write-all"
63 | steps:
64 | - uses: actions/checkout@v4
65 | - name: Bump version and push tag
66 | # Placing the if condition here is a workaround for needing to block
67 | # on this step during workflow dispatch events but the step not
68 | # needing to run on tags. If we had the if condition on the full
69 | # version_bump section, it would skip and not run, which would result
70 | # in goreleaser not running either.
71 | if: ${{ github.event_name == 'workflow_dispatch' }}
72 | uses: mathieudutour/github-tag-action@v6.1
73 | with:
74 | github_token: ${{ secrets.GITHUB_TOKEN }}
75 | default_bump: ${{ inputs.version }}
76 | # Setting the branch name so that release branch other than
77 | # master/main doesn't impact tag name
78 | release_branches: ${{ needs.branch_name.outputs.branch }}
79 |
80 | # Generate the release with goreleaser to include pre-built binaries
81 | goreleaser:
82 | needs: version_bump
83 | runs-on: ubuntu-20.04
84 | if: |
85 | github.event_name == 'workflow_dispatch' ||
86 | (github.event_name == 'push' && contains(github.ref, 'refs/tags/'))
87 | permissions: "write-all"
88 | steps:
89 | - uses: actions/checkout@v4
90 | - run: git fetch --force --tags
91 | - uses: actions/setup-go@v5
92 | with:
93 | go-version: 1.21.6
94 | - name: Import GPG key
95 | id: import_gpg
96 | uses: crazy-max/ghaction-import-gpg@v6
97 | with:
98 | gpg_private_key: ${{ secrets.GPG_SIGNING_KEY }}
99 | passphrase: ${{ secrets.GPG_PASSPHRASE }}
100 | # Generate the binaries and release
101 | - uses: goreleaser/goreleaser-action@v5
102 | with:
103 | distribution: goreleaser
104 | version: latest
105 | args: release --clean
106 | env:
107 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
108 | GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }}
109 |
--------------------------------------------------------------------------------
/.github/workflows/codeql.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 |
14 | on:
15 | push:
16 | branches: [ "main" ]
17 | pull_request:
18 | # The branches below must be a subset of the branches above
19 | branches: [ "main" ]
20 | schedule:
21 | - cron: '17 5 * * 1'
22 |
23 | env:
24 | GO_VERSION: '1.21.6'
25 |
26 | jobs:
27 | analyze:
28 | name: Analyze
29 | # Runner size impacts CodeQL analysis time. To learn more, please see:
30 | # - https://gh.io/recommended-hardware-resources-for-running-codeql
31 | # - https://gh.io/supported-runners-and-hardware-resources
32 | # - https://gh.io/using-larger-runners
33 | # Consider using larger runners for possible analysis time improvements.
34 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
35 | timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }}
36 | permissions:
37 | actions: read
38 | contents: read
39 | security-events: write
40 |
41 | strategy:
42 | fail-fast: false
43 | matrix:
44 | language: [ 'go' ]
45 | # CodeQL supports [ 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' ]
46 | # Use only 'java-kotlin' to analyze code written in Java, Kotlin or both
47 | # Use only 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
48 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
49 |
50 | steps:
51 | - name: Checkout repository
52 | uses: actions/checkout@v4
53 |
54 | # Initializes the CodeQL tools for scanning.
55 | - name: Initialize CodeQL
56 | uses: github/codeql-action/init@v3
57 | with:
58 | languages: ${{ matrix.language }}
59 | # If you wish to specify custom queries, you can do so here or in a config file.
60 | # By default, queries listed here will override any specified in a config file.
61 | # Prefix the list here with "+" to use these queries and those in the config file.
62 |
63 | # For more 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
64 | # queries: security-extended,security-and-quality
65 |
66 | - uses: actions/setup-go@v5
67 | with:
68 | go-version: ${{ env.GO_VERSION }}
69 |
70 | - name: Build binary
71 | run: |
72 | make build
73 |
74 | - name: Perform CodeQL Analysis
75 | uses: github/codeql-action/analyze@v3
76 | with:
77 | category: "/language:${{matrix.language}}"
78 |
--------------------------------------------------------------------------------
/.github/workflows/docker-build-publish.yml:
--------------------------------------------------------------------------------
1 | name: Docker Build & Publish
2 |
3 | # Trigger on all push events, new semantic version tags, and all PRs
4 | on:
5 | push:
6 | branches:
7 | - "main"
8 | tags:
9 | - "v[0-9]+.[0-9]+.[0-9]+"
10 | - "v[0-9]+.[0-9]+.[0-9]+-alpha.[0-9]+"
11 | - "v[0-9]+.[0-9]+.[0-9]+-beta.[0-9]+"
12 | - "v[0-9]+.[0-9]+.[0-9]+-rc[0-9]+"
13 | pull_request:
14 |
15 | jobs:
16 | docker-security-build:
17 | permissions:
18 | contents: write
19 | packages: write
20 | uses: celestiaorg/.github/.github/workflows/reusable_dockerfile_pipeline.yml@v0.2.8 # yamllint disable-line rule:line-length
21 | with:
22 | dockerfile: Dockerfile
23 |
--------------------------------------------------------------------------------
/.github/workflows/lables.yml:
--------------------------------------------------------------------------------
1 | name: Required Labels
2 |
3 | on:
4 | pull_request:
5 | types: [opened, labeled, unlabeled, synchronize]
6 |
7 | concurrency:
8 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
9 | cancel-in-progress: true
10 |
11 | jobs:
12 | label:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: mheap/github-action-required-labels@v5
16 | with:
17 | mode: minimum
18 | count: 1
19 | labels: "bug, chore, CI/CD, enhancement, dependencies, documentation, evm, github_actions, orchestrator, p2p, relayer, store, testing" # yamllint disable-line rule:line-length
20 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | name: Lint
2 |
3 | on:
4 | workflow_call:
5 |
6 | jobs:
7 | markdown-lint:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - uses: actions/checkout@v4
11 | - uses: celestiaorg/.github/.github/actions/markdown-lint@main
12 |
13 | golangci:
14 | name: golangci-lint
15 | runs-on: ubuntu-latest
16 | timeout-minutes: 8
17 | env:
18 | GO111MODULE: on
19 | steps:
20 | - uses: actions/setup-go@v5
21 | with:
22 | go-version: '1.21.6'
23 | - uses: actions/checkout@v4
24 | - uses: technote-space/get-diff-action@v6.1.2
25 | with:
26 | PATTERNS: |
27 | **/**.go
28 | go.mod
29 | go.sum
30 | - uses: golangci/golangci-lint-action@v3
31 | with:
32 | version: v1.54
33 | args: --timeout 10m
34 | github-token: ${{ secrets.github_token }}
35 | if: env.GIT_DIFF
36 |
--------------------------------------------------------------------------------
/.github/workflows/stale.yml:
--------------------------------------------------------------------------------
1 | # TODO: Refactor to common workflow
2 | name: "Close stale issues & pull requests"
3 | on:
4 | schedule:
5 | - cron: "0 0 * * *"
6 |
7 | jobs:
8 | stale:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/stale@v9
12 | with:
13 | repo-token: ${{ secrets.GITHUB_TOKEN }}
14 | stale-pr-message: >
15 | This pull request has been automatically marked as stale because it
16 | has not had recent activity. It will be closed if no further
17 | activity occurs. Thank you for your contributions.
18 | days-before-stale: 45
19 | days-before-close: 6
20 | exempt-pr-labels: "pinned, security, proposal, blocked"
21 |
--------------------------------------------------------------------------------
/.github/workflows/tag.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 | # This workflow helps with creating releases.
3 | # This job will only be triggered when a tag (vX.X.x) is pushed
4 | on:
5 | push:
6 | # Sequence of patterns matched against refs/tags
7 | tags:
8 | - "v[0-9]+.[0-9]+.[0-9]+" # Push events to matching v*, i.e. v1.0, v20.15.10
9 |
10 | jobs:
11 | release:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v4
15 | - name: Install Go
16 | uses: actions/setup-go@v5
17 | with:
18 | go-version: '1.21.6'
19 | - name: Unshallow
20 | run: git fetch --prune --unshallow
21 | - name: Create release
22 | uses: goreleaser/goreleaser-action@v5.0.0
23 | with:
24 | args: release --rm-dist
25 | workdir: ./cmd/blobstream
26 | env:
27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
28 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Tests / Code Coverage
2 | # Tests / Code Coverage workflow runs unit tests and uploads a code coverage report
3 | # This workflow is run on pushes to main & every Pull Requests where a .go, .mod, .sum have been changed
4 | on:
5 | workflow_call:
6 |
7 | env:
8 | GO_VERSION: '1.21.6'
9 |
10 | jobs:
11 | test:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v4
15 | - uses: actions/setup-go@v5
16 | with:
17 | go-version: ${{ env.GO_VERSION }}
18 | - name: Run tests
19 | run: make test
20 |
21 | test-coverage:
22 | runs-on: ubuntu-latest
23 | steps:
24 | - uses: actions/checkout@v4
25 | - uses: actions/setup-go@v5
26 | with:
27 | go-version: ${{ env.GO_VERSION }}
28 | - name: Generate coverage.txt
29 | run: make test-cover
30 | - name: Upload coverage.txt
31 | uses: codecov/codecov-action@v3.1.4
32 | with:
33 | file: ./coverage.txt
34 |
35 | test-race:
36 | runs-on: ubuntu-latest
37 | steps:
38 | - uses: actions/checkout@v4
39 | - uses: actions/setup-go@v5
40 | with:
41 | go-version: ${{ env.GO_VERSION }}
42 | - name: Run tests in race mode
43 | run: make test-race
44 |
45 | test-blobstream-e2e:
46 | runs-on: ubuntu-latest
47 | steps:
48 | - uses: actions/checkout@v4
49 | - uses: actions/setup-go@v5
50 | with:
51 | go-version: ${{ env.GO_VERSION }}
52 | - uses: technote-space/get-diff-action@v6.1.2
53 | with:
54 | PATTERNS: |
55 | **/**.go
56 | go.mod
57 | go.sum
58 | - name: Test
59 | working-directory: ./e2e
60 | run: go test -test.timeout 60m -failfast -v github.com/celestiaorg/orchestrator-relayer/e2e # yamllint disable-line rule:line-length
61 | env:
62 | BLOBSTREAM_INTEGRATION_TEST: true
63 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | build/
2 | *.bak
3 | *.iml
4 | *.log
5 | *.swo
6 | *.swp
7 | *.cpuprof
8 | *.memprof
9 | *.out
10 | *.coverprofile
11 | *.test
12 | *.orig
13 | */vendor
14 | vendor
15 | .DS_Store
16 | .bak
17 | .idea/
18 | .vscode/
19 | celestia
20 | cel-shed
21 | cel-key
22 | coverage.txt
23 | go.work
24 | __debug_bin
25 |
--------------------------------------------------------------------------------
/.golangci.yml:
--------------------------------------------------------------------------------
1 | run:
2 | timeout: 5m
3 | modules-download-mode: readonly
4 |
5 | linters:
6 | enable:
7 | - exportloopref
8 | - gofumpt
9 | - misspell
10 | - revive
11 | - prealloc
12 |
--------------------------------------------------------------------------------
/.goreleaser.yaml:
--------------------------------------------------------------------------------
1 | # This is an example .goreleaser.yml file with some sensible defaults.
2 | # Make sure to check the documentation at https://goreleaser.com
3 |
4 | before:
5 | hooks:
6 | - go mod tidy
7 | builds:
8 | - main: ./cmd/blobstream
9 | binary: blobstream
10 | env:
11 | - VersioningPath={{ "github.com/celestiaorg/orchestrator-relayer/cmd/blobstream/version" }}
12 | goarch:
13 | - amd64
14 | - arm64
15 | goos:
16 | - darwin
17 | - linux
18 | ldflags:
19 | # Ref: https://goreleaser.com/customization/templates/#common-fields
20 | #
21 | # .CommitDate is used to help with reproducible builds, ensuring that the
22 | # same date is always used
23 | #
24 | # .FullCommit is git commit hash goreleaser is using for the release
25 | #
26 | # .Version is the version being released
27 | - -X "{{ .Env.VersioningPath }}.buildTime={{ .CommitDate }}"
28 | - -X "{{ .Env.VersioningPath }}.lastCommit={{ .FullCommit }}"
29 | - -X "{{ .Env.VersioningPath }}.semanticVersion={{ .Version }}"
30 | dist: ./build/goreleaser
31 | archives:
32 | - format: tar.gz
33 | # this name template makes the OS and Arch compatible with the results of
34 | # uname.
35 | name_template: >-
36 | {{ .ProjectName }}_
37 | {{- title .Os }}_
38 | {{- if eq .Arch "amd64" }}x86_64
39 | {{- else if eq .Arch "386" }}i386
40 | {{- else }}{{ .Arch }}{{ end }}
41 | {{- if .Arm }}v{{ .Arm }}{{ end }}
42 | checksum:
43 | name_template: "checksums.txt"
44 | signs:
45 | - artifacts: checksum
46 | args:
47 | [
48 | "--batch",
49 | "-u",
50 | "{{ .Env.GPG_FINGERPRINT }}",
51 | "--output",
52 | "${signature}",
53 | "--detach-sign",
54 | "${artifact}",
55 | ]
56 | snapshot:
57 | name_template: "{{ incpatch .Version }}-next"
58 | changelog:
59 | sort: asc
60 | filters:
61 | exclude:
62 | - "^docs:"
63 | - "^test:"
64 |
--------------------------------------------------------------------------------
/.markdownlint.yaml:
--------------------------------------------------------------------------------
1 | "default": true # Default state for all rules
2 | "MD010":
3 | "code_blocks": false # Disable rule for hard tabs in code blocks
4 | "MD013": false # Disable rule for line length
5 | "MD033": false # Disable rule banning inline HTML
6 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Unreleased Changes
2 |
3 | ## vX.Y.Z
4 |
5 | Month, DD, YYYY
6 |
7 | ### BREAKING CHANGES
8 |
9 | - [go package] (Link to PR) Description @username
10 |
11 | ### FEATURES
12 |
13 | - [go package] (Link to PR) Description @username
14 |
15 | ### IMPROVEMENTS
16 |
17 | - [go package] (Link to PR) Description @username
18 |
19 | ### BUG FIXES
20 |
21 | - [go package] (Link to PR) Description @username
22 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # stage 1 Build blobstream binary
2 | FROM --platform=$BUILDPLATFORM docker.io/golang:1.21.6-alpine3.18 as builder
3 |
4 | ARG TARGETOS
5 | ARG TARGETARCH
6 |
7 | ENV CGO_ENABLED=0
8 | ENV GO111MODULE=on
9 |
10 | RUN apk update && apk --no-cache add make gcc musl-dev git bash
11 |
12 | COPY . /orchestrator-relayer
13 | WORKDIR /orchestrator-relayer
14 | RUN uname -a &&\
15 | CGO_ENABLED=${CGO_ENABLED} GOOS=${TARGETOS} GOARCH=${TARGETARCH} \
16 | make build
17 |
18 | # final image
19 | FROM docker.io/alpine:3.19.0
20 |
21 | ARG UID=10001
22 | ARG USER_NAME=celestia
23 |
24 | ENV CELESTIA_HOME=/home/${USER_NAME}
25 |
26 | # hadolint ignore=DL3018
27 | RUN apk update && apk add --no-cache \
28 | bash \
29 | curl \
30 | jq \
31 | # Creates a user with $UID and $GID=$UID
32 | && adduser ${USER_NAME} \
33 | -D \
34 | -g ${USER_NAME} \
35 | -h ${CELESTIA_HOME} \
36 | -s /sbin/nologin \
37 | -u ${UID}
38 |
39 | COPY --from=builder /orchestrator-relayer/build/blobstream /bin/blobstream
40 | COPY --chown=${USER_NAME}:${USER_NAME} docker/entrypoint.sh /opt/entrypoint.sh
41 |
42 | USER ${USER_NAME}
43 |
44 | # p2p port
45 | EXPOSE 30000
46 |
47 | ENTRYPOINT [ "/bin/bash", "/opt/entrypoint.sh" ]
48 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | #!/usr/bin/make -f
2 |
3 | VERSION := $(shell echo $(shell git describe --tags 2>/dev/null || git log -1 --format='%h') | sed 's/^v//')
4 | DOCKER := $(shell which docker)
5 | versioningPath := "github.com/celestiaorg/orchestrator-relayer/cmd/blobstream/version"
6 | LDFLAGS=-ldflags="-X '$(versioningPath).buildTime=$(shell date)' -X '$(versioningPath).lastCommit=$(shell git rev-parse HEAD)' -X '$(versioningPath).semanticVersion=$(shell git describe --tags --dirty=-dev 2>/dev/null || git rev-parse --abbrev-ref HEAD)'"
7 |
8 | all: install
9 | .PHONY: all
10 |
11 | install: mod-verify
12 | @echo "--> Installing blobstream"
13 | @go install -mod=readonly ${LDFLAGS} ./cmd/blobstream
14 | .PHONY: install
15 |
16 | mod-verify: mod
17 | @echo "--> Verifying dependencies have expected content"
18 | GO111MODULE=on go mod verify
19 | .PHONY: mod-verify
20 |
21 | mod:
22 | @echo "--> Updating go.mod"
23 | @go mod tidy
24 | .PHONY: mod
25 |
26 | pre-build:
27 | @echo "--> Fetching latest git tags"
28 | @git fetch --tags
29 | .PHONY: pre-build
30 |
31 | build: mod
32 | @mkdir -p build/
33 | @go build -o build ${LDFLAGS} ./cmd/blobstream
34 | .PHONY: build
35 |
36 | build-docker:
37 | @echo "--> Building Docker image"
38 | @$(DOCKER) build -t celestiaorg/orchestrator-relayer -f Dockerfile .
39 | .PHONY: build-docker
40 |
41 | lint:
42 | @echo "--> Running golangci-lint"
43 | @golangci-lint run
44 | @echo "--> Running markdownlint"
45 | @markdownlint --config .markdownlint.yaml '**/*.md'
46 | .PHONY: lint
47 |
48 | fmt:
49 | @echo "--> Running golangci-lint --fix"
50 | @golangci-lint run --fix
51 | @echo "--> Running markdownlint --fix"
52 | @markdownlint --fix --quiet --config .markdownlint.yaml .
53 | .PHONY: fmt
54 |
55 | test:
56 | @echo "--> Running unit tests"
57 | @go test -mod=readonly ./...
58 | .PHONY: test
59 |
60 | test-all: test-race test-cover
61 | .PHONY: test-all
62 |
63 | test-race:
64 | @echo "--> Running tests with -race"
65 | @VERSION=$(VERSION) go test -mod=readonly -race -test.short ./...
66 | .PHONY: test-race
67 |
68 | test-cover:
69 | @echo "--> Generating coverage.txt"
70 | @export VERSION=$(VERSION); bash -x scripts/test_cover.sh
71 | .PHONY: test-cover
72 |
73 | benchmark:
74 | @echo "--> Running tests with -bench"
75 | @go test -mod=readonly -bench=. ./...
76 | .PHONY: benchmark
77 |
--------------------------------------------------------------------------------
/NOTICE:
--------------------------------------------------------------------------------
1 | Orchestrator Relayer
2 | Copyright 2022 and onwards Strange Loop Labs AG
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # orchestrator-relayer
2 |
3 | Contains the implementation of the Blobstream orchestrator and relayer.
4 |
5 | The orchestrator is the software that signs Blobstream attestations, and the relayer is the one that relays them to the target EVM chain.
6 |
7 | For a high-level overview of how Blobstream works, check [here](https://github.com/celestiaorg/quantum-gravity-bridge/tree/76efeca0be1a17d32ef633c0fdbd3c8f5e4cc53f#how-it-works) and [here](https://blog.celestia.org/celestiums/).
8 |
9 | **This repo has been archived as vanilla blobstream has been decommissioned. The currently in-production version is [BlobstreamX](https://docs.celestia.org/developers/blobstream#what-is-blobstream-x).**
10 |
11 | ## Install
12 |
13 | 1. [Install Go](https://go.dev/doc/install) 1.21
14 | 2. Clone this repo
15 | 3. Install the Blobstream CLI
16 |
17 | ```shell
18 | make install
19 | ```
20 |
21 | ## Usage
22 |
23 | ```sh
24 | # Print help
25 | blobstream --help
26 | ```
27 |
28 | ## How to run
29 |
30 | If you are a Celestia-app validator, all you need to do is run the orchestrator. Check [here](https://github.com/celestiaorg/orchestrator-relayer/blob/main/docs/orchestrator.md) for more details.
31 |
32 | If you want to post commitments on an EVM chain, you will need to deploy a new Blobstream contract and run a relayer, or run a relayer to an already deployed Blobstream contract. Check [relayer docs](https://github.com/celestiaorg/orchestrator-relayer/blob/main/docs/relayer.md) and [deployment docs](https://github.com/celestiaorg/orchestrator-relayer/blob/main/docs/deploy.md) for more information.
33 |
34 | Note: the Blobstream P2P network is a separate network from the consensus or the data availability one. Thus, you will need its specific bootstrappers to be able to connect to it.
35 |
36 | ## Contributing
37 |
38 | ### Tools
39 |
40 | 1. Install [golangci-lint](https://golangci-lint.run/usage/install/)
41 | 2. Install [markdownlint](https://github.com/DavidAnson/markdownlint)
42 |
43 | ### Helpful Commands
44 |
45 | ```sh
46 | # Build a new orchestrator-relayer binary and output to build/blobstream
47 | make build
48 |
49 | # Run tests
50 | make test
51 |
52 | # Format code with linters (this assumes golangci-lint and markdownlint are installed)
53 | make fmt
54 | ```
55 |
56 | ## Useful links
57 |
58 | The smart contract implementation is in [blobstream-contracts](https://github.com/celestiaorg/blobstream-contracts).
59 |
60 | The state machine implementation is in [x/blobstream](https://github.com/celestiaorg/celestia-app/tree/main/x/blobstream).
61 |
62 | Blobstream ADRs are in the [docs](https://github.com/celestiaorg/celestia-app/tree/main/docs/architecture).
63 |
64 | Blobstream design explained in this [blog](https://blog.celestia.org/celestiums).
65 |
--------------------------------------------------------------------------------
/cmd/blobstream/bootstrapper/cmd.go:
--------------------------------------------------------------------------------
1 | package bootstrapper
2 |
3 | import (
4 | "context"
5 | "strings"
6 | "time"
7 |
8 | "github.com/celestiaorg/orchestrator-relayer/cmd/blobstream/base"
9 |
10 | p2pcmd "github.com/celestiaorg/orchestrator-relayer/cmd/blobstream/keys/p2p"
11 | "github.com/celestiaorg/orchestrator-relayer/helpers"
12 | "github.com/celestiaorg/orchestrator-relayer/p2p"
13 | "github.com/celestiaorg/orchestrator-relayer/store"
14 | ds "github.com/ipfs/go-datastore"
15 | dssync "github.com/ipfs/go-datastore/sync"
16 | "github.com/libp2p/go-libp2p/core/peer"
17 | "github.com/spf13/cobra"
18 | )
19 |
20 | func Command() *cobra.Command {
21 | bsCmd := &cobra.Command{
22 | Use: "bootstrapper",
23 | Aliases: []string{"bs"},
24 | Short: "Blobstream P2P network bootstrapper command",
25 | SilenceUsage: true,
26 | }
27 |
28 | bsCmd.AddCommand(
29 | Start(),
30 | Init(),
31 | p2pcmd.Root(ServiceNameBootstrapper),
32 | )
33 |
34 | bsCmd.SetHelpCommand(&cobra.Command{})
35 |
36 | return bsCmd
37 | }
38 |
39 | func Start() *cobra.Command {
40 | cmd := &cobra.Command{
41 | Use: "start",
42 | Short: "Starts the bootstrapper node using the provided home." +
43 | "Could be connected to other bootstrapper nodes too.",
44 | RunE: func(cmd *cobra.Command, args []string) error {
45 | config, err := parseStartFlags(cmd)
46 | if err != nil {
47 | return err
48 | }
49 |
50 | // creating the logger
51 | logger, err := base.GetLogger(config.logLevel, config.logFormat)
52 | if err != nil {
53 | return err
54 | }
55 | logger.Info("starting bootstrapper node")
56 |
57 | ctx, cancel := context.WithCancel(cmd.Context())
58 | defer cancel()
59 |
60 | // checking if the provided home is already initiated
61 | isInit := store.IsInit(logger, config.home, store.InitOptions{
62 | NeedDataStore: false,
63 | NeedEVMKeyStore: false,
64 | NeedP2PKeyStore: true,
65 | })
66 | if !isInit {
67 | return store.ErrNotInited
68 | }
69 |
70 | // creating the data store
71 | openOptions := store.OpenOptions{
72 | HasDataStore: false,
73 | HasEVMKeyStore: false,
74 | HasP2PKeyStore: true,
75 | }
76 | s, err := store.OpenStore(logger, config.home, openOptions)
77 | if err != nil {
78 | return err
79 | }
80 |
81 | // get the p2p private key or generate a new one
82 | privKey, err := p2pcmd.GetP2PKeyOrGenerateNewOne(s.P2PKeyStore, config.p2pNickname)
83 | if err != nil {
84 | return err
85 | }
86 |
87 | // creating the host
88 | h, err := p2p.CreateHost(config.p2pListenAddr, privKey, nil)
89 | if err != nil {
90 | return err
91 | }
92 | logger.Info(
93 | "created host",
94 | "ID",
95 | h.ID().String(),
96 | "Addresses",
97 | h.Addrs(),
98 | )
99 |
100 | // creating the data store
101 | dataStore := dssync.MutexWrap(ds.NewMapDatastore())
102 |
103 | // get the bootstrappers
104 | var aIBootstrappers []peer.AddrInfo
105 | if config.bootstrappers == "" {
106 | aIBootstrappers = nil
107 | } else {
108 | bs := strings.Split(config.bootstrappers, ",")
109 | aIBootstrappers, err = helpers.ParseAddrInfos(logger, bs)
110 | if err != nil {
111 | return err
112 | }
113 | }
114 |
115 | // creating the dht
116 | dht, err := p2p.NewBlobstreamDHT(ctx, h, dataStore, aIBootstrappers, logger)
117 | if err != nil {
118 | return err
119 | }
120 |
121 | // Listen for and trap any OS signal to graceful shutdown and exit
122 | go helpers.TrapSignal(logger, cancel)
123 |
124 | logger.Info("starting bootstrapper")
125 |
126 | ticker := time.NewTicker(time.Minute)
127 | for {
128 | select {
129 | case <-ctx.Done():
130 | return nil
131 | case <-ticker.C:
132 | logger.Info("listening in bootstrapping mode", "peers_connected", dht.RoutingTable().Size())
133 | }
134 | }
135 | },
136 | }
137 | return addStartFlags(cmd)
138 | }
139 |
140 | func Init() *cobra.Command {
141 | cmd := cobra.Command{
142 | Use: "init",
143 | Short: "Initialize the Blobstream bootstrapper store. Passed flags have persisted effect.",
144 | RunE: func(cmd *cobra.Command, args []string) error {
145 | config, err := parseInitFlags(cmd)
146 | if err != nil {
147 | return err
148 | }
149 |
150 | logger, err := base.GetLogger(config.logLevel, config.logFormat)
151 | if err != nil {
152 | return err
153 | }
154 |
155 | initOptions := store.InitOptions{
156 | NeedDataStore: false,
157 | NeedEVMKeyStore: false,
158 | NeedP2PKeyStore: true,
159 | }
160 | isInit := store.IsInit(logger, config.home, initOptions)
161 | if isInit {
162 | logger.Info("provided path is already initiated", "path", config.home)
163 | return nil
164 | }
165 |
166 | err = store.Init(logger, config.home, initOptions)
167 | if err != nil {
168 | return err
169 | }
170 |
171 | return nil
172 | },
173 | }
174 | return addInitFlags(&cmd)
175 | }
176 |
--------------------------------------------------------------------------------
/cmd/blobstream/bootstrapper/config.go:
--------------------------------------------------------------------------------
1 | package bootstrapper
2 |
3 | import (
4 | "github.com/celestiaorg/orchestrator-relayer/cmd/blobstream/base"
5 | "github.com/spf13/cobra"
6 | )
7 |
8 | const (
9 | ServiceNameBootstrapper = "bootstrapper"
10 | )
11 |
12 | func addStartFlags(cmd *cobra.Command) *cobra.Command {
13 | homeDir, err := base.DefaultServicePath(ServiceNameBootstrapper)
14 | if err != nil {
15 | panic(err)
16 | }
17 | cmd.Flags().String(base.FlagHome, homeDir, "The Blobstream bootstrappers home directory")
18 | base.AddP2PNicknameFlag(cmd)
19 | base.AddP2PListenAddressFlag(cmd)
20 | base.AddBootstrappersFlag(cmd)
21 | base.AddLogLevelFlag(cmd)
22 | base.AddLogFormatFlag(cmd)
23 | return cmd
24 | }
25 |
26 | type StartConfig struct {
27 | home string
28 | p2pListenAddr, p2pNickname string
29 | bootstrappers string
30 | logLevel string
31 | logFormat string
32 | }
33 |
34 | func parseStartFlags(cmd *cobra.Command) (StartConfig, error) {
35 | p2pListenAddress, err := cmd.Flags().GetString(base.FlagP2PListenAddress)
36 | if err != nil {
37 | return StartConfig{}, err
38 | }
39 | p2pNickname, err := cmd.Flags().GetString(base.FlagP2PNickname)
40 | if err != nil {
41 | return StartConfig{}, err
42 | }
43 | homeDir, err := cmd.Flags().GetString(base.FlagHome)
44 | if err != nil {
45 | return StartConfig{}, err
46 | }
47 | if homeDir == "" {
48 | var err error
49 | homeDir, err = base.DefaultServicePath(ServiceNameBootstrapper)
50 | if err != nil {
51 | return StartConfig{}, err
52 | }
53 | }
54 | bootstrappers, err := cmd.Flags().GetString(base.FlagBootstrappers)
55 | if err != nil {
56 | return StartConfig{}, err
57 | }
58 |
59 | logLevel, _, err := base.GetLogLevelFlag(cmd)
60 | if err != nil {
61 | return StartConfig{}, err
62 | }
63 |
64 | logFormat, _, err := base.GetLogFormatFlag(cmd)
65 | if err != nil {
66 | return StartConfig{}, err
67 | }
68 |
69 | return StartConfig{
70 | p2pNickname: p2pNickname,
71 | p2pListenAddr: p2pListenAddress,
72 | home: homeDir,
73 | bootstrappers: bootstrappers,
74 | logFormat: logFormat,
75 | logLevel: logLevel,
76 | }, nil
77 | }
78 |
79 | func addInitFlags(cmd *cobra.Command) *cobra.Command {
80 | homeDir, err := base.DefaultServicePath(ServiceNameBootstrapper)
81 | if err != nil {
82 | panic(err)
83 | }
84 | cmd.Flags().String(base.FlagHome, homeDir, "The Blobstream bootstrappers home directory")
85 | base.AddLogLevelFlag(cmd)
86 | base.AddLogFormatFlag(cmd)
87 | return cmd
88 | }
89 |
90 | type InitConfig struct {
91 | home string
92 | logLevel string
93 | logFormat string
94 | }
95 |
96 | func parseInitFlags(cmd *cobra.Command) (InitConfig, error) {
97 | homeDir, err := cmd.Flags().GetString(base.FlagHome)
98 | if err != nil {
99 | return InitConfig{}, err
100 | }
101 | if homeDir == "" {
102 | var err error
103 | homeDir, err = base.DefaultServicePath(ServiceNameBootstrapper)
104 | if err != nil {
105 | return InitConfig{}, err
106 | }
107 | }
108 | logLevel, _, err := base.GetLogLevelFlag(cmd)
109 | if err != nil {
110 | return InitConfig{}, err
111 | }
112 |
113 | logFormat, _, err := base.GetLogFormatFlag(cmd)
114 | if err != nil {
115 | return InitConfig{}, err
116 | }
117 | return InitConfig{
118 | home: homeDir,
119 | logFormat: logFormat,
120 | logLevel: logLevel,
121 | }, nil
122 | }
123 |
--------------------------------------------------------------------------------
/cmd/blobstream/common/helpers.go:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "strings"
7 | "time"
8 |
9 | "github.com/prometheus/client_golang/prometheus"
10 |
11 | "github.com/celestiaorg/orchestrator-relayer/store"
12 |
13 | "github.com/libp2p/go-libp2p/core/host"
14 |
15 | "github.com/celestiaorg/celestia-app/app"
16 | "github.com/celestiaorg/celestia-app/app/encoding"
17 | common2 "github.com/celestiaorg/orchestrator-relayer/cmd/blobstream/keys/p2p"
18 | "github.com/celestiaorg/orchestrator-relayer/helpers"
19 | "github.com/celestiaorg/orchestrator-relayer/p2p"
20 | "github.com/celestiaorg/orchestrator-relayer/rpc"
21 | keystore2 "github.com/ipfs/boxo/keystore"
22 | ds "github.com/ipfs/go-datastore"
23 | "github.com/libp2p/go-libp2p/core/peer"
24 | tmlog "github.com/tendermint/tendermint/libs/log"
25 | )
26 |
27 | // NewTmAndAppQuerier helper function that creates a new TmQuerier and AppQuerier and registers their stop functions in the
28 | // stopFuncs slice.
29 | func NewTmAndAppQuerier(logger tmlog.Logger, tendermintRPC string, celesGRPC string, grpcInsecure bool) (*rpc.TmQuerier, *rpc.AppQuerier, []func() error, error) {
30 | // load app encoding configuration
31 | encCfg := encoding.MakeConfig(app.ModuleEncodingRegisters...)
32 |
33 | // creating tendermint querier
34 | tmQuerier := rpc.NewTmQuerier(tendermintRPC, logger)
35 | err := tmQuerier.Start()
36 | if err != nil {
37 | return nil, nil, nil, err
38 | }
39 | stopFuncs := make([]func() error, 0)
40 | stopFuncs = append(stopFuncs, func() error {
41 | err := tmQuerier.Stop()
42 | if err != nil {
43 | return err
44 | }
45 | return nil
46 | })
47 |
48 | // creating the application querier
49 | appQuerier := rpc.NewAppQuerier(logger, celesGRPC, encCfg)
50 | err = appQuerier.Start(grpcInsecure)
51 | if err != nil {
52 | return nil, nil, stopFuncs, err
53 | }
54 | stopFuncs = append(stopFuncs, func() error {
55 | err = appQuerier.Stop()
56 | if err != nil {
57 | return err
58 | }
59 | return nil
60 | })
61 |
62 | return tmQuerier, appQuerier, stopFuncs, nil
63 | }
64 |
65 | // CreateDHTAndWaitForPeers helper function that creates a new Blobstream DHT and waits for some peers to connect to it.
66 | func CreateDHTAndWaitForPeers(
67 | ctx context.Context,
68 | logger tmlog.Logger,
69 | p2pKeyStore *keystore2.FSKeystore,
70 | p2pNickname string,
71 | p2pListenAddr string,
72 | bootstrappers string,
73 | dataStore ds.Batching,
74 | registerer prometheus.Registerer,
75 | ) (*p2p.BlobstreamDHT, error) {
76 | // get the p2p private key or generate a new one
77 | privKey, err := common2.GetP2PKeyOrGenerateNewOne(p2pKeyStore, p2pNickname)
78 | if err != nil {
79 | return nil, err
80 | }
81 |
82 | // creating the host
83 | h, err := p2p.CreateHost(p2pListenAddr, privKey, registerer)
84 | if err != nil {
85 | return nil, err
86 | }
87 | logger.Info("created P2P host")
88 |
89 | prettyPrintHost(h)
90 |
91 | // get the bootstrappers
92 | var aIBootstrappers []peer.AddrInfo
93 | if bootstrappers == "" {
94 | aIBootstrappers = nil
95 | } else {
96 | bs := strings.Split(bootstrappers, ",")
97 | aIBootstrappers, err = helpers.ParseAddrInfos(logger, bs)
98 | if err != nil {
99 | return nil, err
100 | }
101 | }
102 |
103 | // creating the dht
104 | dht, err := p2p.NewBlobstreamDHT(ctx, h, dataStore, aIBootstrappers, logger)
105 | if err != nil {
106 | return nil, err
107 | }
108 |
109 | // wait for the dht to have some peers
110 | err = dht.WaitForPeers(ctx, 5*time.Minute, 10*time.Second, 1)
111 | if err != nil {
112 | return nil, err
113 | }
114 | return dht, nil
115 | }
116 |
117 | func OpenStore(logger tmlog.Logger, home string, openOptions store.OpenOptions) (*store.Store, []func() error, error) {
118 | stopFuncs := make([]func() error, 0)
119 |
120 | // checking if the provided home is already initiated
121 | isInit := store.IsInit(logger, home, store.InitOptions{
122 | NeedDataStore: openOptions.HasDataStore,
123 | NeedEVMKeyStore: openOptions.HasEVMKeyStore,
124 | NeedP2PKeyStore: openOptions.HasP2PKeyStore,
125 | NeedSignatureStore: openOptions.HasSignatureStore,
126 | })
127 | if !isInit {
128 | return nil, stopFuncs, store.ErrNotInited
129 | }
130 |
131 | // creating the data store
132 | s, err := store.OpenStore(logger, home, openOptions)
133 | if err != nil {
134 | return nil, stopFuncs, err
135 | }
136 | stopFuncs = append(stopFuncs, func() error { return s.Close(logger, openOptions) })
137 |
138 | return s, stopFuncs, nil
139 | }
140 |
141 | func prettyPrintHost(h host.Host) {
142 | fmt.Printf("ID: %s\n", h.ID().String())
143 | fmt.Println("Listen addresses:")
144 | for _, addr := range h.Addrs() {
145 | fmt.Printf("\t%s\n", addr.String())
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/cmd/blobstream/deploy/config.go:
--------------------------------------------------------------------------------
1 | package deploy
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "strings"
7 |
8 | "github.com/celestiaorg/orchestrator-relayer/cmd/blobstream/base"
9 |
10 | "github.com/spf13/cobra"
11 | )
12 |
13 | const (
14 | ServiceNameDeployer = "deployer"
15 | )
16 |
17 | func addDeployFlags(cmd *cobra.Command) *cobra.Command {
18 | base.AddEVMAccAddressFlag(cmd)
19 | base.AddEVMChainIDFlag(cmd)
20 | base.AddCoreGRPCFlag(cmd)
21 | base.AddCoreRPCFlag(cmd)
22 | base.AddEVMRPCFlag(cmd)
23 | base.AddStartingNonceFlag(cmd)
24 | base.AddEVMGasLimitFlag(cmd)
25 | base.AddEVMPassphraseFlag(cmd)
26 | homeDir, err := base.DefaultServicePath(ServiceNameDeployer)
27 | if err != nil {
28 | panic(err)
29 | }
30 | base.AddHomeFlag(cmd, ServiceNameDeployer, homeDir)
31 | base.AddGRPCInsecureFlag(cmd)
32 | base.AddLogLevelFlag(cmd)
33 | base.AddLogFormatFlag(cmd)
34 | return cmd
35 | }
36 |
37 | type deployConfig struct {
38 | base.Config
39 | evmRPC string
40 | coreRPC, coreGRPC string
41 | evmChainID uint64
42 | evmAccAddress string
43 | startingNonce string
44 | evmGasLimit uint64
45 | grpcInsecure bool
46 | logLevel string
47 | logFormat string
48 | }
49 |
50 | func parseDeployFlags(cmd *cobra.Command) (deployConfig, error) {
51 | evmAccAddr, _, err := base.GetEVMAccAddressFlag(cmd)
52 | if err != nil {
53 | return deployConfig{}, err
54 | }
55 | if evmAccAddr == "" {
56 | return deployConfig{}, errors.New("the evm account address should be specified")
57 | }
58 |
59 | evmChainID, _, err := base.GetEVMChainIDFlag(cmd)
60 | if err != nil {
61 | return deployConfig{}, err
62 | }
63 |
64 | coreRPC, _, err := base.GetCoreRPCFlag(cmd)
65 | if err != nil {
66 | return deployConfig{}, err
67 | }
68 | if !strings.HasPrefix(coreRPC, "tcp://") {
69 | coreRPC = fmt.Sprintf("tcp://%s", coreRPC)
70 | }
71 |
72 | coreGRPC, _, err := base.GetCoreGRPCFlag(cmd)
73 | if err != nil {
74 | return deployConfig{}, err
75 | }
76 |
77 | evmRPC, _, err := base.GetEVMRPCFlag(cmd)
78 | if err != nil {
79 | return deployConfig{}, err
80 | }
81 |
82 | startingNonce, _, err := base.GetStartingNonceFlag(cmd)
83 | if err != nil {
84 | return deployConfig{}, err
85 | }
86 |
87 | evmGasLimit, _, err := base.GetEVMGasLimitFlag(cmd)
88 | if err != nil {
89 | return deployConfig{}, err
90 | }
91 |
92 | homeDir, _, err := base.GetHomeFlag(cmd)
93 | if err != nil {
94 | return deployConfig{}, err
95 | }
96 |
97 | passphrase, _, err := base.GetEVMPassphraseFlag(cmd)
98 | if err != nil {
99 | return deployConfig{}, err
100 | }
101 |
102 | grpcInsecure, _, err := base.GetGRPCInsecureFlag(cmd)
103 | if err != nil {
104 | return deployConfig{}, err
105 | }
106 |
107 | logLevel, _, err := base.GetLogLevelFlag(cmd)
108 | if err != nil {
109 | return deployConfig{}, err
110 | }
111 |
112 | logFormat, _, err := base.GetLogFormatFlag(cmd)
113 | if err != nil {
114 | return deployConfig{}, err
115 | }
116 |
117 | return deployConfig{
118 | Config: base.Config{
119 | Home: homeDir,
120 | EVMPassphrase: passphrase,
121 | },
122 | evmRPC: evmRPC,
123 | coreRPC: coreRPC,
124 | coreGRPC: coreGRPC,
125 | evmChainID: evmChainID,
126 | evmAccAddress: evmAccAddr,
127 | startingNonce: startingNonce,
128 | evmGasLimit: evmGasLimit,
129 | grpcInsecure: grpcInsecure,
130 | logFormat: logFormat,
131 | logLevel: logLevel,
132 | }, nil
133 | }
134 |
--------------------------------------------------------------------------------
/cmd/blobstream/deploy/errors.go:
--------------------------------------------------------------------------------
1 | package deploy
2 |
3 | import "errors"
4 |
5 | var (
6 | ErrUnmarshallValset = errors.New("couldn't unmarshall valset")
7 | ErrNotFound = errors.New("not found")
8 | )
9 |
--------------------------------------------------------------------------------
/cmd/blobstream/generate/cmd.go:
--------------------------------------------------------------------------------
1 | package generate
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/ethereum/go-ethereum/common/hexutil"
7 | util "github.com/ipfs/boxo/util"
8 | "github.com/libp2p/go-libp2p/core/crypto"
9 | "github.com/spf13/cobra"
10 | )
11 |
12 | // Command helper command to generate a new Ed25519 private key in hex format.
13 | // This will be used to generate the key needed when starting the orchestrator or relayer.
14 | // Will be removed once we support a key management tool.
15 | func Command() *cobra.Command {
16 | return &cobra.Command{
17 | Use: "generate",
18 | Short: "Generates a new Ed25519 private key in hex format",
19 | RunE: func(cmd *cobra.Command, args []string) error {
20 | fmt.Println("generating a new Ed25519 private key in hex format...")
21 |
22 | sr := util.NewTimeSeededRand()
23 | priv, _, err := crypto.GenerateEd25519Key(sr)
24 | if err != nil {
25 | fmt.Println(err.Error())
26 | return err
27 | }
28 |
29 | bytez, err := priv.Raw()
30 | if err != nil {
31 | fmt.Println(err.Error())
32 | return err
33 | }
34 | fmt.Println(hexutil.Encode(bytez)[2:])
35 | return nil
36 | },
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/cmd/blobstream/keys/common/common.go:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | import (
4 | "bufio"
5 | "fmt"
6 | "os"
7 |
8 | tmlog "github.com/tendermint/tendermint/libs/log"
9 | )
10 |
11 | // ConfirmDeletePrivateKey is used to get a confirmation before deleting a private key
12 | func ConfirmDeletePrivateKey(logger tmlog.Logger) bool {
13 | logger.Info("Are you sure you want to delete your private key? This action cannot be undone and may result in permanent loss of access to your account.")
14 | fmt.Print("Please enter 'yes' or 'no' to confirm your decision: ")
15 |
16 | scanner := bufio.NewScanner(os.Stdin)
17 | scanner.Scan()
18 | input := scanner.Text()
19 |
20 | return input == "yes"
21 | }
22 |
--------------------------------------------------------------------------------
/cmd/blobstream/keys/evm/config.go:
--------------------------------------------------------------------------------
1 | package evm
2 |
3 | import (
4 | "github.com/celestiaorg/orchestrator-relayer/cmd/blobstream/base"
5 | "github.com/cosmos/cosmos-sdk/client/flags"
6 | "github.com/spf13/cobra"
7 | )
8 |
9 | const (
10 | FlagNewEVMPassphrase = "evm.new-passphrase"
11 | )
12 |
13 | func keysConfigFlags(cmd *cobra.Command, service string) *cobra.Command {
14 | homeDir, err := base.DefaultServicePath(service)
15 | if err != nil {
16 | panic(err)
17 | }
18 | cmd.Flags().String(base.FlagHome, homeDir, "The Blobstream evm keys home directory")
19 | cmd.Flags().String(base.FlagEVMPassphrase, "", "the evm account passphrase (if not specified as a flag, it will be asked interactively)")
20 | base.AddLogLevelFlag(cmd)
21 | base.AddLogFormatFlag(cmd)
22 | return cmd
23 | }
24 |
25 | type KeysConfig struct {
26 | *base.Config
27 | logLevel string
28 | logFormat string
29 | }
30 |
31 | func parseKeysConfigFlags(cmd *cobra.Command, serviceName string) (KeysConfig, error) {
32 | homeDir, err := cmd.Flags().GetString(flags.FlagHome)
33 | if err != nil {
34 | return KeysConfig{}, err
35 | }
36 | if homeDir == "" {
37 | var err error
38 | homeDir, err = base.DefaultServicePath(serviceName)
39 | if err != nil {
40 | return KeysConfig{}, err
41 | }
42 | }
43 | passphrase, err := cmd.Flags().GetString(base.FlagEVMPassphrase)
44 | if err != nil {
45 | return KeysConfig{}, err
46 | }
47 |
48 | logLevel, _, err := base.GetLogLevelFlag(cmd)
49 | if err != nil {
50 | return KeysConfig{}, err
51 | }
52 |
53 | logFormat, _, err := base.GetLogFormatFlag(cmd)
54 | if err != nil {
55 | return KeysConfig{}, err
56 | }
57 |
58 | return KeysConfig{
59 | Config: &base.Config{
60 | Home: homeDir,
61 | EVMPassphrase: passphrase,
62 | },
63 | logFormat: logFormat,
64 | logLevel: logLevel,
65 | }, nil
66 | }
67 |
68 | func keysNewPassphraseConfigFlags(cmd *cobra.Command, service string) *cobra.Command {
69 | homeDir, err := base.DefaultServicePath(service)
70 | if err != nil {
71 | panic(err)
72 | }
73 | cmd.Flags().String(base.FlagHome, homeDir, "The Blobstream evm keys home directory")
74 | cmd.Flags().String(base.FlagEVMPassphrase, "", "the evm account passphrase (if not specified as a flag, it will be asked interactively)")
75 | cmd.Flags().String(FlagNewEVMPassphrase, "", "the evm account new passphrase (if not specified as a flag, it will be asked interactively)")
76 | base.AddLogLevelFlag(cmd)
77 | base.AddLogFormatFlag(cmd)
78 | return cmd
79 | }
80 |
81 | type KeysNewPassphraseConfig struct {
82 | *base.Config
83 | newPassphrase string
84 | logLevel string
85 | logFormat string
86 | }
87 |
88 | func parseKeysNewPassphraseConfigFlags(cmd *cobra.Command, serviceName string) (KeysNewPassphraseConfig, error) {
89 | homeDir, err := cmd.Flags().GetString(flags.FlagHome)
90 | if err != nil {
91 | return KeysNewPassphraseConfig{}, err
92 | }
93 | if homeDir == "" {
94 | var err error
95 | homeDir, err = base.DefaultServicePath(serviceName)
96 | if err != nil {
97 | return KeysNewPassphraseConfig{}, err
98 | }
99 | }
100 | passphrase, err := cmd.Flags().GetString(base.FlagEVMPassphrase)
101 | if err != nil {
102 | return KeysNewPassphraseConfig{}, err
103 | }
104 |
105 | newPassphrase, err := cmd.Flags().GetString(FlagNewEVMPassphrase)
106 | if err != nil {
107 | return KeysNewPassphraseConfig{}, err
108 | }
109 |
110 | logLevel, _, err := base.GetLogLevelFlag(cmd)
111 | if err != nil {
112 | return KeysNewPassphraseConfig{}, err
113 | }
114 |
115 | logFormat, _, err := base.GetLogFormatFlag(cmd)
116 | if err != nil {
117 | return KeysNewPassphraseConfig{}, err
118 | }
119 |
120 | return KeysNewPassphraseConfig{
121 | Config: &base.Config{Home: homeDir, EVMPassphrase: passphrase},
122 | newPassphrase: newPassphrase,
123 | logFormat: logFormat,
124 | logLevel: logLevel,
125 | }, nil
126 | }
127 |
--------------------------------------------------------------------------------
/cmd/blobstream/keys/evm/evm_test.go:
--------------------------------------------------------------------------------
1 | package evm_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/celestiaorg/orchestrator-relayer/cmd/blobstream/keys/evm"
7 | "github.com/ethereum/go-ethereum/crypto"
8 | "github.com/stretchr/testify/assert"
9 | )
10 |
11 | // TestMnemonicToPrivateKey tests the generation of private keys using mnemonics.
12 | // The test vectors were generated and verified using a Ledger Nano X with Ethereum accounts.
13 | func TestMnemonicToPrivateKey(t *testing.T) {
14 | tests := []struct {
15 | name string
16 | mnemonic string
17 | passphrase string
18 | expectedError bool
19 | expectedResult string
20 | expectedAddress string
21 | }{
22 | {
23 | name: "Valid Mnemonic with passphrase",
24 | mnemonic: "eight moment square film same crystal trophy diagram awkward defense crazy garlic exile rabbit coast truck foam broken shed attract bamboo drum dry cage",
25 | passphrase: "abcd",
26 | expectedError: false,
27 | expectedResult: "5dfb97434a8a31cca1d1c2c6b6b9cf09b4946823331ec434894f204acf79d850",
28 | expectedAddress: "0x6Ca3653B3B50892e051Da60b1E14540f2f7EBdBF",
29 | },
30 | {
31 | name: "Valid Mnemonic without passphrase",
32 | mnemonic: "eight moment square film same crystal trophy diagram awkward defense crazy garlic exile rabbit coast truck foam broken shed attract bamboo drum dry cage",
33 | passphrase: "",
34 | expectedError: false,
35 | expectedResult: "4252916c6e7f80dc96928c66a885be5a362790ad2fb3552ab781cd9112aef3a2",
36 | expectedAddress: "0x33bb23EB923C284fC76D93C26aFd1FdCAf770Ea2",
37 | },
38 | {
39 | name: "Invalid Mnemonic",
40 | mnemonic: "wrong mnemonic beginning poverty injury cradle wrong smoke sphere trap tumble girl monkey sibling festival mask second agent slice gadget census glare swear recycle",
41 | expectedError: true,
42 | },
43 | }
44 |
45 | for _, test := range tests {
46 | t.Run(test.name, func(t *testing.T) {
47 | privateKey, err := evm.MnemonicToPrivateKey(test.mnemonic, test.passphrase)
48 |
49 | if test.expectedError {
50 | assert.Error(t, err)
51 | } else {
52 | assert.NoError(t, err)
53 | addr := crypto.PubkeyToAddress(privateKey.PublicKey)
54 | assert.Equal(t, test.expectedAddress, addr.Hex())
55 |
56 | expectedPrivateKey, err := crypto.HexToECDSA(test.expectedResult)
57 | assert.NoError(t, err)
58 | assert.Equal(t, expectedPrivateKey.D.Bytes(), privateKey.D.Bytes())
59 | }
60 | })
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/cmd/blobstream/keys/keys.go:
--------------------------------------------------------------------------------
1 | package keys
2 |
3 | import (
4 | "github.com/celestiaorg/orchestrator-relayer/cmd/blobstream/keys/evm"
5 | "github.com/celestiaorg/orchestrator-relayer/cmd/blobstream/keys/p2p"
6 | "github.com/spf13/cobra"
7 | )
8 |
9 | func Command(serviceName string) *cobra.Command {
10 | keysCmd := &cobra.Command{
11 | Use: "keys",
12 | Short: "Blobstream keys manager",
13 | SilenceUsage: true,
14 | }
15 |
16 | keysCmd.AddCommand(
17 | evm.Root(serviceName),
18 | p2p.Root(serviceName),
19 | )
20 |
21 | keysCmd.SetHelpCommand(&cobra.Command{})
22 |
23 | return keysCmd
24 | }
25 |
--------------------------------------------------------------------------------
/cmd/blobstream/keys/p2p/config.go:
--------------------------------------------------------------------------------
1 | package p2p
2 |
3 | import (
4 | "github.com/celestiaorg/orchestrator-relayer/cmd/blobstream/base"
5 | "github.com/cosmos/cosmos-sdk/client/flags"
6 | "github.com/spf13/cobra"
7 | )
8 |
9 | func keysConfigFlags(cmd *cobra.Command, service string) *cobra.Command {
10 | homeDir, err := base.DefaultServicePath(service)
11 | if err != nil {
12 | panic(err)
13 | }
14 | cmd.Flags().String(base.FlagHome, homeDir, "The Blobstream p2p keys home directory")
15 | base.AddLogLevelFlag(cmd)
16 | base.AddLogFormatFlag(cmd)
17 | return cmd
18 | }
19 |
20 | type KeysConfig struct {
21 | home string
22 | logLevel string
23 | logFormat string
24 | }
25 |
26 | func parseKeysConfigFlags(cmd *cobra.Command, serviceName string) (KeysConfig, error) {
27 | homeDir, err := cmd.Flags().GetString(flags.FlagHome)
28 | if err != nil {
29 | return KeysConfig{}, err
30 | }
31 | if homeDir == "" {
32 | var err error
33 | homeDir, err = base.DefaultServicePath(serviceName)
34 | if err != nil {
35 | return KeysConfig{}, err
36 | }
37 | }
38 | logLevel, _, err := base.GetLogLevelFlag(cmd)
39 | if err != nil {
40 | return KeysConfig{}, err
41 | }
42 |
43 | logFormat, _, err := base.GetLogFormatFlag(cmd)
44 | if err != nil {
45 | return KeysConfig{}, err
46 | }
47 | return KeysConfig{
48 | home: homeDir,
49 | logFormat: logFormat,
50 | logLevel: logLevel,
51 | }, nil
52 | }
53 |
--------------------------------------------------------------------------------
/cmd/blobstream/keys/p2p/p2p_test.go:
--------------------------------------------------------------------------------
1 | package p2p_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/celestiaorg/orchestrator-relayer/cmd/blobstream/keys/p2p"
7 | "github.com/ipfs/boxo/keystore"
8 | "github.com/stretchr/testify/assert"
9 | "github.com/stretchr/testify/require"
10 | )
11 |
12 | func TestGetP2PKeyOrGenerateNewOne(t *testing.T) {
13 | tempDir := t.TempDir()
14 | ks, err := keystore.NewFSKeystore(tempDir)
15 | require.NoError(t, err)
16 |
17 | nickname := "test"
18 | // test non-existing nickname
19 | _, err = p2p.GetP2PKeyOrGenerateNewOne(ks, nickname)
20 | // because the key is still not added
21 | assert.Error(t, err)
22 |
23 | // test empty nickname
24 | priv, err := p2p.GetP2PKeyOrGenerateNewOne(ks, "")
25 | // should create a new key with nickname 0
26 | assert.NoError(t, err)
27 | assert.NotNil(t, priv)
28 |
29 | // get the key with nickname 0
30 | priv2, err := p2p.GetP2PKeyOrGenerateNewOne(ks, "0")
31 | assert.NoError(t, err)
32 | assert.NotNil(t, priv2)
33 | assert.Equal(t, priv, priv2)
34 |
35 | // put a new key
36 | priv3, err := p2p.GenerateNewEd25519()
37 | require.NoError(t, err)
38 | err = ks.Put(nickname, priv3)
39 | require.NoError(t, err)
40 | priv4, err := p2p.GetP2PKeyOrGenerateNewOne(ks, nickname)
41 | assert.NoError(t, err)
42 | assert.NotNil(t, priv4)
43 | assert.Equal(t, priv3, priv4)
44 | }
45 |
46 | func TestGenerateNewEd25519(t *testing.T) {
47 | priv, err := p2p.GenerateNewEd25519()
48 | assert.NoError(t, err)
49 | assert.NotNil(t, priv)
50 | }
51 |
--------------------------------------------------------------------------------
/cmd/blobstream/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "os"
6 |
7 | "github.com/celestiaorg/orchestrator-relayer/cmd/blobstream/root"
8 | )
9 |
10 | func main() {
11 | rootCmd := root.Cmd()
12 | if err := rootCmd.ExecuteContext(context.Background()); err != nil {
13 | os.Exit(1)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/cmd/blobstream/query/config.go:
--------------------------------------------------------------------------------
1 | package query
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 |
7 | "github.com/celestiaorg/orchestrator-relayer/cmd/blobstream/base"
8 |
9 | "github.com/spf13/cobra"
10 | )
11 |
12 | const (
13 | FlagP2PNode = "p2p-node"
14 | FlagOutputFile = "output-file"
15 | )
16 |
17 | func addFlags(cmd *cobra.Command) *cobra.Command {
18 | base.AddCoreGRPCFlag(cmd)
19 | base.AddCoreRPCFlag(cmd)
20 | cmd.Flags().String(FlagP2PNode, "", "P2P target node multiaddress (eg. /ip4/127.0.0.1/tcp/30000/p2p/12D3KooWBSMasWzRSRKXREhediFUwABNZwzJbkZcYz5rYr9Zdmfn)")
21 | cmd.Flags().String(FlagOutputFile, "", "Path to an output file path if the results need to be written to a json file. Leaving it as empty will result in printing the result to stdout")
22 | base.AddGRPCInsecureFlag(cmd)
23 | cmd.Flags().String(base.FlagHome, "", "The Blobstream orchestrator|relayer home directory. If this flag is not set, it will try the orchestrator's default home directory, then the relayer's default home directory to get the necessary configuration")
24 | return cmd
25 | }
26 |
27 | type Config struct {
28 | coreGRPC, coreRPC string
29 | targetNode string
30 | outputFile string
31 | grpcInsecure bool
32 | }
33 |
34 | func NewPartialConfig(coreGRPC, coreRPC, targetNode string, grpcInsecure bool) *Config {
35 | return &Config{
36 | coreGRPC: coreGRPC,
37 | coreRPC: coreRPC,
38 | targetNode: targetNode,
39 | grpcInsecure: grpcInsecure,
40 | }
41 | }
42 |
43 | func DefaultConfig() *Config {
44 | return &Config{
45 | coreGRPC: "localhost:9090",
46 | coreRPC: "tcp://localhost:26657",
47 | targetNode: "",
48 | outputFile: "",
49 | grpcInsecure: true,
50 | }
51 | }
52 |
53 | func parseFlags(cmd *cobra.Command, startConf *Config) (Config, error) {
54 | coreRPC, changed, err := base.GetCoreRPCFlag(cmd)
55 | if err != nil {
56 | return Config{}, err
57 | }
58 | if changed {
59 | if !strings.HasPrefix(coreRPC, "tcp://") {
60 | coreRPC = fmt.Sprintf("tcp://%s", coreRPC)
61 | }
62 | startConf.coreRPC = coreRPC
63 | }
64 |
65 | coreGRPC, changed, err := base.GetCoreGRPCFlag(cmd)
66 | if err != nil {
67 | return Config{}, err
68 | }
69 | if changed {
70 | startConf.coreGRPC = coreGRPC
71 | }
72 |
73 | targetNode, changed, err := getP2PNodeFlag(cmd)
74 | if err != nil {
75 | return Config{}, err
76 | }
77 | if changed {
78 | startConf.targetNode = targetNode
79 | }
80 |
81 | outputFile, err := cmd.Flags().GetString(FlagOutputFile)
82 | if err != nil {
83 | return Config{}, err
84 | }
85 | startConf.outputFile = outputFile
86 |
87 | grpcInsecure, changed, err := base.GetGRPCInsecureFlag(cmd)
88 | if err != nil {
89 | return Config{}, err
90 | }
91 | if changed {
92 | startConf.grpcInsecure = grpcInsecure
93 | }
94 |
95 | return *startConf, nil
96 | }
97 |
98 | func getP2PNodeFlag(cmd *cobra.Command) (string, bool, error) {
99 | changed := cmd.Flags().Changed(FlagP2PNode)
100 | val, err := cmd.Flags().GetString(FlagP2PNode)
101 | if err != nil {
102 | return "", changed, err
103 | }
104 | return val, changed, nil
105 | }
106 |
--------------------------------------------------------------------------------
/cmd/blobstream/root/cmd.go:
--------------------------------------------------------------------------------
1 | package root
2 |
3 | import (
4 | "github.com/celestiaorg/orchestrator-relayer/cmd/blobstream/bootstrapper"
5 | "github.com/celestiaorg/orchestrator-relayer/cmd/blobstream/generate"
6 | "github.com/celestiaorg/orchestrator-relayer/cmd/blobstream/query"
7 | "github.com/celestiaorg/orchestrator-relayer/cmd/blobstream/version"
8 |
9 | "github.com/celestiaorg/celestia-app/x/qgb/client"
10 | "github.com/celestiaorg/orchestrator-relayer/cmd/blobstream/deploy"
11 | "github.com/celestiaorg/orchestrator-relayer/cmd/blobstream/orchestrator"
12 | "github.com/celestiaorg/orchestrator-relayer/cmd/blobstream/relayer"
13 |
14 | "github.com/spf13/cobra"
15 | )
16 |
17 | // Cmd creates a new root command for the Blobstream CLI. It is called once in the
18 | // main function.
19 | func Cmd() *cobra.Command {
20 | rootCmd := &cobra.Command{
21 | Use: "blobstream",
22 | Short: "The Blobstream CLI",
23 | SilenceUsage: true,
24 | }
25 |
26 | rootCmd.AddCommand(
27 | orchestrator.Command(),
28 | relayer.Command(),
29 | deploy.Command(),
30 | client.VerifyCmd(),
31 | generate.Command(),
32 | query.Command(),
33 | bootstrapper.Command(),
34 | version.Cmd,
35 | )
36 |
37 | rootCmd.SetHelpCommand(&cobra.Command{})
38 |
39 | return rootCmd
40 | }
41 |
--------------------------------------------------------------------------------
/cmd/blobstream/version/build_info.go:
--------------------------------------------------------------------------------
1 | package version
2 |
3 | import (
4 | "fmt"
5 | "runtime"
6 | )
7 |
8 | var (
9 | buildTime string
10 | lastCommit string
11 | semanticVersion string
12 |
13 | systemVersion = fmt.Sprintf("%s/%s", runtime.GOARCH, runtime.GOOS)
14 | golangVersion = runtime.Version()
15 | )
16 |
17 | // BuildInfo represents all necessary information about the current build.
18 | type BuildInfo struct {
19 | BuildTime string
20 | LastCommit string
21 | SemanticVersion string
22 | SystemVersion string
23 | GolangVersion string
24 | }
25 |
26 | // GetBuildInfo returns information about the current build.
27 | func GetBuildInfo() *BuildInfo {
28 | return &BuildInfo{
29 | buildTime,
30 | lastCommit,
31 | semanticVersion,
32 | systemVersion,
33 | golangVersion,
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/cmd/blobstream/version/version.go:
--------------------------------------------------------------------------------
1 | package version
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/spf13/cobra"
7 | )
8 |
9 | var Cmd = &cobra.Command{
10 | Use: "version",
11 | Short: "Show information about the current binary build",
12 | Args: cobra.NoArgs,
13 | Run: printBuildInfo,
14 | }
15 |
16 | func printBuildInfo(_ *cobra.Command, _ []string) {
17 | buildInfo := GetBuildInfo()
18 | fmt.Printf("Semantic version: %s\n", buildInfo.SemanticVersion)
19 | fmt.Printf("Commit: %s\n", buildInfo.LastCommit)
20 | fmt.Printf("Build Date: %s\n", buildInfo.BuildTime)
21 | fmt.Printf("System version: %s\n", buildInfo.SystemVersion)
22 | fmt.Printf("Golang version: %s\n", buildInfo.GolangVersion)
23 | }
24 |
--------------------------------------------------------------------------------
/docker/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | echo "Starting Celestia Blobstream with command:"
6 | echo "$@"
7 | echo ""
8 |
9 | exec "$@"
10 |
--------------------------------------------------------------------------------
/docs/bootstrapper.md:
--------------------------------------------------------------------------------
1 | # Blobstream bootstrapper
2 |
3 | To bootstrap the Blobstream P2P network, we use the bootstrapper Blobstream
4 | node type to accept connections from freshly created orchestrators/relayers
5 | and share its peer table with them.
6 |
7 | ## How to run
8 |
9 | ### Install the Blobstream binary
10 |
11 | Make sure to have the Blobstream binary installed. Check
12 | [the Blobstream binary page](https://docs.celestia.org/nodes/blobstream-binary)
13 | for more details.
14 |
15 | ### Init the store
16 |
17 | Before starting the bootstrapper, we will need to init the store:
18 |
19 | ```sh
20 | blobstream bootstrapper init
21 | ```
22 |
23 | By default, the store will be created in `~/.bootstrapper`. However,
24 | if you want to specify a custom location, you can use the `--home` flag.
25 | Or, you can use the following environment variable:
26 |
27 |
28 |
29 | | Variable | Explanation | Default value | Required |
30 | | ------------------- | ----------------------------------- | ----------------- | -------- |
31 | | `BOOTSTRAPPER_HOME` | Home directory for the bootstrapper | `~/.bootstrapper` | Optional |
32 |
33 | ### Add keys
34 |
35 | The P2P private key is optional, and a new one will be generated automatically
36 | on the start if none is provided.
37 |
38 | The `p2p` sub-command will help you set up this key if you want to use a specific
39 | one:
40 |
41 | ```sh
42 | blobstream bootstrapper p2p --help
43 | ```
44 |
45 | ### Open the P2P port
46 |
47 | In order for the bootstrapper node to work, you will need to expose the P2P
48 | port, which is by default `30000`.
49 |
50 | ### Start the bootstrapper
51 |
52 | Now that we have the store initialized, we can start the bootstrapper:
53 |
54 | ```shell
55 | blobstream bootstrapper start
56 | ```
57 |
58 | #### Systemd service
59 |
60 | An example of a systemd service that can be used for bootstrappers can be
61 | found in the
62 | [orchestrator documentation](https://docs.celestia.org/nodes/blobstream-orchestrator).
63 |
--------------------------------------------------------------------------------
/docs/deploy.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_label: Deploy the Blobstream contract
3 | description: Learn how to deploy the Blobstream smart contract.
4 | ---
5 |
6 | # Deploy the Blobstream contract
7 |
8 |
9 |
10 | The `deploy` is a helper command that allows deploying the Blobstream smart contract to a new EVM chain:
11 |
12 | ```sh
13 | blobstream deploy --help
14 |
15 | Deploys the Blobstream contract and initializes it using the provided Celestia chain
16 |
17 | Usage:
18 | blobstream deploy [flags]
19 | blobstream deploy [command]
20 |
21 | Available Commands:
22 | keys Blobstream keys manager
23 | ```
24 |
25 | ## How to run
26 |
27 | ### Install the Blobstream binary
28 |
29 | Make sure to have the Blobstream binary installed. Check [the Blobstream binary page](https://docs.celestia.org/nodes/blobstream-binary) for more details.
30 |
31 | ### Add keys
32 |
33 | In order to deploy a Blobstream smart contract, you will need a funded EVM address and its private key. The `keys` command will help you set up this key:
34 |
35 | ```sh
36 | blobstream deploy keys --help
37 | ```
38 |
39 | To import your EVM private key, there is the `import` subcommand to assist you with that:
40 |
41 | ```sh
42 | blobstream deploy keys evm import --help
43 | ```
44 |
45 | This subcommand allows you to either import a raw ECDSA private key provided as plaintext, or import it from a file. The files are JSON keystore files encrypted using a passphrase like in [this example](https://geth.ethereum.org/docs/developers/dapp-developer/native-accounts).
46 |
47 | After adding the key, you can check that it's added via running:
48 |
49 | ```sh
50 | blobstream deploy keys evm list
51 | ```
52 |
53 | For more information about the `keys` command, check [the `keys` documentation](https://docs.celestia.org/nodes/blobstream-keys).
54 |
55 | ### Deploy the contract
56 |
57 | Now, we can deploy the Blobstream contract to a new EVM chain:
58 |
59 | ```sh
60 | blobstream deploy \
61 | --evm.chain-id 4 \
62 | --core.grpc localhost:9090 \
63 | --core.rpc localhost:26657 \
64 | --starting-nonce latest \
65 | --evm.rpc http://localhost:8545
66 | ```
67 |
68 | The `--starting-nonce` can have the following values:
69 |
70 | - `latest`: to deploy the Blobstream contract starting from the latest validator set.
71 | - `earliest`: to deploy the Blobstream contract starting from genesis.
72 | - `nonce`: you can provide a custom nonce on where you want Blobstream to start. If the provided nonce is not a `Valset` attestation, then the valset before it will be used to deploy the Blobstream smart contract.
73 |
74 | And, now you will see the Blobstream smart contract address in the logs along with the transaction hash.
75 |
--------------------------------------------------------------------------------
/e2e/Dockerfile_e2e:
--------------------------------------------------------------------------------
1 | # stage 1 Build blobstream binary
2 | FROM golang:1.21.6-alpine as builder
3 | RUN apk update && apk --no-cache add make gcc musl-dev git
4 | COPY . /orchestrator-relayer
5 | WORKDIR /orchestrator-relayer
6 | RUN make build
7 |
8 | # final image
9 | FROM ghcr.io/celestiaorg/celestia-app:v1.6.0
10 |
11 | USER root
12 |
13 | # hadolint ignore=DL3018
14 | RUN apk update && apk --no-cache add bash jq coreutils curl
15 |
16 | COPY --from=builder /orchestrator-relayer/build/blobstream /bin/blobstream
17 |
18 | # p2p port
19 | EXPOSE 9090 26657 30000
20 |
21 | CMD [ "/bin/blobstream" ]
22 |
--------------------------------------------------------------------------------
/e2e/Makefile:
--------------------------------------------------------------------------------
1 | #!/usr/bin/make -f
2 |
3 | PACKAGES=$(shell go list ./...)
4 |
5 | all: test
6 |
7 | test:
8 | @BLOBSTREAM_INTEGRATION_TEST=true go test -mod=readonly -failfast -test.timeout 50m -v $(PACKAGES)
9 |
--------------------------------------------------------------------------------
/e2e/README.md:
--------------------------------------------------------------------------------
1 | # Blobstream end to end integration test
2 |
3 | This directory contains the Blobstream e2e integration tests. It serves as a way to fully test the Blobstream orchestrator and relayer in real network scenarios
4 |
5 | ## Topology
6 |
7 | as discussed under [#398](https://github.com/celestiaorg/celestia-app/issues/398) The e2e network defined under `blobstream_network.go` has the following components:
8 |
9 | - 4 Celestia-app nodes that can be validators
10 | - 4 Orchestrator nodes that will each run aside of a celestia-app
11 | - 1 Ethereum node. Probably Ganache as it is easier to set up
12 | - 1 Relayer node that will listen to Celestia chain and relay attestations
13 | - 1 Deployer node that can deploy a new Blobstream contract when needed.
14 |
15 | For more information on the environment variables required to run these tests, please check the `docker-compose.yml` file and the shell scripts defined under `celestia-app` directory.
16 |
17 | ## P2P network
18 |
19 | In some test scenarios, we only care about running a single orchestrator node. The problem is that in order for the DHT to start putting values, it needs to have at least 1 connected peer. Thus, in our testing scenarios, we create an extra DHT that is used mainly used to create the `P2PQuerier`, but also to connect to that orchestrator DHT and allow it to run.
20 |
21 | ```go
22 | // create dht for querying
23 | bootstrapper, err := helpers.ParseAddrInfos(network.Logger, BOOTSTRAPPERS)
24 | HandleNetworkError(t, network, err, false)
25 | _, _, dht := blobstreamtesting.NewTestDHT(ctx, bootstrapper)
26 | defer dht.Close()
27 | ```
28 |
29 | with `bootstrapper` being that orchestrator P2P ID and listening address.
30 |
31 | ## How to run
32 |
33 | ### Requirements
34 |
35 | To run the e2e tests, a working installation of [docker-compose](https://docs.docker.com/compose/install/) is needed.
36 |
37 | ### Makefile
38 |
39 | A Makefile has been defined under this directory to run the tests, with a `test` target:
40 |
41 | ```shell
42 | make test
43 | ```
44 |
45 | ### Run a specific test
46 |
47 | To run a single test, run the following:
48 |
49 | ```shell
50 | BLOBSTREAM_INTEGRATION_TEST=true go test -mod=readonly -test.timeout 30m -v -run
51 | ```
52 |
53 | ### Run all the tests using `go` directly
54 |
55 | ```shell
56 | BLOBSTREAM_INTEGRATION_TEST=true go test -mod=readonly -test.timeout 30m -v
57 | ```
58 |
59 | ## Common issues
60 |
61 | Currently, when the tests are run using the above ways, there are possible issues that might happen.
62 |
63 | ### hanging docker containers after a sudden network stop
64 |
65 | If the tests were stopped unexpectedly, for example, sending a `SIGINT`, ie, `ctrl+c`, the resources will not be released correctly (might be fixed in the future). This will result in seeing similar logs to the following :
66 |
67 | ```text
68 | ERROR: for core0 Cannot create container for service core0: Conflict. The container name "/core0" is already in use by container "4bdaf40e2cd26bf549738ea95f53ba49cb5407c3d892b50b5a75e72e08e
69 | 3e0a8". You have to remove (or rename) that container to be able to reuse that name.
70 | Host is already in use by another container
71 | Creating 626fbf28-7c90-4842-be8e-3346f864b369_ganache_1 ... error
72 |
73 | ERROR: for 626fbf28-7c90-4842-be8e-3346f864b369_ganache_1 Cannot start service ganache: driver failed programming external connectivity on endpoint 626fbf28-7c90-4842-be8e-3346f864b369_gana
74 | che_1 (23bf2faf8fbce45f4a112b59183739f294c0e2d4fb208fec89e4805f3d719381): Bind for 0.0.0.0:8545 failed: port is already allocated
75 |
76 | ERROR: for core0 Cannot create container for service core0: Conflict. The container name "/core0" is already in use by container "4bdaf40e2cd26bf549738ea95f53ba49cb5407c3d892b50b5a75e72e08e
77 | 3e0a8". You have to remove (or rename) that container to be able to reuse that name.
78 |
79 | ERROR: for ganache Cannot start service ganache: driver failed programming external connectivity on endpoint 626fbf28-7c90-4842-be8e-3346f864b369_ganache_1 (23bf2faf8fbce45f4a112b59183739f294c0e2d4fb208fec89e4805f3d719381): Bind for 0.0.0.0:8545 failed: port is already allocated
80 | Encountered errors while bringing up the project.
81 | Attaching to 626fbf28-7c90-4842-be8e-3346f864b369_ganache_1
82 | ```
83 |
84 | To fix it, run the `cleanup.sh` script under `scripts` directory :
85 |
86 | ```shell
87 | ./scripts/cleanup.sh
88 | ```
89 |
90 | NB: This will kill and remove hanging containers and networks related to the executed. But might also delete unrelated ones if they have the same name.
91 |
--------------------------------------------------------------------------------
/e2e/Service.go:
--------------------------------------------------------------------------------
1 | package e2e
2 |
3 | import "fmt"
4 |
5 | type Service int64
6 |
7 | const (
8 | Core0 Service = iota
9 | Core0Orch
10 | Core1
11 | Core1Orch
12 | Core2
13 | Core2Orch
14 | Core3
15 | Core3Orch
16 | Deployer
17 | Relayer
18 | Ganache
19 | )
20 |
21 | const (
22 | // represent the docker-compose network details
23 | // TODO maybe make a struct called Service containing this information
24 |
25 | CORE0 = "core0"
26 | CORE0ACCOUNTADDRESS = "celestia198gj5ges3xayhmrtp4wzrjc2wqu2qtz0kavyg2"
27 | CORE0EVMADDRESS = "0x966e6f22781EF6a6A82BBB4DB3df8E225DfD9488"
28 | COREOORCH = "core0-orch"
29 |
30 | CORE1 = "core1"
31 | CORE1ACCOUNTADDRESS = "celestia1nmu3r37v7lcx0lr68h9v4vr4m20tqkrwt97wej"
32 | CORE1EVMADDRESS = "0x91DEd26b5f38B065FC0204c7929Da1b2A21877Ad"
33 | CORE1ORCH = "core1-orch"
34 |
35 | CORE2 = "core2"
36 | CORE2ACCOUNTADDRESS = "celestia1p236k88sk7tdqgsw539jclmy44vn9m4lk8kqgd"
37 | CORE2EVMADDRESS = "0x3d22f0C38251ebdBE92e14BBF1bd2067F1C3b7D7"
38 | CORE2ORCH = "core2-orch"
39 |
40 | CORE3 = "core3"
41 | CORE3ACCOUNTADDRESS = "celestia1d47nxy65684ptn3l8j7dwf5tx3qe5xjcl2qrdj"
42 | CORE3EVMADDRESS = "0x3EE99606625E740D8b29C8570d855Eb387F3c790"
43 | CORE3ORCH = "core3-orch"
44 |
45 | DEPLOYER = "deployer"
46 | RELAYER = "relayer"
47 | GANACHE = "ganache"
48 | )
49 |
50 | var BOOTSTRAPPERS = []string{"/ip4/127.0.0.1/tcp/30000/p2p/12D3KooWBSMasWzRSRKXREhediFUwABNZwzJbkZcYz5rYr9Zdmfn"}
51 |
52 | func (s Service) toString() (string, error) {
53 | switch s {
54 | case Core0:
55 | return CORE0, nil
56 | case Core0Orch:
57 | return COREOORCH, nil
58 | case Core1:
59 | return CORE1, nil
60 | case Core1Orch:
61 | return CORE1ORCH, nil
62 | case Core2:
63 | return CORE2, nil
64 | case Core2Orch:
65 | return CORE2ORCH, nil
66 | case Core3:
67 | return CORE3, nil
68 | case Core3Orch:
69 | return CORE3ORCH, nil
70 | case Deployer:
71 | return DEPLOYER, nil
72 | case Relayer:
73 | return RELAYER, nil
74 | case Ganache:
75 | return GANACHE, nil
76 | }
77 | return "", fmt.Errorf("unknown service")
78 | }
79 |
--------------------------------------------------------------------------------
/e2e/celestia-app/core0/account-address-core0.txt:
--------------------------------------------------------------------------------
1 | celestia1p2gvpwa7svxd5t7d2zjc0u7gsh2537sjx3cq3a
2 |
--------------------------------------------------------------------------------
/e2e/celestia-app/core0/config/gentx/gentx-de74a671b839639ed638fd4140f797ddadb3c2f1.json:
--------------------------------------------------------------------------------
1 | {"body":{"messages":[{"@type":"/cosmos.staking.v1beta1.MsgCreateValidator","description":{"moniker":"blobstream-e2e","identity":"","website":"","security_contact":"","details":""},"commission":{"rate":"0.100000000000000000","max_rate":"0.200000000000000000","max_change_rate":"0.010000000000000000"},"min_self_delegation":"1","delegator_address":"celestia1p2gvpwa7svxd5t7d2zjc0u7gsh2537sjx3cq3a","validator_address":"celestiavaloper1p2gvpwa7svxd5t7d2zjc0u7gsh2537sjrw6e8m","pubkey":{"@type":"/cosmos.crypto.ed25519.PubKey","key":"bYso1zvhv8DD3mUM688cUff7U90670GM1fjnMzAbGfg="},"value":{"denom":"utia","amount":"5000000000"}}],"memo":"de74a671b839639ed638fd4140f797ddadb3c2f1@192.168.122.1:26656","timeout_height":"0","extension_options":[],"non_critical_extension_options":[]},"auth_info":{"signer_infos":[{"public_key":{"@type":"/cosmos.crypto.secp256k1.PubKey","key":"AsIx/It5+oWua4C/oHtVy6Ns3/z3omXy10uuKSVINbas"},"mode_info":{"single":{"mode":"SIGN_MODE_DIRECT"}},"sequence":"0"}],"fee":{"amount":[],"gas_limit":"210000","payer":"","granter":""},"tip":null},"signatures":["ixd1gyJnu7JHEHfrEkXoerUNjIDweOcyM87Hfp8ByHtG+wrXgjiNLxHV6MLg29CjiwgCj5EEeyc2o5Y8OJW5/w=="]}
2 |
--------------------------------------------------------------------------------
/e2e/celestia-app/core0/config/node_key.json:
--------------------------------------------------------------------------------
1 | {"priv_key":{"type":"tendermint/PrivKeyEd25519","value":"e7Js9od4TAqfyihJrc3+zFo9bQCEX0PD0ho5NJ2jjCfMh/nz645apZWJLLnlFa1iP376gJrJLnpDUN4tfXPb3w=="}}
--------------------------------------------------------------------------------
/e2e/celestia-app/core0/config/priv_validator_key.json:
--------------------------------------------------------------------------------
1 | {
2 | "address": "EB7463ED828FEB5FED88364EBB272044A0FD2990",
3 | "pub_key": {
4 | "type": "tendermint/PubKeyEd25519",
5 | "value": "bYso1zvhv8DD3mUM688cUff7U90670GM1fjnMzAbGfg="
6 | },
7 | "priv_key": {
8 | "type": "tendermint/PrivKeyEd25519",
9 | "value": "itECjJ4X7YWn18RxDrNpQoXz8OI3VyBxv8PJOEGvUe5tiyjXO+G/wMPeZQzrzxxR9/tT3TrvQYzV+OczMBsZ+A=="
10 | }
11 | }
--------------------------------------------------------------------------------
/e2e/celestia-app/core0/keyring-test/0a90c0bbbe830cda2fcd50a587f3c885d548fa12.address:
--------------------------------------------------------------------------------
1 | eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjcmVhdGVkIjoiMjAyMy0wOC0xOSAxMTozNjozOS43NDk5NjM1MjUgKzAyMDAgQ0VTVCBtPSswLjA2NzcyMjcwOSIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjgxOTIsInAycyI6Ik5RWWVyQjcyQXhfblVQcU8ifQ.4h9ZVKAhyE9ALQH2lAyQ6rkHaCQxsGU9rTKWo_AkcqGycpRHvxl9_A.loXzNYRdkoHiO_2N.2nEc1IZw4csAll7CBvAICc0HVmEyLR4zXFthbuETQcIxUPKNxBYfvFI6JHErIPx-fQ60EBk5oSH2PuXdKklHju5AbTPYmguAmVeDvsTznJcpqB_jhQCaQ7T4R5Jeln50qC48wMIMNpn35UTIId8SuFhGIZSCPpnIRbrFYupsyRZB6S5DPr0rbtOQ6bIENuvj0dMqcNfs1Gg9NROtkDkcbV_eN_zya0GK-ZU6Ob0tVXgGKtJgVZU.w4f79mmdL7m20fAHOT5Jkg
--------------------------------------------------------------------------------
/e2e/celestia-app/core0/keyring-test/core0.info:
--------------------------------------------------------------------------------
1 | eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjcmVhdGVkIjoiMjAyMy0wOC0xOSAxMTozNjozOS43NDU4ODQ2MTcgKzAyMDAgQ0VTVCBtPSswLjA2MzY0MzgwMSIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjgxOTIsInAycyI6InBmUGlyOEl1QTBBUjFCQ20ifQ.usbSHCcQLh_RnJ7Pt5-kqocbF6ExOtkKZL8RFdtMywnbruGcJXeOUQ.rQS87GlSaXrh1j1k.pwIFp8VRMDWrMVYHezMjv8PbSPe8zCYlSc-m3i3LVSWAPoymIT9kyRY-6y6ejI1mTDnzv173enX0hPH95lHYbG3tnOS9r4QQzpG57rDljbPZxuCn3g7OYwVMfEnD9UQCwMV0HHirEEupuAy-wdV9xtBCRmEZpJZYVDTO-REaaCCagkfkea3mQwlYDpoTVeTDZ-6MNhXPWz_vPzfXJROM_6aqI0iwRL20rjVUDgbNcKsBj9Q8h8Z89GnuHR51ee52UpdHaXhs-z6PvlVZ2JpeAbv2n9o-yv0v7jjMG3x4wp8PEEspBTVCDmIYdzhg0QTmn_Ea9lKMqnkInBIJTX5Uwc9_Nk-KNA5nSSRD4qPxbgcMGQPub6_ZvyDQTTRXORfFRFKSXIVjyjFF5sD5DSs9vMO96W3966Ku3Ai9oCtLaqr6ZCu-hVUjjYuJCas.vBT8VZtW_0kwVpQJnAoN2Q
--------------------------------------------------------------------------------
/e2e/celestia-app/core1/account-address-core1.txt:
--------------------------------------------------------------------------------
1 | celestia15mwtw3nuqd9equzdsgteuc5nrl5zq49qgjcgzx
2 |
--------------------------------------------------------------------------------
/e2e/celestia-app/core1/config/node_key.json:
--------------------------------------------------------------------------------
1 | {"priv_key":{"type":"tendermint/PrivKeyEd25519","value":"6Bzmn1qiS+qdznTQTSBmtIocwwXSy3b+3+Oio6zY7IoiUY3/QE3KnyFMAadYtzdAY6Yn4dqo/rU4T0PrLOFuVA=="}}
--------------------------------------------------------------------------------
/e2e/celestia-app/core1/config/priv_validator_key.json:
--------------------------------------------------------------------------------
1 | {
2 | "address": "E0B63563D528BDA8AA3D26A703FC8EAAD1556016",
3 | "pub_key": {
4 | "type": "tendermint/PubKeyEd25519",
5 | "value": "txZzDu84JDYFruXqzpd2HuY15Dwr4Viy31N8rUzdINE="
6 | },
7 | "priv_key": {
8 | "type": "tendermint/PrivKeyEd25519",
9 | "value": "bhWQYz9CKoJqHXkTymhuOxfH3qDqF0SsEk2CrfLw/Pu3FnMO7zgkNgWu5erOl3Ye5jXkPCvhWLLfU3ytTN0g0Q=="
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/e2e/celestia-app/core1/keyring-test/a6dcb7467c034b90704d82179e62931fe82054a0.address:
--------------------------------------------------------------------------------
1 | eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjcmVhdGVkIjoiMjAyMy0wOC0xOSAxMTozNjozOS44MTYyOTIyNSArMDIwMCBDRVNUIG09KzAuMDU2Nzc5MTExIiwiZW5jIjoiQTI1NkdDTSIsInAyYyI6ODE5MiwicDJzIjoiWC1KVElaVDlLVHJCT0w1QSJ9.kH8tmgR-nXfX_6N7O-igdFfdgNzEk46XLmZd9LP6k6LzeEsjJGcq1g.kpBkHp1NrI7IPHp0.oVqmSDWesURquSyGL44UZYAQye3BiUjEYRYHVdCzQ7Y9pOogRMBSDEnwz2hWyc5FnhoNirxv4fZe5k-gvjqE7_jaL1t9YO3MX2KkP7pVxuDgpzomcrGZK7kI3Qkt6J2WW09xTeWLiUNWPb9UoqpvpiUdioaiBZQ7KVOxc9AYBBSuq7ZYYQCY_D4RJ-YMKP5Bgum2Iy0PiD3qwhAvPuuxVx-l4aRW0zYemi1HjJxJFqYmkFK9eyo.vlmDxnDMyGbDSKqWM88Z6A
--------------------------------------------------------------------------------
/e2e/celestia-app/core1/keyring-test/core1.info:
--------------------------------------------------------------------------------
1 | eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjcmVhdGVkIjoiMjAyMy0wOC0xOSAxMTozNjozOS44MTE0ODE1MzUgKzAyMDAgQ0VTVCBtPSswLjA1MTk2ODM5NiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjgxOTIsInAycyI6IjdpaUw5OXdRYUNhQ0t3NjMifQ.KYUXuO-SWmi11DvC6jN85v4KfW6PIoWAFCPIUjAKsb3-k8Irf9_ESA.eJ4xGDnHjQbc9AiV.QtsQ6fJurhBQI2V70--aNK5SDb2UA9a0HoQD28PU0k0OzBMONtMaAL2KRppKcvxUCJ2UxsLoZDXaJQEy-Z7LGu5U9wx-QX7PAh0P_KXs3ATv5KeE1EHwexSuJ8S-mGs26aKdeS2FGAykCJs-8HKotdO-5t3yCZ-JYLVTsTNIdn3ET5eCr1jphk_iFidS7RZNcGfdNmRbpVZCHtuL6rp9ZlfZ0G_We2KtAfD74ukVQvG-1NGTC_-uM_3FhAZkf1tkuD9pdE7DUyRhgSOEzTfU6d-i3xpy2HwMZy2jHnTSaip7OCHAtXrZc2S5jS512R-EWOewy-6yvVxayxLlgxoCbW1fGKfflQ5bInhM4iAJiX8lkvOyHqvaQDgJ3Bpyu4Qnjlxe0exzbyq8ep-YbPvPG-3c9FjJDbGOhlRodH3N80gGUY3UWmQPQx9WtPY.EgW0XuKmlUPHfkwTsXetaQ
--------------------------------------------------------------------------------
/e2e/celestia-app/core2/account-address-core2.txt:
--------------------------------------------------------------------------------
1 | celestia1ded973wv2vv6t54522w4ghfh0av40xkfc9zx5d
2 |
--------------------------------------------------------------------------------
/e2e/celestia-app/core2/config/node_key.json:
--------------------------------------------------------------------------------
1 | {"priv_key":{"type":"tendermint/PrivKeyEd25519","value":"Fzw87L+h3k5SqhLmJ9j2ZiJol/yv+nVwtdMSPJV+GZNo5VlY+IcdCt2D/XbahQv+GmpdXqFNYH1i6bakJd5RHA=="}}
--------------------------------------------------------------------------------
/e2e/celestia-app/core2/config/priv_validator_key.json:
--------------------------------------------------------------------------------
1 | {
2 | "address": "BC381AF58CB970ACAF795A003461EE02057C5F92",
3 | "pub_key": {
4 | "type": "tendermint/PubKeyEd25519",
5 | "value": "FofILZcGufMCLOtcSc/Vsq5P2qiWv+hQOnmlq+OGYTQ="
6 | },
7 | "priv_key": {
8 | "type": "tendermint/PrivKeyEd25519",
9 | "value": "g2mO9OAForIzmS79sXO/9/5k9LEtv/AjuQnzVNDy0NAWh8gtlwa58wIs61xJz9Wyrk/aqJa/6FA6eaWr44ZhNA=="
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/e2e/celestia-app/core2/keyring-test/6e5a5f45cc5319a5d2b4529d545d377f59579ac9.address:
--------------------------------------------------------------------------------
1 | eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjcmVhdGVkIjoiMjAyMy0wOC0xOSAxMTozNjozOS44NzY3MDM1NTkgKzAyMDAgQ0VTVCBtPSswLjA1MDIzMzA1MSIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjgxOTIsInAycyI6ImlsMmZLcHFLMGF3Z3gyeDIifQ.6XHIgl-T9FZpKMb5R75Wytfr5j_zWx18zPpSRn5JEwmkcSox7q_B_g.YnYv0xern7dy7kCw.VRtKOeN6kFNdi6LIHvMMKXSnolK8Y96M0WS0zY9ZmOeB0slxDxpB9vkracJZ5Rd6YJLBP_SsqbMGJX90C8Ny5yOf8RUBUct6DXUqzDL7HAzAqgjYIBMombd3lyw9a2A6_MZDy1Gdue_8NUB4HyJ53nADa0bJWCTPiSaEyZdQ9mTrnr8Rf432S41K6TRYQ4g6LVu6eiHNQQ50PCCmF4aevUuyzxfbant3QB5StVG7S371jV9rmco.U6-kf0zNiCMp3idVoCd25Q
--------------------------------------------------------------------------------
/e2e/celestia-app/core2/keyring-test/core2.info:
--------------------------------------------------------------------------------
1 | eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjcmVhdGVkIjoiMjAyMy0wOC0xOSAxMTozNjozOS44NzI4OTY2OTUgKzAyMDAgQ0VTVCBtPSswLjA0NjQyNjE4NyIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjgxOTIsInAycyI6IlBLVFRucnR3NkJWbXY1M2sifQ.wmKkCPzK8j9KV_eh255s-nKG9sqzhDB0-Mdfbznfe-VwSWS_tbJYAQ.E3oVOAJpT2hraC22.P3Z__jbmC1DoUOiVjTf5hZM-VQ8sA85puYva6n0CgnJK6KMDcFUXn_4Qib2A5y55XgrM0pp4xhf-610zoPL8dDjrw6wW4jXQnp_FUZIdO9oJW4z5dHMbxnScXqxOX69zNw-E_LSQ6Y89g72ASLPjYpxq0bZT6avU8vyMHyo_y40PHAPugke5CtCzhFO6oAaZHL9CQgw4RCRuNlNZK7Nhh-hkH9tO78qTOsNxViIXNl5dJNL2EGh7XAMcsIdy7cg3ZhfspbX5GUkvbsJgI38TKX9hbvm4ZnlvFTVFbyTK9sCWCgO0eSuf_I3vKmy8c-f1RsMPAzTrngImZBUho_tJXbY9nmCSDHrRmsdHUEp63gYdqBxdPOQGAQrIrh9XfPEztLYuwMgMkPI0gK6DE2d3bqTLQjZpgouzHUG_bsWMxBcDOhmL4YczMlNMQC8.R2V_WGz2wqZUTMvEFA_PXA
--------------------------------------------------------------------------------
/e2e/celestia-app/core3/account-address-core3.txt:
--------------------------------------------------------------------------------
1 | celestia15nfz020trvnurxccqwxxtx4l4wgm6jjgtkd0mn
2 |
--------------------------------------------------------------------------------
/e2e/celestia-app/core3/config/node_key.json:
--------------------------------------------------------------------------------
1 | {"priv_key":{"type":"tendermint/PrivKeyEd25519","value":"H8Qluvmcjt7kflsxqAZ711SrttWqKUdMzDMwmn+IE2BU/TQf05A5BszktAvUSTCKuyz6z1+SkLPnc+HK5FMvtQ=="}}
--------------------------------------------------------------------------------
/e2e/celestia-app/core3/config/priv_validator_key.json:
--------------------------------------------------------------------------------
1 | {
2 | "address": "223467FF1BD29B323D8CF4AFCFECD5ED1C0D4383",
3 | "pub_key": {
4 | "type": "tendermint/PubKeyEd25519",
5 | "value": "+BDjqMwVmqoQ5skfKtaclG7qoANYCH4Qs8FER3j3Obg="
6 | },
7 | "priv_key": {
8 | "type": "tendermint/PrivKeyEd25519",
9 | "value": "EVJrLtp4tcXDtS4bnewrzk3BKqavipm0H7vpfuLTAlL4EOOozBWaqhDmyR8q1pyUbuqgA1gIfhCzwURHePc5uA=="
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/e2e/celestia-app/core3/keyring-test/a4d227a9eb1b27c19b18038c659abfab91bd4a48.address:
--------------------------------------------------------------------------------
1 | eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjcmVhdGVkIjoiMjAyMy0wOC0xOSAxMTozNjozOS45MzgyMDg0NTYgKzAyMDAgQ0VTVCBtPSswLjA1MDY3Mzg2OCIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjgxOTIsInAycyI6ImJkM1JZTjR3clJqcFFuVTEifQ._MvUAy3eBhcioKCck0NVtNgpM-07wcv2lhPqEcVE5LY-_6RrH_LLNA.DwkIAzj6R93Dugy_.IktC8RZyte-qyDzXw8hZTxJuFWWtYv575Fc-zywkMDE4w686BQWqBsrMEyKXmfJDNCKTolQrYVrafHcITJSyfdjnS8Y3PFdqSW7ZBLQvsa0FYuXe56ciG7_BMyvG0FX9W0zKdMEaGA606RVOsTFMX0YhB9wXe9LWX2OAUqjlxeBAiV0LaEBJ2JZVLuByYd3Nmy2stWuSXDXlrgr9FpWMGwKVjxVyQ1mBcUo2Mzwd-r5NaqoaYBs.LoVdUSX3QpErt_vo364Z2w
--------------------------------------------------------------------------------
/e2e/celestia-app/core3/keyring-test/core3.info:
--------------------------------------------------------------------------------
1 | eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjcmVhdGVkIjoiMjAyMy0wOC0xOSAxMTozNjozOS45MzQxNjQ5NzMgKzAyMDAgQ0VTVCBtPSswLjA0NjYzMDM3NSIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjgxOTIsInAycyI6Ikt3aC1RM0ZNZ3QzWHRSeFcifQ.ocxZ4HRbxwl5npB9_mW7nv_akIRjIV4rCN-NdaZO11vH0ZOH13X7tg.KrKGI65C4l4W2GBK.qir1ISeblv1SHcG9igzH-il4C523WZTDO1L8uq_2AA5-MPNP7XUsO4HLdUQVYmAK0APFVt4IbDPWdrC4OvF1j5c1tSNUSUgIEOk9_8CKK-3BoVDje5_VGOEc4qiKOZPgwH7Bd0NNGRYy-2xbwwv6c--CpzcWPD52Khz5XMPG6QYEtjn0ULpvkMZ0jdpjDs_cWQZa5cKBO9RfUMs_ZAmi2UTTE5Ysro2FQAMoNC8jUxatmsqlWSpfDBzXm1jSdsmviXHmxKfPtHFLzPmu_lOYipaAeyYAgAPaWYiGUKvdx2CwBR4_Dn6PCTMYsHSY2PQsQRSpjExm6GpXJlSwk9uNYa3_kYj5s33Bl8h9wgYbWmhf45wV6myKOyJKTbzbWrbOaoT5OUl2MRWWR5jwNQR2FpNiLo92CFa4T9cdSdM6eLtXjfO6DL6Z_6PNLUU.qoC1PgfPJ-d62-6IOu7H2Q
--------------------------------------------------------------------------------
/e2e/deployer_test.go:
--------------------------------------------------------------------------------
1 | package e2e
2 |
3 | import (
4 | "context"
5 | "os"
6 | "testing"
7 | "time"
8 |
9 | "github.com/celestiaorg/orchestrator-relayer/evm"
10 |
11 | "github.com/ethereum/go-ethereum/accounts/abi/bind"
12 | "github.com/stretchr/testify/assert"
13 | )
14 |
15 | func TestDeployer(t *testing.T) {
16 | if os.Getenv("BLOBSTREAM_INTEGRATION_TEST") != TRUE {
17 | t.Skip("Skipping Blobstream integration tests")
18 | }
19 |
20 | network, err := NewBlobstreamNetwork()
21 | HandleNetworkError(t, network, err, false)
22 |
23 | // to release resources after tests
24 | defer network.DeleteAll() //nolint:errcheck
25 |
26 | err = network.StartMultiple(Core0, Ganache)
27 | HandleNetworkError(t, network, err, false)
28 |
29 | ctx := context.Background()
30 |
31 | err = network.WaitForBlock(ctx, 2)
32 | HandleNetworkError(t, network, err, false)
33 |
34 | _, err = network.GetLatestDeployedBlobstreamContractWithCustomTimeout(ctx, 15*time.Second)
35 | HandleNetworkError(t, network, err, true)
36 |
37 | err = network.DeployBlobstreamContract()
38 | HandleNetworkError(t, network, err, false)
39 |
40 | bridge, err := network.GetLatestDeployedBlobstreamContract(ctx)
41 | HandleNetworkError(t, network, err, false)
42 |
43 | evmClient := evm.NewClient(nil, bridge, nil, nil, network.EVMRPC, evm.DefaultEVMGasLimit)
44 |
45 | eventNonce, err := evmClient.StateLastEventNonce(&bind.CallOpts{Context: ctx})
46 | assert.NoError(t, err)
47 | assert.Equal(t, uint64(1), eventNonce)
48 | }
49 |
--------------------------------------------------------------------------------
/e2e/errors.go:
--------------------------------------------------------------------------------
1 | package e2e
2 |
3 | import "errors"
4 |
5 | var ErrNetworkStopped = errors.New("network is stopping")
6 |
--------------------------------------------------------------------------------
/e2e/scripts/cleanup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # this script cleans up the docker environment after an unexpected test stop
4 |
5 | echo "releasing resources..."
6 |
7 | # kill known containers
8 | docker container kill /core0 2> /dev/null
9 | docker container kill /core0-orch 2> /dev/null
10 | docker container kill /core1 2> /dev/null
11 | docker container kill /core1-orch 2> /dev/null
12 | docker container kill /core2 2> /dev/null
13 | docker container kill /core2-orch 2> /dev/null
14 | docker container kill /core3 2> /dev/null
15 | docker container kill /core3-orch 2> /dev/null
16 | docker container kill /relayer 2> /dev/null
17 | docker container kill /deployer 2> /dev/null
18 |
19 | # remove known containers
20 | docker container rm /core0 2> /dev/null
21 | docker container rm /core0-orch 2> /dev/null
22 | docker container rm /core1 2> /dev/null
23 | docker container rm /core1-orch 2> /dev/null
24 | docker container rm /core2 2> /dev/null
25 | docker container rm /core2-orch 2> /dev/null
26 | docker container rm /core3 2> /dev/null
27 | docker container rm /core3-orch 2> /dev/null
28 | docker container rm /relayer 2> /dev/null
29 | docker container rm /deployer 2> /dev/null
30 |
31 | # handle ganache
32 | ganache_ids=$(docker container ps -a | grep ganache | cut -f 1 -d\ )
33 | for id in ${ganache_ids} ; do
34 | echo ${id}
35 | docker container kill ${id} 2> /dev/null
36 | docker container rm ${id} 2> /dev/null
37 | done
38 |
39 | # remove potential networks that might have been created by docker-compose
40 | potential_networks=$(docker network ls | grep default | cut -f 1 -d\ )
41 | for net in ${potential_networks} ; do
42 | echo ${net}
43 | docker network rm ${net} 2> /dev/null
44 | done
45 |
46 | echo "done."
47 | exit 0
48 |
--------------------------------------------------------------------------------
/e2e/scripts/deploy_blobstream_contract.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # This script deploys the Blobstream contract and outputs the address to stdout.
4 |
5 | # check whether to deploy a new contract or no need
6 | if [[ "${DEPLOY_NEW_CONTRACT}" != "true" ]]
7 | then
8 | echo "no need to deploy a new Blobstream contract. exiting..."
9 | exit 0
10 | fi
11 |
12 | # check if environment variables are set
13 | if [[ -z "${EVM_CHAIN_ID}" || -z "${PRIVATE_KEY}" ]] || \
14 | [[ -z "${CORE_RPC_HOST}" || -z "${CORE_RPC_PORT}" ]] || \
15 | [[ -z "${CORE_GRPC_HOST}" || -z "${CORE_GRPC_PORT}" ]] || \
16 | [[ -z "${EVM_ENDPOINT}" || -z "${STARTING_NONCE}" ]]
17 | then
18 | echo "Environment not setup correctly. Please set:"
19 | echo "EVM_CHAIN_ID, PRIVATE_KEY, CORE_RPC_HOST, CORE_RPC_PORT, CORE_GRPC_HOST, CORE_GRPC_PORT, EVM_ENDPOINT, STARTING_NONCE variables"
20 | exit 1
21 | fi
22 |
23 | # wait for the node to get up and running
24 | while true
25 | do
26 | # verify that the node is listening on gRPC
27 | nc -z -w5 "$CORE_GRPC_HOST" "$CORE_GRPC_PORT"
28 | result=$?
29 | if [ "${result}" != "0" ]; then
30 | echo "Waiting for node gRPC to be available ..."
31 | sleep 1s
32 | continue
33 | fi
34 |
35 | height=$(/bin/celestia-appd query block 1 -n tcp://${CORE_RPC_HOST}:${CORE_RPC_PORT} 2>/dev/null)
36 | if [[ -n ${height} ]] ; then
37 | break
38 | fi
39 | echo "Waiting for block 1 to be generated..."
40 | sleep 1s
41 | done
42 |
43 | # wait for the evm node to start
44 | while true
45 | do
46 | status_code=$(curl --write-out '%{http_code}' --silent --output /dev/null \
47 | --location --request POST ${EVM_ENDPOINT} \
48 | --header 'Content-Type: application/json' \
49 | --data-raw "{
50 | \"jsonrpc\":\"2.0\",
51 | \"method\":\"eth_blockNumber\",
52 | \"params\":[],
53 | \"id\":${EVM_CHAIN_ID}}")
54 | if [[ "${status_code}" -eq 200 ]] ; then
55 | break
56 | fi
57 | echo "Waiting for ethereum node to be up..."
58 | sleep 1s
59 | done
60 |
61 | # import keys to deployer
62 | /bin/blobstream deploy keys evm import ecdsa "${PRIVATE_KEY}" --evm.passphrase=123
63 |
64 | echo "deploying Blobstream contract..."
65 |
66 | /bin/blobstream deploy \
67 | --evm.chain-id "${EVM_CHAIN_ID}" \
68 | --evm.account "${EVM_ACCOUNT}" \
69 | --core.rpc="${CORE_RPC_HOST}:${CORE_RPC_PORT}" \
70 | --core.grpc="${CORE_GRPC_HOST}:${CORE_GRPC_PORT}" \
71 | --grpc.insecure \
72 | --starting-nonce "${STARTING_NONCE}" \
73 | --evm.rpc "${EVM_ENDPOINT}" \
74 | --evm.passphrase=123 2> /opt/output
75 |
76 | echo $(cat /opt/output)
77 |
78 | cat /opt/output | grep "deployed" | awk '{ print $6 }' | grep -o '0x.*' > /opt/blobstream_address.txt
79 |
--------------------------------------------------------------------------------
/e2e/scripts/start_core0.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # This script starts core0
4 |
5 | # set the genesis time to current time for pruning to work properly
6 | new_time=$(date -u +"%Y-%m-%dT%H:%M:%S.%N")"Z"
7 | jq --arg new_time "$new_time" '.genesis_time = $new_time' /opt/config/genesis_template.json > /opt/config/genesis.json
8 |
9 | if [[ ! -f /opt/data/priv_validator_state.json ]]
10 | then
11 | mkdir /opt/data
12 | cat < /opt/data/priv_validator_state.json
13 | {
14 | "height": "0",
15 | "round": 0,
16 | "step": 0
17 | }
18 | EOF
19 | fi
20 |
21 | {
22 | # wait for the node to get up and running
23 | while true
24 | do
25 | status_code=$(curl --write-out '%{http_code}' --silent --output /dev/null localhost:26657/status)
26 | if [[ "${status_code}" -eq 200 ]] ; then
27 | break
28 | fi
29 | echo "Waiting for node to be up..."
30 | sleep 2s
31 | done
32 |
33 | VAL_ADDRESS=$(celestia-appd keys show core0 --keyring-backend test --bech=val --home /opt -a)
34 |
35 | # Register the validator EVM address
36 | celestia-appd tx qgb register \
37 | "${VAL_ADDRESS}" \
38 | 0x966e6f22781EF6a6A82BBB4DB3df8E225DfD9488 \
39 | --from core0 \
40 | --home /opt \
41 | --fees "30000utia" \
42 | -b block \
43 | --chain-id="blobstream-e2e" \
44 | --yes
45 | } &
46 |
47 | /bin/celestia-appd start \
48 | --moniker core0 \
49 | --rpc.laddr tcp://0.0.0.0:26657 \
50 | --home /opt
51 |
--------------------------------------------------------------------------------
/e2e/scripts/start_node_and_create_validator.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # This script starts a Celestia-app, creates a validator with the provided parameters, then
4 | # keeps running it validating blocks.
5 |
6 | # check if environment variables are set
7 | if [[ -z "${CELESTIA_HOME}" || -z "${MONIKER}" || -z "${EVM_ACCOUNT}" || -z "${AMOUNT}" ]]
8 | then
9 | echo "Environment not setup correctly. Please set: CELESTIA_HOME, MONIKER, EVM_ACCOUNT, AMOUNT variables"
10 | exit 1
11 | fi
12 |
13 | # create necessary structure if doesn't exist
14 | if [[ ! -f ${CELESTIA_HOME}/data/priv_validator_state.json ]]
15 | then
16 | mkdir "${CELESTIA_HOME}"/data
17 | cat < ${CELESTIA_HOME}/data/priv_validator_state.json
18 | {
19 | "height": "0",
20 | "round": 0,
21 | "step": 0
22 | }
23 | EOF
24 | fi
25 |
26 | {
27 | # wait for the node to get up and running
28 | while true
29 | do
30 | status_code=$(curl --write-out '%{http_code}' --silent --output /dev/null localhost:26657/status)
31 | if [[ "${status_code}" -eq 200 ]] ; then
32 | break
33 | fi
34 | echo "Waiting for node to be up..."
35 | sleep 2s
36 | done
37 |
38 | VAL_ADDRESS=$(celestia-appd keys show "${MONIKER}" --keyring-backend test --bech=val --home /opt -a)
39 | # keep retrying to create a validator
40 | while true
41 | do
42 | # create validator
43 | celestia-appd tx staking create-validator \
44 | --amount="${AMOUNT}" \
45 | --pubkey="$(celestia-appd tendermint show-validator --home "${CELESTIA_HOME}")" \
46 | --moniker="${MONIKER}" \
47 | --chain-id="blobstream-e2e" \
48 | --commission-rate=0.1 \
49 | --commission-max-rate=0.2 \
50 | --commission-max-change-rate=0.01 \
51 | --min-self-delegation=1000000 \
52 | --from="${MONIKER}" \
53 | --keyring-backend=test \
54 | --home="${CELESTIA_HOME}" \
55 | --broadcast-mode=block \
56 | --fees="300000utia" \
57 | --yes
58 | output=$(celestia-appd query staking validator "${VAL_ADDRESS}" 2>/dev/null)
59 | if [[ -n "${output}" ]] ; then
60 | break
61 | fi
62 | echo "trying to create validator..."
63 | sleep 1s
64 | done
65 |
66 | # Register the validator EVM address
67 | celestia-appd tx qgb register \
68 | "${VAL_ADDRESS}" \
69 | "${EVM_ACCOUNT}" \
70 | --from "${MONIKER}" \
71 | --home "${CELESTIA_HOME}" \
72 | --fees "30000utia" -b block \
73 | --chain-id="blobstream-e2e" \
74 | --yes
75 | } &
76 |
77 | # start node
78 | celestia-appd start \
79 | --home="${CELESTIA_HOME}" \
80 | --moniker="${MONIKER}" \
81 | --p2p.persistent_peers=de74a671b839639ed638fd4140f797ddadb3c2f1@core0:26656 \
82 | --rpc.laddr=tcp://0.0.0.0:26657
83 |
--------------------------------------------------------------------------------
/e2e/scripts/start_orchestrator_after_validator_created.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # This script waits for the validator to be created before starting the orchestrator
4 |
5 | # check if environment variables are set
6 | if [[ -z "${MONIKER}" || -z "${PRIVATE_KEY}" ]] || \
7 | [[ -z "${CORE_GRPC_HOST}" || -z "${CORE_GRPC_PORT}" ]] || \
8 | [[ -z "${CORE_RPC_HOST}" || -z "${CORE_RPC_PORT}" ]] || \
9 | [[ -z "${P2P_LISTEN}" || -z "${METRICS_ENDPOINT}" ]]
10 | then
11 | echo "Environment not setup correctly. Please set:"
12 | echo "MONIKER, PRIVATE_KEY, CORE_GRPC_HOST, CORE_GRPC_PORT, CORE_RPC_HOST, CORE_RPC_PORT, P2P_LISTEN, METRICS_ENDPOINT variables"
13 | exit 1
14 | fi
15 |
16 | # wait for the validator to be created before starting the orchestrator
17 | VAL_ADDRESS=$(celestia-appd keys show ${MONIKER} --keyring-backend test --bech=val --home /opt -a)
18 | while true
19 | do
20 | # verify that the node is listening on gRPC
21 | nc -z -w5 $CORE_GRPC_HOST $CORE_GRPC_PORT
22 | result=$?
23 | if [ "${result}" != "0" ]; then
24 | echo "Waiting for node gRPC to be available ..."
25 | sleep 1s
26 | continue
27 | fi
28 |
29 | # verify if RPC is running and the validator was created
30 | output=$(celestia-appd query staking validator ${VAL_ADDRESS} --node tcp://$CORE_RPC_HOST:$CORE_RPC_PORT 2>/dev/null)
31 | if [[ -n "${output}" ]] ; then
32 | break
33 | fi
34 | echo "Waiting for validator to be created..."
35 | sleep 3s
36 | done
37 |
38 | # initialize orchestrator
39 | /bin/blobstream orch init
40 |
41 | # add keys to keystore
42 | /bin/blobstream orch keys evm import ecdsa "${PRIVATE_KEY}" --evm.passphrase 123
43 |
44 | # start orchestrator
45 | if [[ -z "${P2P_BOOTSTRAPPERS}" ]]
46 | then
47 | # import the p2p key to use
48 | /bin/blobstream orchestrator keys p2p import key "${P2P_IDENTITY}"
49 |
50 | /bin/blobstream orchestrator start \
51 | --evm.account="${EVM_ACCOUNT}" \
52 | --core.rpc="${CORE_RPC_HOST}:${CORE_RPC_PORT}" \
53 | --core.grpc="${CORE_GRPC_HOST}:${CORE_GRPC_PORT}" \
54 | --grpc.insecure \
55 | --p2p.nickname=key \
56 | --p2p.listen-addr="${P2P_LISTEN}" \
57 | --evm.passphrase=123 \
58 | --log.level=debug \
59 | --metrics \
60 | --metrics.endpoint="${METRICS_ENDPOINT}"
61 | else
62 | # to give time for the bootstrappers to be up
63 | sleep 5s
64 |
65 | /bin/blobstream orchestrator start \
66 | --evm.account="${EVM_ACCOUNT}" \
67 | --core.rpc="${CORE_RPC_HOST}:${CORE_RPC_PORT}" \
68 | --core.grpc="${CORE_GRPC_HOST}:${CORE_GRPC_PORT}" \
69 | --grpc.insecure \
70 | --p2p.listen-addr="${P2P_LISTEN}" \
71 | --p2p.bootstrappers="${P2P_BOOTSTRAPPERS}" \
72 | --evm.passphrase=123 \
73 | --log.level=debug \
74 | --metrics \
75 | --metrics.endpoint="${METRICS_ENDPOINT}"
76 | fi
77 |
--------------------------------------------------------------------------------
/e2e/scripts/start_relayer.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # This script runs the Blobstream relayer with the ability to deploy a new Blobstream contract or
4 | # pass one as an environment variable BLOBSTREAM_CONTRACT
5 |
6 | # check if environment variables are set
7 | if [[ -z "${EVM_CHAIN_ID}" || -z "${PRIVATE_KEY}" ]] || \
8 | [[ -z "${CORE_GRPC_HOST}" || -z "${CORE_GRPC_PORT}" ]] || \
9 | [[ -z "${CORE_RPC_HOST}" || -z "${CORE_RPC_PORT}" ]] || \
10 | [[ -z "${EVM_ENDPOINT}" || -z "${P2P_BOOTSTRAPPERS}" ]] || \
11 | [[ -z "${P2P_LISTEN}" || -z "${METRICS_ENDPOINT}" ]]
12 | then
13 | echo "Environment not setup correctly. Please set:"
14 | echo "EVM_CHAIN_ID, PRIVATE_KEY, CORE_GRPC_HOST, CORE_GRPC_PORT, CORE_RPC_HOST, CORE_RPC_PORT, EVM_ENDPOINT, P2P_BOOTSTRAPPERS, P2P_LISTEN, METRICS_ENDPOINT variables"
15 | exit 1
16 | fi
17 |
18 | echo "starting relayer..."
19 |
20 | # wait for the node to get up and running
21 | while true
22 | do
23 | height=$(/bin/celestia-appd query block 1 -n tcp://$CORE_RPC_HOST:$CORE_RPC_PORT 2>/dev/null)
24 | if [[ -n ${height} ]] ; then
25 | break
26 | fi
27 | echo "Waiting for block 1 to be generated..."
28 | sleep 5s
29 | done
30 |
31 | # this will introduce flakiness but it's gonna be complicated to wait for validators to be created
32 | # and also waiting for them to change their addresses in bash. Also, depending on the testing scenarios,
33 | # the network topology varies. So, the best we can do now is sleep.
34 | sleep 120s
35 |
36 | # check whether to deploy a new contract or use an existing one
37 | if [[ -z "${BLOBSTREAM_CONTRACT}" ]]
38 | then
39 | export DEPLOY_NEW_CONTRACT=true
40 | export STARTING_NONCE=latest
41 | # expects the script to be mounted to this directory
42 | /bin/bash /opt/deploy_blobstream_contract.sh
43 | fi
44 |
45 | # get the address from the `blobstream_address.txt` file
46 | BLOBSTREAM_CONTRACT=$(cat /opt/blobstream_address.txt)
47 |
48 | # init the relayer
49 | /bin/blobstream relayer init
50 |
51 | # import keys to relayer
52 | /bin/blobstream relayer keys evm import ecdsa "${PRIVATE_KEY}" --evm.passphrase 123
53 |
54 | # to give time for the bootstrappers to be up
55 | sleep 5s
56 | /bin/blobstream relayer start \
57 | --evm.account="${EVM_ACCOUNT}" \
58 | --core.rpc="${CORE_RPC_HOST}:${CORE_RPC_PORT}" \
59 | --core.grpc="${CORE_GRPC_HOST}:${CORE_GRPC_PORT}" \
60 | --grpc.insecure \
61 | --evm.chain-id="${EVM_CHAIN_ID}" \
62 | --evm.rpc="${EVM_ENDPOINT}" \
63 | --evm.contract-address="${BLOBSTREAM_CONTRACT}" \
64 | --p2p.bootstrappers="${P2P_BOOTSTRAPPERS}" \
65 | --p2p.listen-addr="${P2P_LISTEN}" \
66 | --evm.passphrase=123 \
67 | --metrics \
68 | --metrics.endpoint="${METRICS_ENDPOINT}" \
69 | --log.level=debug
70 |
--------------------------------------------------------------------------------
/e2e/telemetry/grafana/datasources/config.yml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: 1
3 |
4 | datasources:
5 | - name: Prometheus
6 | type: prometheus
7 | access: proxy
8 | url: http://prometheus:9090
9 |
10 |
--------------------------------------------------------------------------------
/e2e/telemetry/otel-collector/config.yml:
--------------------------------------------------------------------------------
1 | ---
2 | extensions:
3 | health_check:
4 |
5 | receivers:
6 | otlp:
7 | protocols:
8 | grpc:
9 | # endpoint: "0.0.0.0:4317"
10 | http:
11 | # endpoint: "0.0.0.0:4318"
12 |
13 | exporters:
14 | prometheus:
15 | endpoint: "otel-collector:8889"
16 | send_timestamps: true
17 | metric_expiration: 1800m
18 |
19 | service:
20 | extensions: [health_check]
21 | pipelines:
22 | metrics:
23 | receivers: [otlp]
24 | exporters: [prometheus]
25 |
--------------------------------------------------------------------------------
/e2e/telemetry/prometheus/prometheus.yml:
--------------------------------------------------------------------------------
1 | ---
2 | global:
3 | scrape_interval: 15s
4 | scrape_timeout: 10s
5 | evaluation_interval: 15s
6 |
7 | scrape_configs:
8 | - job_name: 'collector'
9 | metrics_path: /metrics
10 | honor_timestamps: true
11 | scrape_interval: 15s
12 | scrape_timeout: 10s
13 | scheme: http
14 | static_configs:
15 | - targets:
16 | - 'otel-collector:8889'
17 | - job_name: 'libp2p_metrics'
18 | static_configs:
19 | - targets: [ 'core0-orch:30001' ]
20 |
--------------------------------------------------------------------------------
/e2e/test_commons.go:
--------------------------------------------------------------------------------
1 | package e2e
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "testing"
7 | "time"
8 |
9 | "github.com/celestiaorg/orchestrator-relayer/p2p"
10 | "github.com/libp2p/go-libp2p/core/host"
11 | "github.com/libp2p/go-libp2p/core/peer"
12 |
13 | "github.com/stretchr/testify/assert"
14 | )
15 |
16 | const TRUE = "true"
17 |
18 | func HandleNetworkError(t *testing.T, network *BlobstreamNetwork, err error, expectError bool) {
19 | if expectError && err == nil {
20 | network.PrintLogs()
21 | assert.Error(t, err)
22 | t.FailNow()
23 | } else if !expectError && err != nil {
24 | network.PrintLogs()
25 | assert.NoError(t, err)
26 | if errors.Is(err, ErrNetworkStopped) {
27 | // if some other error occurred, we notify.
28 | network.toStopChan <- struct{}{}
29 | }
30 | t.FailNow()
31 | }
32 | }
33 |
34 | func ConnectToDHT(ctx context.Context, h host.Host, dht *p2p.BlobstreamDHT, target peer.AddrInfo) error {
35 | timeout := time.NewTimer(time.Minute)
36 | for {
37 | select {
38 | case <-timeout.C:
39 | return errors.New("couldn't connect to dht")
40 | default:
41 | if len(dht.RoutingTable().ListPeers()) == 0 {
42 | if h.Connect(ctx, target) == nil {
43 | return nil
44 | }
45 | time.Sleep(time.Second)
46 | } else {
47 | return nil
48 | }
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/evm/errors.go:
--------------------------------------------------------------------------------
1 | package evm
2 |
3 | import (
4 | "errors"
5 | )
6 |
7 | var ErrInvalid = errors.New("invalid")
8 |
--------------------------------------------------------------------------------
/evm/ethereum_signature.go:
--------------------------------------------------------------------------------
1 | package evm
2 |
3 | import (
4 | "github.com/ethereum/go-ethereum/accounts"
5 | "github.com/ethereum/go-ethereum/accounts/keystore"
6 |
7 | "github.com/pkg/errors"
8 |
9 | celestiatypes "github.com/celestiaorg/celestia-app/x/qgb/types"
10 | "github.com/ethereum/go-ethereum/common"
11 | "github.com/ethereum/go-ethereum/crypto"
12 | )
13 |
14 | const (
15 | signaturePrefix = "\x19Ethereum Signed Message:\n32"
16 | )
17 |
18 | // NewEthereumSignature creates a new eip-191 signature over a given byte array.
19 | // hash: digest to be signed over.
20 | // ks: the keystore to use for the signature
21 | // acc: the account in the keystore to use for the signature
22 | func NewEthereumSignature(hash []byte, ks *keystore.KeyStore, acc accounts.Account) ([]byte, error) {
23 | if ks == nil {
24 | return nil, errors.Wrap(celestiatypes.ErrEmpty, "nil keystore")
25 | }
26 | protectedHash := crypto.Keccak256Hash([]uint8(signaturePrefix), hash)
27 | return ks.SignHash(acc, protectedHash.Bytes())
28 | }
29 |
30 | func EthAddressFromSignature(hash []byte, signature []byte) (common.Address, error) {
31 | if len(signature) < 65 {
32 | return common.Address{}, errors.Wrap(ErrInvalid, "signature too short")
33 | }
34 | protectedHash := crypto.Keccak256Hash([]uint8(signaturePrefix), hash)
35 | sigPublicKey, err := crypto.Ecrecover(protectedHash.Bytes(), signature)
36 | if err != nil {
37 | return common.Address{}, errors.Wrap(err, "ec recover failed")
38 | }
39 | pubKey, err := crypto.UnmarshalPubkey(sigPublicKey)
40 | if err != nil {
41 | return common.Address{}, errors.Wrap(err, "unmarshalling signature public key failed")
42 | }
43 | addr := crypto.PubkeyToAddress(*pubKey)
44 | return addr, nil
45 | }
46 |
47 | // ValidateEthereumSignature takes a message, an associated signature and public key and
48 | // returns an error if the signature isn't valid.
49 | func ValidateEthereumSignature(hash []byte, signature []byte, ethAddress common.Address) error {
50 | addr, err := EthAddressFromSignature(hash, signature)
51 | if err != nil {
52 | return errors.Wrap(err, "unable to get address from signature")
53 | }
54 |
55 | if addr.Hex() != ethAddress.Hex() {
56 | return errors.Wrap(ErrInvalid, "signature not matching")
57 | }
58 |
59 | return nil
60 | }
61 |
--------------------------------------------------------------------------------
/evm/ethereum_signature_test.go:
--------------------------------------------------------------------------------
1 | package evm_test
2 |
3 | import (
4 | "crypto/ecdsa"
5 | "testing"
6 |
7 | "github.com/ethereum/go-ethereum/accounts/keystore"
8 |
9 | "github.com/celestiaorg/orchestrator-relayer/evm"
10 | ethcmn "github.com/ethereum/go-ethereum/common"
11 | "github.com/ethereum/go-ethereum/common/hexutil"
12 | "github.com/ethereum/go-ethereum/crypto"
13 | "github.com/stretchr/testify/assert"
14 | "github.com/stretchr/testify/require"
15 | )
16 |
17 | // The signatures in these tests are generated using the foundry setup in the blobstream-contracts repository.
18 |
19 | func TestNewEthereumSignature(t *testing.T) {
20 | digest, err := hexutil.Decode("0x078c42ff72a01b355f9d76bfeecd2132a0d3f1aad9380870026c56e23e6d00e5")
21 | require.NoError(t, err)
22 | testPrivateKey, err := crypto.HexToECDSA("64a1d6f0e760a8d62b4afdde4096f16f51b401eaaecc915740f71770ea76a8ad")
23 | require.NoError(t, err)
24 | ks := keystore.NewKeyStore(t.TempDir(), keystore.LightScryptN, keystore.LightScryptP)
25 |
26 | tests := []struct {
27 | name string
28 | privKey *ecdsa.PrivateKey
29 | expectedSignature string
30 | expectErr bool
31 | }{
32 | {
33 | name: "valid signature",
34 | privKey: testPrivateKey,
35 | expectedSignature: "ca2aa01f5b32722238e8f45356878e2cfbdc7c3335fbbf4e1dc3dfc53465e3e137103769d6956414014ae340cc4cb97384b2980eea47942f135931865471031a00",
36 | expectErr: false,
37 | },
38 | }
39 | for _, tt := range tests {
40 | t.Run(tt.name, func(t *testing.T) {
41 | acc, err := ks.ImportECDSA(tt.privKey, "123")
42 | require.NoError(t, err)
43 | err = ks.Unlock(acc, "123")
44 | require.NoError(t, err)
45 |
46 | got, err := evm.NewEthereumSignature(digest, ks, acc)
47 | if tt.expectErr {
48 | assert.Error(t, err)
49 | } else {
50 | assert.NoError(t, err)
51 | assert.Equal(t, tt.expectedSignature, ethcmn.Bytes2Hex(got))
52 | }
53 | })
54 | }
55 | }
56 |
57 | func TestEthAddressFromSignature(t *testing.T) {
58 | digest, err := hexutil.Decode("0x078c42ff72a01b355f9d76bfeecd2132a0d3f1aad9380870026c56e23e6d00e5")
59 | require.NoError(t, err)
60 | signature, err := hexutil.Decode("0xca2aa01f5b32722238e8f45356878e2cfbdc7c3335fbbf4e1dc3dfc53465e3e137103769d6956414014ae340cc4cb97384b2980eea47942f135931865471031a00")
61 | require.NoError(t, err)
62 | address := ethcmn.HexToAddress("0x9c2B12b5a07FC6D719Ed7646e5041A7E85758329")
63 | tests := []struct {
64 | name string
65 | signature []byte
66 | expectedAddress ethcmn.Address
67 | expectErr bool
68 | }{
69 | {
70 | name: "valid signature and hash",
71 | signature: signature,
72 | expectedAddress: address,
73 | expectErr: false,
74 | },
75 | {
76 | name: "short signature",
77 | signature: func() []byte {
78 | wrongSig, _ := hexutil.Decode("0x12345")
79 | return wrongSig
80 | }(),
81 | expectErr: true,
82 | },
83 | {
84 | name: "invalid signature",
85 | signature: func() []byte {
86 | wrongSig := make([]byte, len(signature))
87 | copy(wrongSig, signature)
88 | wrongSig[10] = 10 // changing a single byte to make the signature invalid
89 | return wrongSig
90 | }(),
91 | expectErr: true,
92 | },
93 | }
94 | for _, tt := range tests {
95 | t.Run(tt.name, func(t *testing.T) {
96 | got, err := evm.EthAddressFromSignature(digest, tt.signature)
97 | if tt.expectErr {
98 | assert.Error(t, err)
99 | } else {
100 | assert.NoError(t, err)
101 | assert.Equal(t, tt.expectedAddress, got)
102 | }
103 | })
104 | }
105 | }
106 |
107 | func TestValidateEthereumSignature(t *testing.T) {
108 | digest, err := hexutil.Decode("0x078c42ff72a01b355f9d76bfeecd2132a0d3f1aad9380870026c56e23e6d00e5")
109 | require.NoError(t, err)
110 | signature, err := hexutil.Decode("0xca2aa01f5b32722238e8f45356878e2cfbdc7c3335fbbf4e1dc3dfc53465e3e137103769d6956414014ae340cc4cb97384b2980eea47942f135931865471031a00")
111 | require.NoError(t, err)
112 | address := ethcmn.HexToAddress("0x9c2B12b5a07FC6D719Ed7646e5041A7E85758329")
113 | tests := []struct {
114 | name string
115 | address ethcmn.Address
116 | digest []byte
117 | signature []byte
118 | expectErr bool
119 | }{
120 | {
121 | name: "valid digest, signature and hash",
122 | digest: digest,
123 | signature: signature,
124 | address: address,
125 | expectErr: false,
126 | },
127 | {
128 | name: "different address",
129 | digest: digest,
130 | signature: signature,
131 | address: ethcmn.HexToAddress("0x7c2B12b5a07FC6D719Ed7646e5041A7E85758329"),
132 | expectErr: true,
133 | },
134 | {
135 | name: "different digest",
136 | digest: []byte("12345"),
137 | signature: signature,
138 | address: ethcmn.HexToAddress("0x7c2B12b5a07FC6D719Ed7646e5041A7E85758329"),
139 | expectErr: true,
140 | },
141 | {
142 | name: "different signature",
143 | digest: digest,
144 | signature: func() []byte {
145 | wrongSig := make([]byte, len(signature))
146 | copy(wrongSig, signature)
147 |
148 | // changing a single byte to make the signature different but still valid
149 | wrongSig[10] = 10
150 | return wrongSig
151 | }(),
152 | address: address,
153 | expectErr: true,
154 | },
155 | }
156 | for _, tt := range tests {
157 | t.Run(tt.name, func(t *testing.T) {
158 | err := evm.ValidateEthereumSignature(digest, tt.signature, tt.address)
159 | if tt.expectErr {
160 | assert.Error(t, err)
161 | } else {
162 | assert.NoError(t, err)
163 | }
164 | })
165 | }
166 | }
167 |
--------------------------------------------------------------------------------
/evm/evm_client_test.go:
--------------------------------------------------------------------------------
1 | package evm_test
2 |
3 | import (
4 | "context"
5 | "math/big"
6 |
7 | "github.com/ethereum/go-ethereum/accounts/keystore"
8 |
9 | wrapper "github.com/celestiaorg/blobstream-contracts/v4/wrappers/Blobstream.sol"
10 | celestiatypes "github.com/celestiaorg/celestia-app/x/qgb/types"
11 | "github.com/celestiaorg/orchestrator-relayer/evm"
12 | "github.com/celestiaorg/orchestrator-relayer/types"
13 | ethcmn "github.com/ethereum/go-ethereum/common"
14 | )
15 |
16 | func (s *EVMTestSuite) TestSubmitDataCommitment() {
17 | // deploy a new bridge contract
18 | _, _, _, err := s.Client.DeployBlobstreamContract(s.Chain.Auth, s.Chain.Backend, *s.InitVs, 1, true)
19 | s.NoError(err)
20 |
21 | // we just need something to sign over, it doesn't matter what
22 | commitment := ethcmn.HexToHash("0x12345")
23 | signBytes := types.DataCommitmentTupleRootSignBytes(
24 | big.NewInt(2),
25 | commitment[:],
26 | )
27 |
28 | ks := keystore.NewKeyStore(s.T().TempDir(), keystore.LightScryptN, keystore.LightScryptP)
29 | acc, err := ks.ImportECDSA(s.VsPrivateKey, "123")
30 | s.NoError(err)
31 | err = ks.Unlock(acc, "123")
32 | s.NoError(err)
33 |
34 | signature, err := evm.NewEthereumSignature(signBytes.Bytes(), ks, acc)
35 | s.NoError(err)
36 |
37 | evmVals := make([]wrapper.Validator, len(s.InitVs.Members))
38 | for i, val := range s.InitVs.Members {
39 | evmVals[i] = wrapper.Validator{
40 | Addr: ethcmn.HexToAddress(val.EvmAddress),
41 | Power: big.NewInt(int64(val.Power)),
42 | }
43 | }
44 |
45 | hexSig := ethcmn.Bytes2Hex(signature)
46 | v, r, ss, err := evm.SigToVRS(hexSig)
47 | s.NoError(err)
48 | tx, err := s.Client.SubmitDataRootTupleRoot(
49 | s.Chain.Auth,
50 | commitment,
51 | 2,
52 | *s.InitVs,
53 | []wrapper.Signature{
54 | {
55 | V: v,
56 | R: r,
57 | S: ss,
58 | },
59 | },
60 | )
61 | s.NoError(err)
62 | s.Chain.Backend.Commit()
63 |
64 | recp, err := s.Chain.Backend.TransactionReceipt(context.TODO(), tx.Hash())
65 | s.NoError(err)
66 | s.Assert().Equal(uint64(1), recp.Status)
67 |
68 | dcNonce, err := s.Client.StateLastEventNonce(nil)
69 | s.NoError(err)
70 | s.Assert().Equal(uint64(2), dcNonce)
71 | }
72 |
73 | func (s *EVMTestSuite) TestUpdateValset() {
74 | // deploy a new bridge contract
75 | _, _, _, err := s.Client.DeployBlobstreamContract(s.Chain.Auth, s.Chain.Backend, *s.InitVs, 1, true)
76 | s.NoError(err)
77 |
78 | updatedValset := celestiatypes.Valset{
79 | Members: []celestiatypes.BridgeValidator{
80 | {
81 | EvmAddress: "0x9c2B12b5a07FC6D719Ed7646e5041A7E85758328",
82 | Power: 5000,
83 | },
84 | {
85 | EvmAddress: "0x9c2B12b5a07FC6D719Ed7646e5041A7E85758327",
86 | Power: 5000,
87 | },
88 | },
89 | // because the bridge was redeployed
90 | Nonce: 2,
91 | Height: 10,
92 | }
93 |
94 | ks := keystore.NewKeyStore(s.T().TempDir(), keystore.LightScryptN, keystore.LightScryptP)
95 | acc, err := ks.ImportECDSA(s.VsPrivateKey, "123")
96 | s.NoError(err)
97 | err = ks.Unlock(acc, "123")
98 | s.NoError(err)
99 |
100 | signBytes, err := updatedValset.SignBytes()
101 | s.NoError(err)
102 | signature, err := evm.NewEthereumSignature(signBytes.Bytes(), ks, acc)
103 | s.NoError(err)
104 |
105 | hexSig := ethcmn.Bytes2Hex(signature)
106 |
107 | evmVals := make([]wrapper.Validator, len(s.InitVs.Members))
108 | for i, val := range s.InitVs.Members {
109 | evmVals[i] = wrapper.Validator{
110 | Addr: ethcmn.HexToAddress(val.EvmAddress),
111 | Power: big.NewInt(int64(val.Power)),
112 | }
113 | }
114 |
115 | thresh := updatedValset.TwoThirdsThreshold()
116 |
117 | v, r, ss, err := evm.SigToVRS(hexSig)
118 | s.NoError(err)
119 |
120 | tx, err := s.Client.UpdateValidatorSet(
121 | s.Chain.Auth,
122 | 2,
123 | thresh,
124 | *s.InitVs,
125 | updatedValset,
126 | []wrapper.Signature{
127 | {
128 | V: v,
129 | R: r,
130 | S: ss,
131 | },
132 | },
133 | )
134 | s.NoError(err)
135 | s.Chain.Backend.Commit()
136 |
137 | recp, err := s.Chain.Backend.TransactionReceipt(context.TODO(), tx.Hash())
138 | s.NoError(err)
139 | s.Equal(uint64(1), recp.Status)
140 |
141 | nonce, err := s.Client.StateLastEventNonce(nil)
142 | s.NoError(err)
143 | // check that the validator set was changed.
144 | s.Equal(uint64(2), nonce)
145 | }
146 |
--------------------------------------------------------------------------------
/evm/evm_transaction_opts.go:
--------------------------------------------------------------------------------
1 | package evm
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "math/big"
7 | "strings"
8 |
9 | "github.com/ethereum/go-ethereum/accounts"
10 | "github.com/ethereum/go-ethereum/accounts/keystore"
11 |
12 | "github.com/pkg/errors"
13 |
14 | "github.com/ethereum/go-ethereum/accounts/abi/bind"
15 | ethcmn "github.com/ethereum/go-ethereum/common"
16 | "github.com/ethereum/go-ethereum/ethclient"
17 | )
18 |
19 | // TODO: make gas price configurable.
20 | type transactOpsBuilder func(ctx context.Context, client *ethclient.Client, gasLim uint64) (*bind.TransactOpts, error)
21 |
22 | func newTransactOptsBuilder(ks *keystore.KeyStore, acc *accounts.Account) transactOpsBuilder {
23 | return func(ctx context.Context, client *ethclient.Client, gasLim uint64) (*bind.TransactOpts, error) {
24 | nonce, err := client.PendingNonceAt(ctx, acc.Address)
25 | if err != nil {
26 | return nil, err
27 | }
28 |
29 | ethChainID, err := client.ChainID(ctx)
30 | if err != nil {
31 | return nil, fmt.Errorf("failed to get Ethereum chain ID: %w", err)
32 | }
33 |
34 | auth, err := bind.NewKeyStoreTransactorWithChainID(ks, *acc, ethChainID)
35 | if err != nil {
36 | return nil, fmt.Errorf("failed to create Ethereum transactor: %w", err)
37 | }
38 |
39 | auth.Nonce = new(big.Int).SetUint64(nonce)
40 | auth.Value = big.NewInt(0) // in wei
41 | auth.GasLimit = gasLim // in units
42 |
43 | return auth, nil
44 | }
45 | }
46 |
47 | const (
48 | MalleabilityThreshold = "0x7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0"
49 | ZeroSValue = "0x0000000000000000000000000000000000000000000000000000000000000000"
50 | )
51 |
52 | // SigToVRS breaks apart a signature into its components to make it compatible with the contracts
53 | // The validation done in here is defined under https://github.com/celestiaorg/orchestrator-relayer/issues/105
54 | func SigToVRS(sigHex string) (v uint8, r, s ethcmn.Hash, err error) {
55 | signatureBytes := ethcmn.FromHex(strings.ToLower(sigHex))
56 |
57 | // signature length should be 65: 32 bytes + vParam
58 | if len(signatureBytes) != 65 {
59 | err = errors.Wrap(ErrInvalid, "signature length")
60 | return
61 | }
62 |
63 | // vParam should be 0, 1, 27 or 28
64 | vParam := signatureBytes[64]
65 | switch vParam {
66 | case byte(0):
67 | vParam = byte(27)
68 | case byte(1):
69 | vParam = byte(28)
70 | case byte(27):
71 | case byte(28):
72 | default:
73 | err = errors.Wrap(ErrInvalid, "signature vParam. Should be 0, 1, 27 or 28")
74 | return
75 | }
76 |
77 | v = vParam
78 | r = ethcmn.BytesToHash(signatureBytes[0:32])
79 | s = ethcmn.BytesToHash(signatureBytes[32:64])
80 |
81 | // sValue shouldn't be malleable
82 | if MalleabilityThreshold <= s.String() || s.String() == ZeroSValue {
83 | err = errors.Wrap(ErrInvalid, "signature s. Should be 0 < s < secp256k1n ÷ 2 + 1")
84 | return
85 | }
86 |
87 | return
88 | }
89 |
--------------------------------------------------------------------------------
/evm/evm_transaction_opts_test.go:
--------------------------------------------------------------------------------
1 | package evm_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/celestiaorg/orchestrator-relayer/evm"
7 | "github.com/stretchr/testify/assert"
8 | )
9 |
10 | func TestSigToVRS(t *testing.T) {
11 | tests := []struct {
12 | name string
13 | signature string
14 | expectedR string
15 | expectedS string
16 | expectedV uint8
17 | expectErr bool
18 | }{
19 | {
20 | name: "valid signature with vParam=0",
21 | signature: "0xca2aa01f5b32722238e8f45356878e2cfbdc7c3335fbbf4e1dc3dfc53465e3e137103769d6956414014ae340cc4cb97384b2980eea47942f135931865471031a00",
22 | expectedR: "0xca2aa01f5b32722238e8f45356878e2cfbdc7c3335fbbf4e1dc3dfc53465e3e1",
23 | expectedS: "0x37103769d6956414014ae340cc4cb97384b2980eea47942f135931865471031a",
24 | expectedV: uint8(27),
25 | expectErr: false,
26 | },
27 | {
28 | name: "valid signature with vParam=27",
29 | signature: "0xca2aa01f5b32722238e8f45356878e2cfbdc7c3335fbbf4e1dc3dfc53465e3e137103769d6956414014ae340cc4cb97384b2980eea47942f135931865471031a1b",
30 | expectedR: "0xca2aa01f5b32722238e8f45356878e2cfbdc7c3335fbbf4e1dc3dfc53465e3e1",
31 | expectedS: "0x37103769d6956414014ae340cc4cb97384b2980eea47942f135931865471031a",
32 | expectedV: uint8(27),
33 | expectErr: false,
34 | },
35 | {
36 | name: "short signature",
37 | signature: "0xca2aa01f5b32722238e8f45356878e2cfbdc7c3335fbbf4e1dc3dfc53465e3e137103769d6956414014ae340cc4cb97384b2980eea47942f135931865471031a",
38 | expectErr: true,
39 | },
40 | {
41 | name: "long signature",
42 | signature: "0xca2aa01f5b32722238e8f45356878e2cfbdc7c3335fbbf4e1dc3dfc53465e3e137103769d6956414014ae340cc4cb97384b2980eea47942f135931865471031a001b1",
43 | expectErr: true,
44 | },
45 | {
46 | name: "valid signature with invalid vParam=10",
47 | signature: "0xca2aa01f5b32722238e8f45356878e2cfbdc7c3335fbbf4e1dc3dfc53465e3e137103769d6956414014ae340cc4cb97384b2980eea47942f135931865471031a0a",
48 | expectErr: true,
49 | },
50 | {
51 | name: "invalid zero sParam",
52 | signature: "0xca2aa01f5b32722238e8f45356878e2cfbdc7c3335fbbf4e1dc3dfc53465e3e1000000000000000000000000000000000000000000000000000000000000000000",
53 | expectErr: true,
54 | },
55 | {
56 | name: "sParam higher than malleability threshold",
57 | signature: "0xca2aa01f5b32722238e8f45356878e2cfbdc7c3335fbbf4e1dc3dfc53465e3e17fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a000",
58 | expectErr: true,
59 | },
60 | }
61 | for _, tt := range tests {
62 | t.Run(tt.name, func(t *testing.T) {
63 | v, r, s, err := evm.SigToVRS(tt.signature)
64 | if tt.expectErr {
65 | assert.Error(t, err)
66 | } else {
67 | assert.NoError(t, err)
68 | assert.Equal(t, tt.expectedV, v)
69 | assert.Equal(t, tt.expectedR, r.Hex())
70 | assert.Equal(t, tt.expectedS, s.Hex())
71 | }
72 | })
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/evm/suite_test.go:
--------------------------------------------------------------------------------
1 | package evm_test
2 |
3 | import (
4 | "crypto/ecdsa"
5 | "testing"
6 | "time"
7 |
8 | "github.com/ethereum/go-ethereum/accounts/keystore"
9 |
10 | celestiatypes "github.com/celestiaorg/celestia-app/x/qgb/types"
11 | "github.com/celestiaorg/orchestrator-relayer/evm"
12 | blobstreamtesting "github.com/celestiaorg/orchestrator-relayer/testing"
13 | ethcmn "github.com/ethereum/go-ethereum/common"
14 | "github.com/ethereum/go-ethereum/crypto"
15 | "github.com/stretchr/testify/require"
16 | "github.com/stretchr/testify/suite"
17 | )
18 |
19 | type EVMTestSuite struct {
20 | suite.Suite
21 | Chain *blobstreamtesting.EVMChain
22 | Client *evm.Client
23 | InitVs *celestiatypes.Valset
24 | VsPrivateKey *ecdsa.PrivateKey
25 | }
26 |
27 | func (s *EVMTestSuite) SetupTest() {
28 | t := s.T()
29 | testPrivateKey, err := crypto.HexToECDSA("64a1d6f0e760a8d62b4afdde4096f16f51b401eaaecc915740f71770ea76a8ad")
30 | s.VsPrivateKey = testPrivateKey
31 | require.NoError(t, err)
32 | s.Chain = blobstreamtesting.NewEVMChain(testPrivateKey)
33 |
34 | ks := keystore.NewKeyStore(t.TempDir(), keystore.LightScryptN, keystore.LightScryptP)
35 | acc, err := ks.ImportECDSA(testPrivateKey, "123")
36 | require.NoError(t, err)
37 | err = ks.Unlock(acc, "123")
38 | require.NoError(t, err)
39 |
40 | s.Client = blobstreamtesting.NewEVMClient(ks, &acc)
41 | s.InitVs, err = celestiatypes.NewValset(
42 | 1,
43 | 10,
44 | celestiatypes.InternalBridgeValidators{{
45 | Power: 1000,
46 | EVMAddress: ethcmn.HexToAddress("0x9c2B12b5a07FC6D719Ed7646e5041A7E85758329"),
47 | }},
48 | time.Now(),
49 | )
50 | require.NoError(t, err)
51 | }
52 |
53 | func (s *EVMTestSuite) TearDown() {
54 | s.Chain.Close()
55 | }
56 |
57 | func TestEVMSuite(t *testing.T) {
58 | suite.Run(t, new(EVMTestSuite))
59 | }
60 |
--------------------------------------------------------------------------------
/helpers/interrupt.go:
--------------------------------------------------------------------------------
1 | package helpers
2 |
3 | import (
4 | "context"
5 | "os"
6 | "os/signal"
7 | "syscall"
8 |
9 | tmlog "github.com/tendermint/tendermint/libs/log"
10 | )
11 |
12 | // TrapSignal will listen for any OS signal and cancel the context to gracefully exit.
13 | func TrapSignal(logger tmlog.Logger, cancel context.CancelFunc) {
14 | sigCh := make(chan os.Signal, 1)
15 |
16 | signal.Notify(sigCh, syscall.SIGTERM)
17 | signal.Notify(sigCh, syscall.SIGINT)
18 |
19 | sig := <-sigCh
20 | logger.Info("caught signal; shutting down...", "signal", sig.String())
21 | cancel()
22 | }
23 |
--------------------------------------------------------------------------------
/helpers/parse.go:
--------------------------------------------------------------------------------
1 | package helpers
2 |
3 | import (
4 | "github.com/libp2p/go-libp2p/core/peer"
5 | tmlog "github.com/tendermint/tendermint/libs/log"
6 | )
7 |
8 | // ParseAddrInfos converts strings to AddrInfos
9 | func ParseAddrInfos(logger tmlog.Logger, addrs []string) ([]peer.AddrInfo, error) {
10 | infos := make([]peer.AddrInfo, 0, len(addrs))
11 | for _, addr := range addrs {
12 | info, err := peer.AddrInfoFromString(addr)
13 | if err != nil {
14 | logger.Error("parsing info from multiaddr", "addr", addr, "err", err)
15 | return nil, err
16 | }
17 | infos = append(infos, *info)
18 | }
19 | return infos, nil
20 | }
21 |
--------------------------------------------------------------------------------
/helpers/parse_test.go:
--------------------------------------------------------------------------------
1 | package helpers
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/libp2p/go-libp2p/core/peer"
7 | "github.com/stretchr/testify/assert"
8 | tmlog "github.com/tendermint/tendermint/libs/log"
9 | )
10 |
11 | func TestParseAddrInfos(t *testing.T) {
12 | testCases := []struct {
13 | name string
14 | addrs []string
15 | want []peer.AddrInfo
16 | shouldErr bool
17 | }{
18 | {
19 | name: "empty input",
20 | addrs: []string{},
21 | want: []peer.AddrInfo{},
22 | },
23 | {
24 | name: "valid input",
25 | addrs: []string{
26 | "/ip4/127.0.0.1/tcp/8080/p2p/12D3KooWHr2wqFAsMXnPzpFsgxmePgXb8BqpkePebwUgLyZc95bd",
27 | "/dns4/limani.celestia-devops.dev/tcp/2121/p2p/12D3KooWDgG69kXfmSiHjUErN2ahpUC1SXpSfB2urrqMZ6aWC8NS",
28 | },
29 | want: []peer.AddrInfo{
30 | func() peer.AddrInfo {
31 | info, _ := peer.AddrInfoFromString("/ip4/127.0.0.1/tcp/8080/p2p/12D3KooWHr2wqFAsMXnPzpFsgxmePgXb8BqpkePebwUgLyZc95bd")
32 | return *info
33 | }(),
34 | func() peer.AddrInfo {
35 | info, _ := peer.AddrInfoFromString("/dns4/limani.celestia-devops.dev/tcp/2121/p2p/12D3KooWDgG69kXfmSiHjUErN2ahpUC1SXpSfB2urrqMZ6aWC8NS")
36 | return *info
37 | }(),
38 | },
39 | },
40 | {
41 | name: "invalid multiaddr",
42 | addrs: []string{
43 | "/ip4/127.0.0.1/tcp/8080",
44 | "invalid-multiaddr",
45 | },
46 | shouldErr: true,
47 | },
48 | }
49 |
50 | for _, tc := range testCases {
51 | t.Run(tc.name, func(t *testing.T) {
52 | got, err := ParseAddrInfos(tmlog.NewNopLogger(), tc.addrs)
53 | if tc.shouldErr {
54 | assert.Error(t, err)
55 | assert.Nil(t, got)
56 | } else {
57 | assert.NoError(t, err)
58 | for i, info := range tc.want {
59 | assert.Equal(t, info.ID.String(), got[i].ID.String())
60 | assert.Equal(t, info.Addrs, got[i].Addrs)
61 | }
62 | }
63 | })
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/helpers/retrier.go:
--------------------------------------------------------------------------------
1 | package helpers
2 |
3 | import (
4 | "context"
5 | "time"
6 |
7 | tmlog "github.com/tendermint/tendermint/libs/log"
8 | )
9 |
10 | // Retrier handles retries of failed services.
11 | type Retrier struct {
12 | logger tmlog.Logger
13 | retriesNumber int
14 | baseDelay time.Duration
15 | }
16 |
17 | // DefaultRetrierDelay default retrier baseDelay
18 | const DefaultRetrierDelay = 10 * time.Second
19 |
20 | func NewRetrier(logger tmlog.Logger, retriesNumber int, baseDelay time.Duration) *Retrier {
21 | return &Retrier{
22 | logger: logger,
23 | retriesNumber: retriesNumber,
24 | baseDelay: baseDelay,
25 | }
26 | }
27 |
28 | // Retry retries the `retryMethod` for `r.retriesNumber` times, separated by an exponential delay
29 | // calculated using the `NextTick(retryCount)` method.
30 | // Returns the final execution error if all retries failed.
31 | func (r Retrier) Retry(ctx context.Context, retryMethod func() error) error {
32 | r.logger.Info("trying to recover from error...")
33 | var err error
34 | for i := 0; i < r.retriesNumber; i++ {
35 | nextTick := time.NewTimer(r.NextTick(i))
36 | select {
37 | case <-ctx.Done():
38 | return ctx.Err()
39 | case <-nextTick.C:
40 | r.logger.Debug("retrying", "retry_number", i, "retries_left", r.retriesNumber-i)
41 | err = retryMethod()
42 | if err == nil {
43 | r.logger.Info("succeeded", "retries_number", i)
44 | return nil
45 | }
46 | r.logger.Debug("failed attempt", "retry", i, "err", err)
47 | }
48 | }
49 | return err
50 | }
51 |
52 | // RetryThenFail similar to `Retry` but panics upon failure.
53 | func (r Retrier) RetryThenFail(ctx context.Context, retryMethod func() error) {
54 | err := r.Retry(ctx, retryMethod)
55 | if err != nil {
56 | panic(err)
57 | }
58 | }
59 |
60 | // NextTick calculates the next exponential tick based on the provided retry count
61 | // and the initialized base delay.
62 | func (r Retrier) NextTick(retryCount int) time.Duration {
63 | return 1 << retryCount * r.baseDelay
64 | }
65 |
--------------------------------------------------------------------------------
/helpers/retrier_test.go:
--------------------------------------------------------------------------------
1 | package helpers_test
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "testing"
7 | "time"
8 |
9 | "github.com/celestiaorg/orchestrator-relayer/helpers"
10 |
11 | "github.com/stretchr/testify/assert"
12 | tmlog "github.com/tendermint/tendermint/libs/log"
13 | )
14 |
15 | func TestRetry(t *testing.T) {
16 | ret := helpers.NewRetrier(tmlog.NewNopLogger(), 10, time.Millisecond)
17 | var count int
18 | tests := []struct {
19 | name string
20 | f func() error
21 | expectedCount int
22 | wantErr bool
23 | }{
24 | {
25 | name: "always error",
26 | f: func() error {
27 | count++
28 | return errors.New("test error")
29 | },
30 | expectedCount: 10,
31 | wantErr: true,
32 | },
33 | {
34 | name: "never error",
35 | f: func() error {
36 | count++
37 | return nil
38 | },
39 | expectedCount: 1,
40 | wantErr: false,
41 | },
42 | {
43 | name: "error in the middle",
44 | f: func() error {
45 | count++
46 | if count == 5 {
47 | return nil
48 | }
49 | return errors.New("test error")
50 | },
51 | expectedCount: 5,
52 | wantErr: false,
53 | },
54 | }
55 | for _, tt := range tests {
56 | t.Run(tt.name, func(t *testing.T) {
57 | count = 0
58 | err := ret.Retry(context.Background(), func() error {
59 | return tt.f()
60 | })
61 | if tt.wantErr {
62 | assert.Error(t, err)
63 | } else {
64 | assert.NoError(t, err)
65 | }
66 | assert.Equal(t, tt.expectedCount, count)
67 | })
68 | }
69 | }
70 |
71 | func TestRetryThenFail(t *testing.T) {
72 | ret := helpers.NewRetrier(tmlog.NewNopLogger(), 10, time.Millisecond)
73 | var count int
74 | tests := []struct {
75 | name string
76 | f func() error
77 | expectedCount int
78 | wantPanic bool
79 | }{
80 | {
81 | name: "panic at the end",
82 | f: func() error {
83 | count++
84 | return errors.New("test error")
85 | },
86 | expectedCount: 10,
87 | wantPanic: true,
88 | },
89 | {
90 | name: "never panic",
91 | f: func() error {
92 | count++
93 | return nil
94 | },
95 | expectedCount: 1,
96 | wantPanic: false,
97 | },
98 | }
99 | for _, tt := range tests {
100 | t.Run(tt.name, func(t *testing.T) {
101 | count = 0
102 | if tt.wantPanic {
103 | assert.Panics(t, func() {
104 | ret.RetryThenFail(context.Background(), func() error {
105 | return tt.f()
106 | })
107 | })
108 | } else {
109 | assert.NotPanics(t, func() {
110 | ret.RetryThenFail(context.Background(), func() error {
111 | return tt.f()
112 | })
113 | })
114 | }
115 | assert.Equal(t, tt.expectedCount, count)
116 | })
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/helpers/ticker.go:
--------------------------------------------------------------------------------
1 | package helpers
2 |
3 | import (
4 | "context"
5 | "time"
6 | )
7 |
8 | // ImmediateTicker is a wrapper around time.Ticker that ticks an extra time during its creation before
9 | // starting to tick on every `duration`.
10 | // The reason for adding it is for multiple services that use the ticker, to be able to run their logic
11 | // a single time before waiting for the `duration` to elapse.
12 | // This allows for faster execution.
13 | func ImmediateTicker(ctx context.Context, duration time.Duration, f func() error) error {
14 | ticker := time.NewTicker(duration)
15 | if err := f(); err != nil {
16 | return err
17 | }
18 | for {
19 | select {
20 | case <-ctx.Done():
21 | return ctx.Err()
22 | case <-ticker.C:
23 | err := f()
24 | if err != nil {
25 | return err
26 | }
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/helpers/ticker_test.go:
--------------------------------------------------------------------------------
1 | package helpers
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "testing"
7 | "time"
8 |
9 | "github.com/stretchr/testify/assert"
10 | )
11 |
12 | func TestImmediateTicker(t *testing.T) {
13 | count := 0
14 | tests := []struct {
15 | name string
16 | f func() error
17 | expectedErrCount int
18 | }{
19 | {
20 | name: "error at the first execution",
21 | f: func() error {
22 | count++
23 | return errors.New("test error")
24 | },
25 | expectedErrCount: 1,
26 | },
27 | {
28 | name: "error at the second execution",
29 | f: func() error {
30 | count++
31 | if count == 2 {
32 | return errors.New("test error")
33 | }
34 | return nil
35 | },
36 | expectedErrCount: 2,
37 | },
38 | {
39 | name: "error at the third execution",
40 | f: func() error {
41 | count++
42 | if count == 3 {
43 | return errors.New("test error")
44 | }
45 | return nil
46 | },
47 | expectedErrCount: 3,
48 | },
49 | }
50 | for _, tt := range tests {
51 | t.Run(tt.name, func(t *testing.T) {
52 | count = 0
53 | err := ImmediateTicker(context.Background(), time.Millisecond, tt.f)
54 | assert.Error(t, err)
55 | assert.Equal(t, tt.expectedErrCount, count)
56 | })
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/orchestrator/broadcaster.go:
--------------------------------------------------------------------------------
1 | package orchestrator
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/celestiaorg/orchestrator-relayer/p2p"
7 |
8 | "github.com/celestiaorg/orchestrator-relayer/types"
9 | )
10 |
11 | type Broadcaster struct {
12 | BlobstreamDHT *p2p.BlobstreamDHT
13 | }
14 |
15 | func NewBroadcaster(blobStreamDHT *p2p.BlobstreamDHT) *Broadcaster {
16 | return &Broadcaster{BlobstreamDHT: blobStreamDHT}
17 | }
18 |
19 | func (b Broadcaster) ProvideDataCommitmentConfirm(ctx context.Context, nonce uint64, confirm types.DataCommitmentConfirm, dataRootTupleRoot string) error {
20 | if len(b.BlobstreamDHT.RoutingTable().ListPeers()) == 0 {
21 | return ErrEmptyPeersTable
22 | }
23 | return b.BlobstreamDHT.PutDataCommitmentConfirm(ctx, p2p.GetDataCommitmentConfirmKey(nonce, confirm.EthAddress, dataRootTupleRoot), confirm)
24 | }
25 |
26 | func (b Broadcaster) ProvideValsetConfirm(ctx context.Context, nonce uint64, confirm types.ValsetConfirm, signBytes string) error {
27 | if len(b.BlobstreamDHT.RoutingTable().ListPeers()) == 0 {
28 | return ErrEmptyPeersTable
29 | }
30 | return b.BlobstreamDHT.PutValsetConfirm(ctx, p2p.GetValsetConfirmKey(nonce, confirm.EthAddress, signBytes), confirm)
31 | }
32 |
33 | func (b Broadcaster) ProvideLatestValset(ctx context.Context, latestValset types.LatestValset) error {
34 | if len(b.BlobstreamDHT.RoutingTable().ListPeers()) == 0 {
35 | return ErrEmptyPeersTable
36 | }
37 | return b.BlobstreamDHT.PutLatestValset(ctx, latestValset)
38 | }
39 |
--------------------------------------------------------------------------------
/orchestrator/errors.go:
--------------------------------------------------------------------------------
1 | package orchestrator
2 |
3 | import "errors"
4 |
5 | var (
6 | ErrEmptyPeersTable = errors.New("empty peers table")
7 | ErrSignalChanNotif = errors.New("signal channel sent notification to stop")
8 | )
9 |
--------------------------------------------------------------------------------
/orchestrator/suite_test.go:
--------------------------------------------------------------------------------
1 | package orchestrator_test
2 |
3 | import (
4 | "context"
5 | "testing"
6 | "time"
7 |
8 | "github.com/celestiaorg/celestia-app/app"
9 | "github.com/celestiaorg/celestia-app/app/encoding"
10 | "github.com/celestiaorg/celestia-app/test/util/testnode"
11 | "github.com/celestiaorg/celestia-app/x/qgb/types"
12 | "github.com/celestiaorg/orchestrator-relayer/orchestrator"
13 | blobstreamtesting "github.com/celestiaorg/orchestrator-relayer/testing"
14 | "github.com/stretchr/testify/suite"
15 | )
16 |
17 | type OrchestratorTestSuite struct {
18 | suite.Suite
19 | Node *blobstreamtesting.TestNode
20 | Orchestrator *orchestrator.Orchestrator
21 | }
22 |
23 | func (s *OrchestratorTestSuite) SetupSuite() {
24 | t := s.T()
25 | ctx := context.Background()
26 | codec := encoding.MakeConfig(app.ModuleEncodingRegisters...).Codec
27 | s.Node = blobstreamtesting.NewTestNode(
28 | ctx,
29 | t,
30 | blobstreamtesting.CelestiaNetworkParams{
31 | GenesisOpts: []testnode.GenesisOption{
32 | testnode.ImmediateProposals(codec),
33 | blobstreamtesting.SetDataCommitmentWindowParams(codec, types.Params{DataCommitmentWindow: 101}),
34 | },
35 | TimeIotaMs: 1,
36 | Pruning: "default",
37 | TimeoutCommit: 5 * time.Millisecond,
38 | },
39 | )
40 | s.Orchestrator = blobstreamtesting.NewOrchestrator(t, s.Node)
41 | }
42 |
43 | func (s *OrchestratorTestSuite) TearDownSuite() {
44 | s.Node.Close()
45 | }
46 |
47 | func TestOrchestrator(t *testing.T) {
48 | suite.Run(t, new(OrchestratorTestSuite))
49 | }
50 |
--------------------------------------------------------------------------------
/p2p/errors.go:
--------------------------------------------------------------------------------
1 | package p2p
2 |
3 | import (
4 | "errors"
5 | )
6 |
7 | var (
8 | ErrPeersTimeout = errors.New("timeout while waiting for peers")
9 | ErrPeersThresholdCannotBeNegative = errors.New("peers threshold cannot be negative")
10 | ErrNilPrivateKey = errors.New("private key cannot be nil")
11 | ErrNotEnoughValsetConfirms = errors.New("couldn't find enough valset confirms")
12 | ErrNotEnoughDataCommitmentConfirms = errors.New("couldn't find enough data commitment confirms")
13 | ErrInvalidConfirmNamespace = errors.New("invalid confirm namespace")
14 | ErrInvalidEVMAddress = errors.New("invalid evm address")
15 | ErrNotTheSameEVMAddress = errors.New("not the same evm address")
16 | ErrInvalidConfirmKey = errors.New("invalid confirm key")
17 | ErrNoValues = errors.New("can't select from no values")
18 | ErrNoValidValueFound = errors.New("no valid dht confirm value found")
19 | ErrEmptyNamespace = errors.New("empty namespace")
20 | ErrEmptyEVMAddr = errors.New("empty evm address")
21 | ErrEmptyDigest = errors.New("empty digest")
22 | ErrEmptyValset = errors.New("empty valset")
23 | ErrInvalidLatestValsetKey = errors.New("invalid latest valset key")
24 | )
25 |
--------------------------------------------------------------------------------
/p2p/host.go:
--------------------------------------------------------------------------------
1 | package p2p
2 |
3 | import (
4 | "github.com/libp2p/go-libp2p"
5 | "github.com/libp2p/go-libp2p/core/crypto"
6 | "github.com/libp2p/go-libp2p/core/host"
7 | "github.com/multiformats/go-multiaddr"
8 | "github.com/prometheus/client_golang/prometheus"
9 | )
10 |
11 | // CreateHost Creates a LibP2P host using a listen address and a private key.
12 | // The listen address is a MultiAddress of the format: /ip4/0.0.0.0/tcp/0
13 | // Using port 0 means that it will use a random open port.
14 | // The private key shouldn't be nil.
15 | func CreateHost(listenMultiAddr string, privateKey crypto.PrivKey, registerer prometheus.Registerer) (host.Host, error) {
16 | multiAddr, err := multiaddr.NewMultiaddr(listenMultiAddr)
17 | if err != nil {
18 | return nil, err
19 | }
20 |
21 | if privateKey == nil {
22 | return nil, ErrNilPrivateKey
23 | }
24 |
25 | params := []libp2p.Option{libp2p.ListenAddrs(multiAddr), libp2p.Identity(privateKey), libp2p.EnableNATService()}
26 | if registerer != nil {
27 | params = append(params, libp2p.PrometheusRegisterer(registerer))
28 | } else {
29 | params = append(params, libp2p.DisableMetrics())
30 | }
31 |
32 | h, err := libp2p.New(params...)
33 | if err != nil {
34 | return nil, err
35 | }
36 |
37 | return h, nil
38 | }
39 |
--------------------------------------------------------------------------------
/p2p/host_test.go:
--------------------------------------------------------------------------------
1 | package p2p_test
2 |
3 | import (
4 | "context"
5 | "encoding/hex"
6 | "testing"
7 |
8 | "github.com/celestiaorg/orchestrator-relayer/p2p"
9 | "github.com/libp2p/go-libp2p/core/crypto"
10 | "github.com/libp2p/go-libp2p/core/peer"
11 | "github.com/stretchr/testify/assert"
12 | "github.com/stretchr/testify/require"
13 | )
14 |
15 | func TestCreateHost(t *testing.T) {
16 | validKeyHex, err := hex.DecodeString("398E0B0478862529D79F21C028317DD181C1E67AF44D099CDC78BD064A13DF671FC02B117A792377BFE4E931045FB47BAB9B236ED3AC43A254A76E9CE9CAD8B8")
17 | require.NoError(t, err)
18 | validPrivateKey, err := crypto.UnmarshalEd25519PrivateKey(validKeyHex)
19 | require.NoError(t, err)
20 |
21 | tests := []struct {
22 | name string
23 | listenMultiAddr string
24 | privateKey crypto.PrivKey
25 | wantErr bool
26 | }{
27 | {
28 | name: "valid input",
29 | listenMultiAddr: "/ip4/127.0.0.1/tcp/0",
30 | privateKey: validPrivateKey,
31 | wantErr: false,
32 | },
33 | {
34 | name: "invalid multiaddress",
35 | listenMultiAddr: "invalid_multiaddress",
36 | privateKey: validPrivateKey,
37 | wantErr: true,
38 | },
39 | {
40 | name: "invalid private key",
41 | listenMultiAddr: "/ip4/127.0.0.1/tcp/0",
42 | privateKey: nil,
43 | wantErr: true,
44 | },
45 | }
46 | for _, tt := range tests {
47 | t.Run(tt.name, func(t *testing.T) {
48 | host, err := p2p.CreateHost(tt.listenMultiAddr, tt.privateKey, nil)
49 | if tt.wantErr {
50 | assert.Error(t, err)
51 | } else {
52 | assert.NoError(t, err)
53 | assert.NotNil(t, host)
54 | assert.NoError(t, host.Close())
55 | }
56 | })
57 | }
58 | }
59 |
60 | func TestConnectHosts(t *testing.T) {
61 | validKeyHex1, err := hex.DecodeString("398E0B0478862529D79F21C028317DD181C1E67AF44D099CDC78BD064A13DF671FC02B117A792377BFE4E931045FB47BAB9B236ED3AC43A254A76E9CE9CAD8B8")
62 | require.NoError(t, err)
63 | validPrivateKey1, err := crypto.UnmarshalEd25519PrivateKey(validKeyHex1)
64 | require.NoError(t, err)
65 |
66 | validKeyHex2, err := hex.DecodeString("1BFC789DBD7B3CA13B4CF47898088CBB5CE467668DA63740ADF62B06F474452C6E12BD8B0C964D17438B8FEE1AC019D5290E2D4BE5BEED0113E13926581FFCB4")
67 | require.NoError(t, err)
68 | validPrivateKey2, err := crypto.UnmarshalEd25519PrivateKey(validKeyHex2)
69 | require.NoError(t, err)
70 |
71 | host1, err := p2p.CreateHost("/ip4/0.0.0.0/tcp/0", validPrivateKey1, nil)
72 | require.NoError(t, err)
73 | require.NotNil(t, host1)
74 |
75 | host2, err := p2p.CreateHost("/ip4/0.0.0.0/tcp/0", validPrivateKey2, nil)
76 | require.NoError(t, err)
77 | require.NotNil(t, host2)
78 |
79 | err = host1.Connect(context.Background(), peer.AddrInfo{
80 | ID: host2.ID(),
81 | Addrs: host2.Addrs(),
82 | })
83 | require.NoError(t, err)
84 |
85 | haha := host2.Peerstore().PeerInfo(host1.ID()).ID
86 | assert.Equal(t, haha, host1.ID())
87 |
88 | assert.NoError(t, host1.Close())
89 | assert.NoError(t, host2.Close())
90 | }
91 |
--------------------------------------------------------------------------------
/p2p/keys.go:
--------------------------------------------------------------------------------
1 | package p2p
2 |
3 | import (
4 | "fmt"
5 | "strconv"
6 | "strings"
7 | )
8 |
9 | // GetDataCommitmentConfirmKey creates a data commitment confirm in the
10 | // format: "//::":
11 | // - nonce: in hex format
12 | // - evm address: the 0x prefixed orchestrator EVM address in hex format
13 | // - data root tuple root: is the digest, in a 0x prefixed hex format, that is signed over for a
14 | // data commitment and whose signature is relayed to the Blobstream smart contract.
15 | // Expects the EVM address to be a correct address.
16 | func GetDataCommitmentConfirmKey(nonce uint64, evmAddr string, dataRootTupleRoot string) string {
17 | return "/" + DataCommitmentConfirmNamespace + "/" +
18 | strconv.FormatUint(nonce, 16) + ":" +
19 | evmAddr + ":" + dataRootTupleRoot
20 | }
21 |
22 | // GetValsetConfirmKey creates a valset confirm in the
23 | // format: "//::":
24 | // - nonce: in hex format
25 | // - evm address: the orchestrator EVM address in hex format
26 | // - sign bytes: is the digest, in a 0x prefixed hex format, that is signed over for a valset and
27 | // whose signature is relayed to the Blobstream smart contract.
28 | // Expects the EVM address to be a correct address.
29 | func GetValsetConfirmKey(nonce uint64, evmAddr string, signBytes string) string {
30 | return "/" + ValsetConfirmNamespace + "/" +
31 | strconv.FormatUint(nonce, 16) + ":" +
32 | evmAddr + ":" + signBytes
33 | }
34 |
35 | // GetLatestValsetKey creates the latest valset key.
36 | func GetLatestValsetKey() string {
37 | return "/" + LatestValsetNamespace + "/latest"
38 | }
39 |
40 | // ParseKey parses a key and returns its fields.
41 | // Will return an error if the key is missing some fields, some fields are empty, or otherwise invalid.
42 | func ParseKey(key string) (namespace string, nonce uint64, evmAddr string, digest string, err error) {
43 | parts := strings.Split(key, "/")
44 | if len(parts) != 3 {
45 | return "", 0, "", "", ErrInvalidConfirmKey
46 | }
47 | namespace = parts[1]
48 | if namespace == "" {
49 | return "", 0, "", "", ErrEmptyNamespace
50 | }
51 | values := strings.Split(parts[2], ":")
52 | if len(values) != 3 {
53 | return "", 0, "", "", ErrInvalidConfirmKey
54 | }
55 | nonce, err = strconv.ParseUint(values[0], 16, 64)
56 | if err != nil {
57 | return "", 0, "", "", fmt.Errorf("failed to parse nonce: %s", err.Error())
58 | }
59 | evmAddr = values[1]
60 | if evmAddr == "" {
61 | return "", 0, "", "", ErrEmptyEVMAddr
62 | }
63 | digest = values[2]
64 | if digest == "" {
65 | return "", 0, "", "", ErrEmptyDigest
66 | }
67 | return
68 | }
69 |
--------------------------------------------------------------------------------
/p2p/keys_test.go:
--------------------------------------------------------------------------------
1 | package p2p_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/celestiaorg/orchestrator-relayer/p2p"
7 | "github.com/stretchr/testify/assert"
8 | )
9 |
10 | func TestGetValsetConfirmKey(t *testing.T) {
11 | nonce := uint64(10)
12 | evmAddr := "0xfA906e15C9Eaf338c4110f0E21983c6b3b2d622b"
13 | signBytes := "0x1234"
14 |
15 | expectedKey := "/vc/a:0xfA906e15C9Eaf338c4110f0E21983c6b3b2d622b:0x1234"
16 | actualKey := p2p.GetValsetConfirmKey(nonce, evmAddr, signBytes)
17 |
18 | assert.Equal(t, expectedKey, actualKey)
19 | }
20 |
21 | func TestGetDataCommitmentConfirmKey(t *testing.T) {
22 | nonce := uint64(10)
23 | evmAddr := "0xfA906e15C9Eaf338c4110f0E21983c6b3b2d622b"
24 | dataRootTupleRoot := "0x1234"
25 |
26 | expectedKey := "/dcc/a:0xfA906e15C9Eaf338c4110f0E21983c6b3b2d622b:0x1234"
27 | actualKey := p2p.GetDataCommitmentConfirmKey(nonce, evmAddr, dataRootTupleRoot)
28 |
29 | assert.Equal(t, expectedKey, actualKey)
30 | }
31 |
32 | func TestParseKey(t *testing.T) {
33 | tests := []struct {
34 | name string
35 | key string
36 | expectedNs string
37 | expectedNonce uint64
38 | expectedEVMAddr string
39 | expectedDigest string
40 | wantErr bool
41 | }{
42 | {
43 | name: "valid valset confirm key",
44 | key: "/vc/b:0xfA906e15C9Eaf338c4110f0E21983c6b3b2d622b:0x1234",
45 | expectedNs: p2p.ValsetConfirmNamespace,
46 | expectedNonce: 11,
47 | expectedEVMAddr: "0xfA906e15C9Eaf338c4110f0E21983c6b3b2d622b",
48 | expectedDigest: "0x1234",
49 | wantErr: false,
50 | },
51 | {
52 | name: "valid data commitment confirm key",
53 | key: "/dcc/a:0xfA906e15C9Eaf338c4110f0E21983c6b3b2d622b:0x1234",
54 | expectedNs: p2p.DataCommitmentConfirmNamespace,
55 | expectedNonce: 10,
56 | expectedEVMAddr: "0xfA906e15C9Eaf338c4110f0E21983c6b3b2d622b",
57 | expectedDigest: "0x1234",
58 | wantErr: false,
59 | },
60 | {
61 | name: "missing namespace",
62 | key: "/10:0xfA906e15C9Eaf338c4110f0E21983c6b3b2d622b",
63 | wantErr: true,
64 | },
65 | {
66 | name: "empty namespace",
67 | key: "//10:0xfA906e15C9Eaf338c4110f0E21983c6b3b2d622b",
68 | wantErr: true,
69 | },
70 | {
71 | name: "missing nonce",
72 | key: "/inv/0xfA906e15C9Eaf338c4110f0E21983c6b3b2d622b",
73 | wantErr: true,
74 | },
75 | {
76 | name: "empty nonce",
77 | key: "/inv/:0xfA906e15C9Eaf338c4110f0E21983c6b3b2d622b",
78 | wantErr: true,
79 | },
80 | {
81 | name: "invalid nonce",
82 | key: "/inv/abjj:0xfA906e15C9Eaf338c4110f0E21983c6b3b2d622b",
83 | wantErr: true,
84 | },
85 | {
86 | name: "missing evm address",
87 | key: "/inv/123",
88 | wantErr: true,
89 | },
90 | {
91 | name: "empty evm address",
92 | key: "/inv/123:",
93 | wantErr: true,
94 | },
95 | {
96 | name: "missing digest",
97 | key: "/inv/123:0xfA906e15C9Eaf338c4110f0E21983c6b3b2d622b",
98 | wantErr: true,
99 | },
100 | {
101 | name: "empty digest",
102 | key: "/inv/123:0xfA906e15C9Eaf338c4110f0E21983c6b3b2d622b:",
103 | wantErr: true,
104 | },
105 | {
106 | name: "more /",
107 | key: "/inv/123/123",
108 | wantErr: true,
109 | },
110 | {
111 | name: "more :",
112 | key: "/inv/123:123:123:123",
113 | wantErr: true,
114 | },
115 | {
116 | name: "empty key",
117 | key: "",
118 | wantErr: true,
119 | },
120 | }
121 | for _, tt := range tests {
122 | t.Run(tt.name, func(t *testing.T) {
123 | namespace, nonce, evmAddr, digest, err := p2p.ParseKey(tt.key)
124 | if tt.wantErr {
125 | assert.Error(t, err)
126 | } else {
127 | assert.NoError(t, err)
128 | assert.Equal(t, tt.expectedNs, namespace)
129 | assert.Equal(t, tt.expectedNonce, nonce)
130 | assert.Equal(t, tt.expectedEVMAddr, evmAddr)
131 | assert.Equal(t, tt.expectedDigest, digest)
132 | }
133 | })
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/relayer/errors.go:
--------------------------------------------------------------------------------
1 | package relayer
2 |
3 | import (
4 | "errors"
5 | )
6 |
7 | var (
8 | ErrAttestationNotValsetRequest = errors.New("attestation is not a valset request")
9 | ErrAttestationNotDataCommitmentRequest = errors.New("attestation is not a data commitment request")
10 | ErrAttestationNotFound = errors.New("attestation not found")
11 | ErrValidatorSetMismatch = errors.New("p2p validator set is different from the trusted contract one")
12 | ErrTransactionStillPending = errors.New("evm transaction still pending")
13 | )
14 |
--------------------------------------------------------------------------------
/relayer/historic_relayer_test.go:
--------------------------------------------------------------------------------
1 | package relayer_test
2 |
3 | import (
4 | "context"
5 | "math/big"
6 | "time"
7 |
8 | blobstreamtypes "github.com/celestiaorg/orchestrator-relayer/types"
9 |
10 | "github.com/celestiaorg/celestia-app/x/qgb/types"
11 | "github.com/stretchr/testify/require"
12 | )
13 |
14 | func (s *HistoricalRelayerTestSuite) TestProcessHistoricAttestation() {
15 | t := s.T()
16 | _, err := s.Node.CelestiaNetwork.WaitForHeightWithTimeout(400, 30*time.Second)
17 | require.NoError(t, err)
18 |
19 | ctx := context.Background()
20 | valset, err := s.Orchestrator.AppQuerier.QueryLatestValset(ctx)
21 | require.NoError(t, err)
22 |
23 | // wait for the valset to be pruned to test if the relayer is able to
24 | // relay using a pruned valset.
25 | for {
26 | _, err = s.Orchestrator.AppQuerier.QueryAttestationByNonce(ctx, valset.Nonce)
27 | if err != nil {
28 | break
29 | }
30 | }
31 |
32 | // sign a test data commitment so that the relayer can relay it
33 | att := types.NewDataCommitment(valset.Nonce+1, 10, 100, time.Now())
34 | commitment, err := s.Orchestrator.TmQuerier.QueryCommitment(ctx, att.BeginBlock, att.EndBlock)
35 | require.NoError(t, err)
36 | dataRootTupleRoot := blobstreamtypes.DataCommitmentTupleRootSignBytes(big.NewInt(int64(att.Nonce)), commitment)
37 | err = s.Orchestrator.ProcessDataCommitmentEvent(ctx, *att, dataRootTupleRoot)
38 | require.NoError(t, err)
39 |
40 | // process the test data commitment that needs the pruned valset to be relayed.
41 | _, err = s.Relayer.ProcessAttestation(ctx, s.Node.EVMChain.Auth, att)
42 | require.NoError(t, err)
43 | }
44 |
--------------------------------------------------------------------------------
/relayer/historic_suite_test.go:
--------------------------------------------------------------------------------
1 | package relayer_test
2 |
3 | import (
4 | "context"
5 | "testing"
6 | "time"
7 |
8 | "github.com/celestiaorg/celestia-app/app"
9 | "github.com/celestiaorg/celestia-app/app/encoding"
10 | "github.com/celestiaorg/celestia-app/test/util/testnode"
11 | "github.com/celestiaorg/celestia-app/x/qgb/types"
12 | "github.com/celestiaorg/orchestrator-relayer/rpc"
13 |
14 | "github.com/celestiaorg/orchestrator-relayer/orchestrator"
15 |
16 | "github.com/celestiaorg/orchestrator-relayer/relayer"
17 | blobstreamtesting "github.com/celestiaorg/orchestrator-relayer/testing"
18 | "github.com/stretchr/testify/require"
19 | "github.com/stretchr/testify/suite"
20 | )
21 |
22 | type HistoricalRelayerTestSuite struct {
23 | suite.Suite
24 | Node *blobstreamtesting.TestNode
25 | Orchestrator *orchestrator.Orchestrator
26 | Relayer *relayer.Relayer
27 | }
28 |
29 | func (s *HistoricalRelayerTestSuite) SetupSuite() {
30 | t := s.T()
31 | if testing.Short() {
32 | t.Skip("skipping relayer tests in short mode.")
33 | }
34 | ctx := context.Background()
35 | s.Node = blobstreamtesting.NewTestNode(
36 | ctx,
37 | t,
38 | blobstreamtesting.CelestiaNetworkParams{
39 | GenesisOpts: []testnode.GenesisOption{blobstreamtesting.SetDataCommitmentWindowParams(
40 | encoding.MakeConfig(app.ModuleEncodingRegisters...).Codec,
41 | types.Params{DataCommitmentWindow: 101},
42 | )},
43 | TimeIotaMs: 3048000, // so that old attestations are deleted as soon as a new one appears
44 | Pruning: "nothing", // make the node an archive one
45 | TimeoutCommit: 20 * time.Millisecond,
46 | },
47 | )
48 | _, err := s.Node.CelestiaNetwork.WaitForHeight(2)
49 | require.NoError(t, err)
50 | s.Orchestrator = blobstreamtesting.NewOrchestrator(t, s.Node)
51 | s.Relayer = blobstreamtesting.NewRelayer(t, s.Node)
52 | go s.Node.EVMChain.PeriodicCommit(ctx, time.Millisecond)
53 | initVs, err := s.Relayer.AppQuerier.QueryLatestValset(s.Node.Context)
54 | require.NoError(t, err)
55 | _, _, _, err = s.Relayer.EVMClient.DeployBlobstreamContract(s.Node.EVMChain.Auth, s.Node.EVMChain.Backend, *initVs, initVs.Nonce, true)
56 | require.NoError(t, err)
57 | rpc.BlocksIn20DaysPeriod = 50
58 | }
59 |
60 | func (s *HistoricalRelayerTestSuite) TearDownSuite() {
61 | s.Node.Close()
62 | }
63 |
64 | func TestHistoricRelayer(t *testing.T) {
65 | suite.Run(t, new(HistoricalRelayerTestSuite))
66 | }
67 |
--------------------------------------------------------------------------------
/relayer/suite_test.go:
--------------------------------------------------------------------------------
1 | package relayer_test
2 |
3 | import (
4 | "context"
5 | "testing"
6 | "time"
7 |
8 | "github.com/celestiaorg/orchestrator-relayer/orchestrator"
9 |
10 | "github.com/celestiaorg/orchestrator-relayer/relayer"
11 | blobstreamtesting "github.com/celestiaorg/orchestrator-relayer/testing"
12 | "github.com/stretchr/testify/require"
13 | "github.com/stretchr/testify/suite"
14 | )
15 |
16 | type RelayerTestSuite struct {
17 | suite.Suite
18 | Node *blobstreamtesting.TestNode
19 | Orchestrator *orchestrator.Orchestrator
20 | Relayer *relayer.Relayer
21 | }
22 |
23 | func (s *RelayerTestSuite) SetupSuite() {
24 | t := s.T()
25 | if testing.Short() {
26 | t.Skip("skipping relayer tests in short mode.")
27 | }
28 | ctx := context.Background()
29 | s.Node = blobstreamtesting.NewTestNode(ctx, t, blobstreamtesting.DefaultCelestiaNetworkParams())
30 | _, err := s.Node.CelestiaNetwork.WaitForHeight(2)
31 | require.NoError(t, err)
32 | s.Orchestrator = blobstreamtesting.NewOrchestrator(t, s.Node)
33 | s.Relayer = blobstreamtesting.NewRelayer(t, s.Node)
34 | go s.Node.EVMChain.PeriodicCommit(ctx, time.Millisecond)
35 | initVs, err := s.Relayer.AppQuerier.QueryLatestValset(s.Node.Context)
36 | require.NoError(t, err)
37 | _, _, _, err = s.Relayer.EVMClient.DeployBlobstreamContract(s.Node.EVMChain.Auth, s.Node.EVMChain.Backend, *initVs, initVs.Nonce, true)
38 | require.NoError(t, err)
39 | }
40 |
41 | func (s *RelayerTestSuite) TearDownSuite() {
42 | s.Node.Close()
43 | }
44 |
45 | func TestRelayer(t *testing.T) {
46 | suite.Run(t, new(RelayerTestSuite))
47 | }
48 |
--------------------------------------------------------------------------------
/rpc/app_historic_querier_test.go:
--------------------------------------------------------------------------------
1 | package rpc_test
2 |
3 | import (
4 | "context"
5 | )
6 |
7 | func (s *HistoricQuerierTestSuite) TestQueryHistoricAttestationByNonce() {
8 | appQuerier := s.setupAppQuerier()
9 |
10 | // this one should fail because the attestation is deleted from the state
11 | _, err := appQuerier.QueryAttestationByNonce(context.Background(), 1)
12 | s.Error(err)
13 |
14 | att, err := appQuerier.QueryHistoricalAttestationByNonce(context.Background(), 1, 10)
15 | s.NoError(err)
16 | s.NotNil(att)
17 | s.Equal(uint64(1), att.GetNonce())
18 | }
19 |
20 | func (s *HistoricQuerierTestSuite) TestQueryRecursiveHistoricAttestationByNonce() {
21 | appQuerier := s.setupAppQuerier()
22 |
23 | // this one should fail because the attestation is deleted from the state
24 | _, err := appQuerier.QueryAttestationByNonce(context.Background(), 1)
25 | s.Error(err)
26 |
27 | height, err := s.Network.LatestHeight()
28 | s.Require().NoError(err)
29 | att, err := appQuerier.QueryRecursiveHistoricalAttestationByNonce(context.Background(), 1, uint64(height))
30 | s.Require().NoError(err)
31 | s.NotNil(att)
32 | s.Equal(uint64(1), att.GetNonce())
33 | }
34 |
35 | func (s *HistoricQuerierTestSuite) TestQueryHistoricalLatestAttestationNonce() {
36 | appQuerier := s.setupAppQuerier()
37 |
38 | nonce, err := appQuerier.QueryHistoricalLatestAttestationNonce(context.Background(), 2)
39 | s.Require().NoError(err)
40 | s.Equal(uint64(1), nonce)
41 | }
42 |
43 | func (s *HistoricQuerierTestSuite) TestQueryHistoricalValsetByNonce() {
44 | appQuerier := s.setupAppQuerier()
45 |
46 | // this one should fail because the attestation is deleted from the state
47 | _, err := appQuerier.QueryValsetByNonce(context.Background(), 1)
48 | s.Error(err)
49 |
50 | att, err := appQuerier.QueryHistoricalValsetByNonce(context.Background(), 1, 10)
51 | s.Require().NoError(err)
52 | s.NotNil(att)
53 | s.Equal(uint64(1), att.GetNonce())
54 | }
55 |
56 | func (s *HistoricQuerierTestSuite) TestQueryHistoricalLastValsetBeforeNonce() {
57 | appQuerier := s.setupAppQuerier()
58 |
59 | // this one should fail because the attestation is deleted from the state
60 | _, err := appQuerier.QueryLastValsetBeforeNonce(context.Background(), 2)
61 | s.Error(err)
62 |
63 | att, err := appQuerier.QueryHistoricalLastValsetBeforeNonce(context.Background(), 2, 102)
64 | s.Require().NoError(err)
65 | s.NotNil(att)
66 | s.Equal(uint64(1), att.GetNonce())
67 | }
68 |
69 | func (s *HistoricQuerierTestSuite) TestQueryRecursiveHistoricalLastValsetBeforeNonce() {
70 | appQuerier := s.setupAppQuerier()
71 |
72 | // this one should fail because the attestation is deleted from the state
73 | _, err := appQuerier.QueryLastValsetBeforeNonce(context.Background(), 2)
74 | s.Error(err)
75 |
76 | att, err := appQuerier.QueryRecursiveHistoricalLastValsetBeforeNonce(context.Background(), 2, 201)
77 | s.Require().NoError(err)
78 | s.NotNil(att)
79 | s.Equal(uint64(1), att.GetNonce())
80 | }
81 |
--------------------------------------------------------------------------------
/rpc/app_querier_test.go:
--------------------------------------------------------------------------------
1 | package rpc_test
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/stretchr/testify/require"
7 |
8 | "github.com/celestiaorg/orchestrator-relayer/rpc"
9 | )
10 |
11 | func (s *QuerierTestSuite) TestQueryAttestationByNonce() {
12 | appQuerier := rpc.NewAppQuerier(
13 | s.Logger,
14 | s.Network.GRPCAddr,
15 | s.EncConf,
16 | )
17 | require.NoError(s.T(), appQuerier.Start(true))
18 | defer appQuerier.Stop() //nolint:errcheck
19 |
20 | att, err := appQuerier.QueryAttestationByNonce(context.Background(), 1)
21 | s.NoError(err)
22 | s.Equal(uint64(1), att.GetNonce())
23 | }
24 |
25 | func (s *QuerierTestSuite) TestQueryLatestAttestationNonce() {
26 | appQuerier := rpc.NewAppQuerier(
27 | s.Logger,
28 | s.Network.GRPCAddr,
29 | s.EncConf,
30 | )
31 | require.NoError(s.T(), appQuerier.Start(true))
32 | defer appQuerier.Stop() //nolint:errcheck
33 |
34 | nonce, err := appQuerier.QueryLatestAttestationNonce(context.Background())
35 | s.NoError(err)
36 | s.Greater(nonce, uint64(1))
37 | }
38 |
39 | func (s *QuerierTestSuite) TestQueryDataCommitmentByNonce() {
40 | appQuerier := rpc.NewAppQuerier(
41 | s.Logger,
42 | s.Network.GRPCAddr,
43 | s.EncConf,
44 | )
45 | require.NoError(s.T(), appQuerier.Start(true))
46 | defer appQuerier.Stop() //nolint:errcheck
47 |
48 | dc, err := appQuerier.QueryDataCommitmentByNonce(context.Background(), 2)
49 | s.NoError(err)
50 | s.Equal(uint64(2), dc.Nonce)
51 | }
52 |
53 | func (s *QuerierTestSuite) TestQueryDataCommitmentForHeight() {
54 | appQuerier := rpc.NewAppQuerier(
55 | s.Logger,
56 | s.Network.GRPCAddr,
57 | s.EncConf,
58 | )
59 | require.NoError(s.T(), appQuerier.Start(true))
60 | defer appQuerier.Stop() //nolint:errcheck
61 |
62 | dc, err := appQuerier.QueryDataCommitmentForHeight(context.Background(), 10)
63 | s.NoError(err)
64 | s.Equal(uint64(2), dc.Nonce)
65 | }
66 |
67 | func (s *QuerierTestSuite) TestQueryValsetByNonce() {
68 | appQuerier := rpc.NewAppQuerier(
69 | s.Logger,
70 | s.Network.GRPCAddr,
71 | s.EncConf,
72 | )
73 | require.NoError(s.T(), appQuerier.Start(true))
74 | defer appQuerier.Stop() //nolint:errcheck
75 |
76 | vs, err := appQuerier.QueryValsetByNonce(context.Background(), 1)
77 | s.NoError(err)
78 | s.Equal(uint64(1), vs.Nonce)
79 | }
80 |
81 | func (s *QuerierTestSuite) TestQueryLatestValset() {
82 | appQuerier := rpc.NewAppQuerier(
83 | s.Logger,
84 | s.Network.GRPCAddr,
85 | s.EncConf,
86 | )
87 | require.NoError(s.T(), appQuerier.Start(true))
88 | defer appQuerier.Stop() //nolint:errcheck
89 |
90 | vs, err := appQuerier.QueryLatestValset(context.Background())
91 | s.NoError(err)
92 | s.GreaterOrEqual(vs.Nonce, uint64(1))
93 | }
94 |
95 | func (s *QuerierTestSuite) TestQueryLastValsetBeforeNonce() {
96 | appQuerier := rpc.NewAppQuerier(
97 | s.Logger,
98 | s.Network.GRPCAddr,
99 | s.EncConf,
100 | )
101 | require.NoError(s.T(), appQuerier.Start(true))
102 | defer appQuerier.Stop() //nolint:errcheck
103 |
104 | vs, err := appQuerier.QueryLastValsetBeforeNonce(context.Background(), 2)
105 | s.NoError(err)
106 | s.GreaterOrEqual(vs.Nonce, uint64(1))
107 | }
108 |
109 | func (s *QuerierTestSuite) TestQueryLastUnbondingHeight() {
110 | appQuerier := rpc.NewAppQuerier(
111 | s.Logger,
112 | s.Network.GRPCAddr,
113 | s.EncConf,
114 | )
115 | require.NoError(s.T(), appQuerier.Start(true))
116 | defer appQuerier.Stop() //nolint:errcheck
117 |
118 | unbondingHeight, err := appQuerier.QueryLastUnbondingHeight(context.Background())
119 | s.NoError(err)
120 | s.Equal(int64(0), unbondingHeight)
121 | }
122 |
123 | func (s *QuerierTestSuite) TestQueryEarliestAttestationNonce() {
124 | appQuerier := rpc.NewAppQuerier(
125 | s.Logger,
126 | s.Network.GRPCAddr,
127 | s.EncConf,
128 | )
129 | require.NoError(s.T(), appQuerier.Start(true))
130 | defer appQuerier.Stop() //nolint:errcheck
131 |
132 | earliestNonce, err := appQuerier.QueryEarliestAttestationNonce(context.Background())
133 | s.NoError(err)
134 | s.Equal(int64(1), earliestNonce)
135 | }
136 |
--------------------------------------------------------------------------------
/rpc/errors.go:
--------------------------------------------------------------------------------
1 | package rpc
2 |
3 | import "errors"
4 |
5 | var (
6 | ErrCouldntReachSpecifiedHeight = errors.New("couldn't reach specified height")
7 | ErrNotFound = errors.New("not found")
8 | )
9 |
--------------------------------------------------------------------------------
/rpc/historic_suite_test.go:
--------------------------------------------------------------------------------
1 | package rpc_test
2 |
3 | import (
4 | "context"
5 | "testing"
6 | "time"
7 |
8 | "github.com/celestiaorg/celestia-app/test/util/testnode"
9 | "github.com/celestiaorg/celestia-app/x/qgb/types"
10 | "github.com/celestiaorg/orchestrator-relayer/rpc"
11 |
12 | "github.com/celestiaorg/celestia-app/app"
13 | "github.com/celestiaorg/celestia-app/app/encoding"
14 | tmlog "github.com/tendermint/tendermint/libs/log"
15 |
16 | "github.com/stretchr/testify/require"
17 |
18 | blobstreamtesting "github.com/celestiaorg/orchestrator-relayer/testing"
19 | "github.com/stretchr/testify/suite"
20 | )
21 |
22 | type HistoricQuerierTestSuite struct {
23 | suite.Suite
24 | Network *blobstreamtesting.CelestiaNetwork
25 | EncConf encoding.Config
26 | Logger tmlog.Logger
27 | }
28 |
29 | func (s *HistoricQuerierTestSuite) SetupSuite() {
30 | t := s.T()
31 | ctx := context.Background()
32 | s.EncConf = encoding.MakeConfig(app.ModuleEncodingRegisters...)
33 | s.Network = blobstreamtesting.NewCelestiaNetwork(
34 | ctx,
35 | t,
36 | blobstreamtesting.CelestiaNetworkParams{
37 | GenesisOpts: []testnode.GenesisOption{blobstreamtesting.SetDataCommitmentWindowParams(s.EncConf.Codec, types.Params{DataCommitmentWindow: 101})},
38 | TimeIotaMs: 6048000, // so that old attestations are deleted as soon as a new one appears
39 | Pruning: "nothing", // make the node an archive one
40 | TimeoutCommit: 20 * time.Millisecond,
41 | },
42 | )
43 | _, err := s.Network.WaitForHeightWithTimeout(401, 30*time.Second)
44 | require.NoError(t, err)
45 | s.Logger = tmlog.NewNopLogger()
46 | rpc.BlocksIn20DaysPeriod = 100
47 | }
48 |
49 | func TestHistoricQueriers(t *testing.T) {
50 | suite.Run(t, new(HistoricQuerierTestSuite))
51 | }
52 |
53 | func (s *HistoricQuerierTestSuite) setupAppQuerier() *rpc.AppQuerier {
54 | appQuerier := rpc.NewAppQuerier(
55 | s.Logger,
56 | s.Network.GRPCAddr,
57 | s.EncConf,
58 | )
59 | require.NoError(s.T(), appQuerier.Start(true))
60 | s.T().Cleanup(func() {
61 | appQuerier.Stop() //nolint:errcheck
62 | })
63 | return appQuerier
64 | }
65 |
--------------------------------------------------------------------------------
/rpc/suite_test.go:
--------------------------------------------------------------------------------
1 | package rpc_test
2 |
3 | import (
4 | "context"
5 | "testing"
6 | "time"
7 |
8 | "github.com/celestiaorg/celestia-app/app"
9 | "github.com/celestiaorg/celestia-app/app/encoding"
10 | tmlog "github.com/tendermint/tendermint/libs/log"
11 |
12 | "github.com/stretchr/testify/require"
13 |
14 | blobstreamtesting "github.com/celestiaorg/orchestrator-relayer/testing"
15 | "github.com/stretchr/testify/suite"
16 | )
17 |
18 | type QuerierTestSuite struct {
19 | suite.Suite
20 | Network *blobstreamtesting.CelestiaNetwork
21 | EncConf encoding.Config
22 | Logger tmlog.Logger
23 | }
24 |
25 | func (s *QuerierTestSuite) SetupSuite() {
26 | t := s.T()
27 | ctx := context.Background()
28 | s.Network = blobstreamtesting.NewCelestiaNetwork(ctx, t, blobstreamtesting.DefaultCelestiaNetworkParams())
29 | _, err := s.Network.WaitForHeightWithTimeout(400, 30*time.Second)
30 | s.EncConf = encoding.MakeConfig(app.ModuleEncodingRegisters...)
31 | s.Logger = tmlog.NewNopLogger()
32 | require.NoError(t, err)
33 | }
34 |
35 | func TestQueriers(t *testing.T) {
36 | suite.Run(t, new(QuerierTestSuite))
37 | }
38 |
--------------------------------------------------------------------------------
/rpc/tm_querier.go:
--------------------------------------------------------------------------------
1 | package rpc
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "time"
7 |
8 | "github.com/tendermint/tendermint/libs/bytes"
9 | tmlog "github.com/tendermint/tendermint/libs/log"
10 | "github.com/tendermint/tendermint/rpc/client"
11 | "github.com/tendermint/tendermint/rpc/client/http"
12 | coretypes "github.com/tendermint/tendermint/rpc/core/types"
13 | "github.com/tendermint/tendermint/types"
14 | )
15 |
16 | // TmQuerier queries tendermint for commitments and events.
17 | type TmQuerier struct {
18 | logger tmlog.Logger
19 | tendermintRPC string
20 | clientConn client.Client
21 | }
22 |
23 | func NewTmQuerier(
24 | tendermintRPC string,
25 | logger tmlog.Logger,
26 | ) *TmQuerier {
27 | return &TmQuerier{
28 | logger: logger,
29 | tendermintRPC: tendermintRPC,
30 | }
31 | }
32 |
33 | func (tq *TmQuerier) Start() error {
34 | // creating an RPC connection to tendermint
35 | trpc, err := http.New(tq.tendermintRPC, "/websocket")
36 | if err != nil {
37 | return err
38 | }
39 | err = trpc.Start()
40 | if err != nil {
41 | return err
42 | }
43 | tq.clientConn = trpc
44 | return nil
45 | }
46 |
47 | func (tq *TmQuerier) Stop() error {
48 | err := tq.clientConn.Stop()
49 | if err != nil {
50 | return err
51 | }
52 | return nil
53 | }
54 |
55 | func (tq *TmQuerier) WithClientConn(trpc client.Client) {
56 | tq.clientConn = trpc
57 | }
58 |
59 | func (tq *TmQuerier) QueryCommitment(ctx context.Context, beginBlock uint64, endBlock uint64) (bytes.HexBytes, error) {
60 | dcResp, err := tq.clientConn.DataCommitment(ctx, beginBlock, endBlock)
61 | if err != nil {
62 | return nil, err
63 | }
64 | return dcResp.DataCommitment, nil
65 | }
66 |
67 | func (tq *TmQuerier) QueryHeight(ctx context.Context) (int64, error) {
68 | status, err := tq.clientConn.Status(ctx)
69 | if err != nil {
70 | return 0, err
71 | }
72 | return status.SyncInfo.LatestBlockHeight, nil
73 | }
74 |
75 | func (tq *TmQuerier) WaitForHeight(ctx context.Context, height int64) error {
76 | currentHeight, err := tq.QueryHeight(ctx)
77 | if err != nil {
78 | return err
79 | }
80 | if currentHeight >= height {
81 | return nil
82 | }
83 |
84 | query := fmt.Sprintf("%s='%s'", types.EventTypeKey, types.EventNewBlock)
85 | results, err := tq.SubscribeEvents(ctx, "sub-height", query)
86 | if err != nil {
87 | return err
88 | }
89 | defer func() {
90 | err := tq.UnsubscribeEvents(ctx, "sub-height", query)
91 | if err != nil {
92 | tq.logger.Error(err.Error())
93 | }
94 | }()
95 |
96 | timeout := time.NewTimer(time.Minute)
97 | for {
98 | select {
99 | case <-ctx.Done():
100 | return ctx.Err()
101 | case <-timeout.C:
102 | return ErrCouldntReachSpecifiedHeight
103 | case <-results:
104 | currentHeight, err := tq.QueryHeight(ctx)
105 | if err != nil {
106 | return err
107 | }
108 | if currentHeight >= height {
109 | return nil
110 | }
111 | }
112 | }
113 | }
114 |
115 | func (tq *TmQuerier) SubscribeEvents(ctx context.Context, subscriptionName string, query string) (<-chan coretypes.ResultEvent, error) {
116 | // This doesn't seem to complain when the node is down
117 | results, err := tq.clientConn.Subscribe(
118 | ctx,
119 | subscriptionName,
120 | query,
121 | )
122 | if err != nil {
123 | return nil, err
124 | }
125 | return results, err
126 | }
127 |
128 | func (tq *TmQuerier) UnsubscribeEvents(ctx context.Context, subscriptionName string, query string) error {
129 | return tq.clientConn.Unsubscribe(
130 | ctx,
131 | subscriptionName,
132 | query,
133 | )
134 | }
135 |
136 | func (tq *TmQuerier) IsRunning(ctx context.Context) bool {
137 | _, err := tq.clientConn.Status(ctx)
138 | return err == nil
139 | }
140 |
141 | func (tq *TmQuerier) Reconnect() error {
142 | _ = tq.clientConn.Stop()
143 | newConnection, err := http.New(tq.tendermintRPC, "/websocket")
144 | if err != nil {
145 | return err
146 | }
147 | err = newConnection.Start()
148 | if err != nil {
149 | return err
150 | }
151 | tq.clientConn = newConnection
152 | return nil
153 | }
154 |
--------------------------------------------------------------------------------
/rpc/tm_querier_test.go:
--------------------------------------------------------------------------------
1 | package rpc_test
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | celestiatypes "github.com/celestiaorg/celestia-app/x/qgb/types"
8 | sdk "github.com/cosmos/cosmos-sdk/types"
9 |
10 | "github.com/celestiaorg/orchestrator-relayer/rpc"
11 | "github.com/stretchr/testify/assert"
12 | "github.com/stretchr/testify/require"
13 | tmlog "github.com/tendermint/tendermint/libs/log"
14 | )
15 |
16 | func (s *QuerierTestSuite) TestQueryCommitment() {
17 | t := s.T()
18 | _, err := s.Network.WaitForHeight(101)
19 | require.NoError(t, err)
20 |
21 | tmQuerier := rpc.NewTmQuerier(
22 | s.Network.RPCAddr,
23 | tmlog.NewNopLogger(),
24 | )
25 | tmQuerier.WithClientConn(s.Network.Client)
26 |
27 | expectedCommitment, err := s.Network.Client.DataCommitment(context.Background(), 1, 100)
28 | require.NoError(t, err)
29 | actualCommitment, err := tmQuerier.QueryCommitment(context.Background(), 1, 100)
30 | require.NoError(t, err)
31 |
32 | assert.Equal(t, expectedCommitment.DataCommitment, actualCommitment)
33 | }
34 |
35 | func (s *QuerierTestSuite) TestQueryHeight() {
36 | t := s.T()
37 | _, err := s.Network.WaitForHeight(101)
38 | require.NoError(t, err)
39 |
40 | tmQuerier := rpc.NewTmQuerier(
41 | s.Network.RPCAddr,
42 | tmlog.NewNopLogger(),
43 | )
44 | tmQuerier.WithClientConn(s.Network.Client)
45 |
46 | height, err := tmQuerier.QueryHeight(context.Background())
47 | require.NoError(t, err)
48 |
49 | assert.Greater(t, height, int64(101))
50 | }
51 |
52 | func (s *QuerierTestSuite) TestWaitForHeight() {
53 | t := s.T()
54 | _, err := s.Network.WaitForHeight(10)
55 | require.NoError(t, err)
56 |
57 | tmQuerier := rpc.NewTmQuerier(
58 | s.Network.RPCAddr,
59 | tmlog.NewNopLogger(),
60 | )
61 | tmQuerier.WithClientConn(s.Network.Client)
62 |
63 | height, err := tmQuerier.QueryHeight(context.Background())
64 | require.NoError(t, err)
65 |
66 | err = tmQuerier.WaitForHeight(context.Background(), height+20)
67 | require.NoError(t, err)
68 |
69 | currentHeight, err := tmQuerier.QueryHeight(context.Background())
70 | require.NoError(t, err)
71 | assert.GreaterOrEqual(t, currentHeight, height+20)
72 | }
73 |
74 | func (s *QuerierTestSuite) TestSubscribeEvents() {
75 | t := s.T()
76 | _, err := s.Network.WaitForHeight(101)
77 | require.NoError(t, err)
78 |
79 | tmQuerier := rpc.NewTmQuerier(
80 | s.Network.RPCAddr,
81 | tmlog.NewNopLogger(),
82 | )
83 | tmQuerier.WithClientConn(s.Network.Client)
84 |
85 | eventsChan, err := tmQuerier.SubscribeEvents(
86 | context.Background(),
87 | "test-subscription",
88 | fmt.Sprintf("%s.%s='%s'", celestiatypes.EventTypeAttestationRequest, sdk.AttributeKeyModule, celestiatypes.ModuleName),
89 | )
90 | require.NoError(t, err)
91 | event := <-eventsChan
92 | assert.NotNil(t, event.Events["AttestationRequest.nonce"])
93 | }
94 |
--------------------------------------------------------------------------------
/scripts/test_cover.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -e
3 |
4 | PKGS=$(go list ./...)
5 |
6 | set -e
7 | echo "mode: atomic" > coverage.txt
8 | for pkg in ${PKGS[@]}; do
9 | go test -v -timeout 30m -race -test.short -coverprofile=profile.out -covermode=atomic "$pkg"
10 | if [ -f profile.out ]; then
11 | tail -n +2 profile.out >> coverage.txt;
12 | rm profile.out
13 | fi
14 | done
15 |
--------------------------------------------------------------------------------
/store/badger.go:
--------------------------------------------------------------------------------
1 | package store
2 |
3 | import (
4 | badger2 "github.com/dgraph-io/badger/v2"
5 | badger "github.com/ipfs/go-ds-badger2"
6 | )
7 |
8 | // DefaultBadgerOptions creates the default options for badger.
9 | // For our purposes, we don't want the store to perform any garbage collection or
10 | // expire newly added keys after a certain period, because:
11 | // 1. the data in the store will be light.
12 | // 2. we want to keep the data, i.e. confirms, for the longest time possible to be able
13 | // to retrieve them if needed.
14 | func DefaultBadgerOptions(path string) *badger.Options {
15 | return &badger.Options{
16 | GcDiscardRatio: 0,
17 | GcInterval: 0,
18 | GcSleep: 0,
19 | TTL: 0,
20 | Options: badger2.DefaultOptions(path),
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/store/errors.go:
--------------------------------------------------------------------------------
1 | package store
2 |
3 | import "errors"
4 |
5 | var (
6 | // ErrOpened is thrown on attempt to open already open/in-use Store.
7 | ErrOpened = errors.New("store is in use")
8 | // ErrNotInited is thrown on attempt to open Store without initialization.
9 | ErrNotInited = errors.New("store is not initialized")
10 | )
11 |
--------------------------------------------------------------------------------
/store/fslock/lock_unix.go:
--------------------------------------------------------------------------------
1 | package fslock
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "strconv"
7 | "syscall"
8 | )
9 |
10 | func (l *Locker) lock() (err error) {
11 | l.file, err = os.OpenFile(l.path, os.O_CREATE|os.O_RDWR, 0o666)
12 | if err != nil {
13 | return fmt.Errorf("fslock: error opening file: %w", err)
14 | }
15 |
16 | _, err = l.file.WriteString(strconv.Itoa(os.Getpid()))
17 | if err != nil {
18 | return fmt.Errorf("fslock: error writing process id: %w", err)
19 | }
20 |
21 | err = syscall.Flock(int(l.file.Fd()), syscall.LOCK_EX|syscall.LOCK_NB)
22 | if err != nil && err.Error() == "resource temporarily unavailable" {
23 | return ErrLocked // we have to check here for a string in err, as there is no error types defined for this case.
24 | }
25 | if err != nil {
26 | return fmt.Errorf("fslock: flocking error: %w", err)
27 | }
28 |
29 | return
30 | }
31 |
32 | func (l *Locker) unlock() error {
33 | err := syscall.Flock(int(l.file.Fd()), syscall.LOCK_UN|syscall.LOCK_NB)
34 | if err != nil {
35 | return fmt.Errorf("fslock: unflocking error: %w", err)
36 | }
37 |
38 | file := l.file
39 | l.file = nil
40 | err = file.Close()
41 | if err != nil {
42 | return fmt.Errorf("fslock: while closing file: %w", err)
43 | }
44 |
45 | return os.Remove(l.path)
46 | }
47 |
--------------------------------------------------------------------------------
/store/fslock/locker.go:
--------------------------------------------------------------------------------
1 | package fslock
2 |
3 | import (
4 | "errors"
5 | "os"
6 | )
7 |
8 | // ErrLocked is signaled when someone tries to lock an already locked file.
9 | var ErrLocked = errors.New("fslock: directory is locked")
10 |
11 | // Lock creates a new Locker under the given 'path'
12 | // and immediately does Lock on it.
13 | func Lock(path string) (*Locker, error) {
14 | l := New(path)
15 | err := l.Lock()
16 | if err != nil {
17 | return nil, err
18 | }
19 |
20 | return l, nil
21 | }
22 |
23 | // Locker is a simple utility meant to create lock files.
24 | // This is to prevent multiple processes from managing the same working directory by purpose or
25 | // accident. NOTE: Windows is not supported.
26 | type Locker struct {
27 | file *os.File
28 | path string
29 | }
30 |
31 | // New creates a new Locker with a File pointing to the given 'path'.
32 | func New(path string) *Locker {
33 | return &Locker{path: path}
34 | }
35 |
36 | // Lock locks the file.
37 | // Subsequent calls will error with ErrLocked on any Locker instance looking to the same path.
38 | func (l *Locker) Lock() error {
39 | return l.lock()
40 | }
41 |
42 | // Unlock frees up the lock.
43 | func (l *Locker) Unlock() error {
44 | if l == nil || l.file == nil {
45 | return nil
46 | }
47 |
48 | return l.unlock()
49 | }
50 |
--------------------------------------------------------------------------------
/store/fslock/locker_test.go:
--------------------------------------------------------------------------------
1 | package fslock
2 |
3 | import (
4 | "os"
5 | "path/filepath"
6 | "testing"
7 | )
8 |
9 | func TestLocker(t *testing.T) {
10 | path := filepath.Join(os.TempDir(), ".lock")
11 | defer os.Remove(path)
12 |
13 | locker := New(path)
14 | locker2 := New(path)
15 |
16 | err := locker.Lock()
17 | if err != nil {
18 | t.Fatal(err)
19 | }
20 |
21 | err = locker2.Lock()
22 | if err != ErrLocked {
23 | t.Fatal("No locking")
24 | }
25 |
26 | err = locker.Unlock()
27 | if err != nil {
28 | t.Fatal(err)
29 | }
30 |
31 | err = locker2.Lock()
32 | if err != nil {
33 | t.Fatal(err)
34 | }
35 |
36 | err = locker2.Unlock()
37 | if err != nil {
38 | t.Fatal(err)
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/store/init.go:
--------------------------------------------------------------------------------
1 | package store
2 |
3 | import (
4 | "errors"
5 | "os"
6 | "path/filepath"
7 |
8 | "github.com/celestiaorg/orchestrator-relayer/store/fslock"
9 | "github.com/mitchellh/go-homedir"
10 | tmlog "github.com/tendermint/tendermint/libs/log"
11 | )
12 |
13 | const (
14 | // DataPath the subdir for the data folder containing the p2p data relative to the path.
15 | DataPath = "data"
16 | // SignaturePath the subdir for the signatures folder containing all the signatures that the relayer query.
17 | SignaturePath = "signatures"
18 | // EVMKeyStorePath the subdir for the path containing the EVM keystore.
19 | EVMKeyStorePath = "keystore/evm"
20 | // P2PKeyStorePath the subdir for the path containing the p2p keystore.
21 | P2PKeyStorePath = "keystore/p2p"
22 | )
23 |
24 | // storePath clean up the store path.
25 | func storePath(path string) (string, error) {
26 | return homedir.Expand(filepath.Clean(path))
27 | }
28 |
29 | // InitOptions contains the options used to init a path or check if a path
30 | // is already initiated.
31 | type InitOptions struct {
32 | NeedDataStore bool
33 | NeedSignatureStore bool
34 | NeedEVMKeyStore bool
35 | NeedP2PKeyStore bool
36 | }
37 |
38 | // Init initializes the Blobstream file system in the directory under
39 | // 'path'.
40 | // It also creates a lock under that directory, so it can't be used
41 | // by multiple processes.
42 | func Init(log tmlog.Logger, path string, options InitOptions) error {
43 | path, err := storePath(path)
44 | if err != nil {
45 | return err
46 | }
47 | log.Info("initializing Blobstream store", "path", path)
48 |
49 | err = initRoot(path)
50 | if err != nil {
51 | return err
52 | }
53 |
54 | flock, err := fslock.Lock(lockPath(path))
55 | if err != nil {
56 | if errors.Is(err, fslock.ErrLocked) {
57 | return ErrOpened
58 | }
59 | return err
60 | }
61 |
62 | if options.NeedDataStore {
63 | err = initDir(dataPath(path))
64 | if err != nil {
65 | return err
66 | }
67 |
68 | log.Info("data dir initialized", "path", dataPath(path))
69 | }
70 |
71 | if options.NeedSignatureStore {
72 | err = initDir(signaturePath(path))
73 | if err != nil {
74 | return err
75 | }
76 |
77 | log.Info("signature dir initialized", "path", signaturePath(path))
78 | }
79 |
80 | if options.NeedP2PKeyStore {
81 | err = initDir(p2pKeyStorePath(path))
82 | if err != nil {
83 | return err
84 | }
85 |
86 | log.Info("p2p keystore dir initialized", "path", p2pKeyStorePath(path))
87 | }
88 |
89 | if options.NeedEVMKeyStore {
90 | err = initDir(evmKeyStorePath(path))
91 | if err != nil {
92 | return err
93 | }
94 |
95 | log.Info("evm keystore dir initialized", "path", evmKeyStorePath(path))
96 | }
97 |
98 | err = flock.Unlock()
99 | if err != nil {
100 | return err
101 | }
102 |
103 | log.Info("Blobstream store initialized", "path", path)
104 |
105 | return nil
106 | }
107 |
108 | // IsInit checks whether FileSystem Store was set up under given 'path'.
109 | // If the paths of the provided options don't exist, then it returns false.
110 | func IsInit(logger tmlog.Logger, path string, options InitOptions) bool {
111 | path, err := storePath(path)
112 | if err != nil {
113 | logger.Error("parsing store path", "path", path, "err", err)
114 | return false
115 | }
116 |
117 | // check if the root path exists
118 | if !Exists(path) {
119 | return false
120 | }
121 |
122 | // check if the data store exists if it's needed
123 | if options.NeedDataStore && !Exists(dataPath(path)) {
124 | logger.Info("data path not initialized", "path", path)
125 | return false
126 | }
127 |
128 | // check if the signature store exists if it's needed
129 | if options.NeedSignatureStore && !Exists(signaturePath(path)) {
130 | logger.Info("signature path not initialized", "path", path)
131 | return false
132 | }
133 |
134 | // check if the p2p key store path exists if it's needed
135 | if options.NeedP2PKeyStore && !Exists(p2pKeyStorePath(path)) {
136 | logger.Info("p2p keystore not initialized", "path", path)
137 | return false
138 | }
139 |
140 | // check if the EVM key store path exists if it's needed
141 | if options.NeedEVMKeyStore && !Exists(evmKeyStorePath(path)) {
142 | logger.Info("evm keystore not initialized", "path", path)
143 | return false
144 | }
145 |
146 | return true
147 | }
148 |
149 | const perms = 0o755
150 |
151 | // initRoot initializes(creates) directory if not created and check if it is writable
152 | func initRoot(path string) error {
153 | err := initDir(path)
154 | if err != nil {
155 | return err
156 | }
157 |
158 | // check for writing permissions
159 | f, err := os.Create(filepath.Join(path, ".check"))
160 | if err != nil {
161 | return err
162 | }
163 |
164 | err = f.Close()
165 | if err != nil {
166 | return err
167 | }
168 |
169 | return os.Remove(f.Name())
170 | }
171 |
172 | // initDir creates a dir if not exist
173 | func initDir(path string) error {
174 | if Exists(path) {
175 | return nil
176 | }
177 | return os.MkdirAll(path, perms)
178 | }
179 |
180 | // Exists checks whether file or directory exists under the given 'path' on the system.
181 | func Exists(path string) bool {
182 | _, err := os.Stat(path)
183 | return !os.IsNotExist(err)
184 | }
185 |
--------------------------------------------------------------------------------
/store/init_test.go:
--------------------------------------------------------------------------------
1 | package store_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/celestiaorg/orchestrator-relayer/store"
7 | "github.com/stretchr/testify/assert"
8 | tmlog "github.com/tendermint/tendermint/libs/log"
9 | )
10 |
11 | func TestInit(t *testing.T) {
12 | logger := tmlog.NewNopLogger()
13 | tmp := t.TempDir()
14 |
15 | options := store.InitOptions{
16 | NeedDataStore: true,
17 | NeedSignatureStore: true,
18 | NeedEVMKeyStore: true,
19 | NeedP2PKeyStore: true,
20 | }
21 |
22 | err := store.Init(logger, tmp, options)
23 | assert.NoError(t, err)
24 |
25 | isInit := store.IsInit(logger, tmp, options)
26 | assert.True(t, isInit)
27 | }
28 |
--------------------------------------------------------------------------------
/store/store_test.go:
--------------------------------------------------------------------------------
1 | package store_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/celestiaorg/orchestrator-relayer/store"
7 | "github.com/stretchr/testify/assert"
8 | tmlog "github.com/tendermint/tendermint/libs/log"
9 | )
10 |
11 | func TestStore(t *testing.T) {
12 | logger := tmlog.NewNopLogger()
13 | path := t.TempDir()
14 |
15 | options := store.OpenOptions{
16 | HasDataStore: true,
17 | BadgerOptions: store.DefaultBadgerOptions(path),
18 | HasSignatureStore: true,
19 | HasEVMKeyStore: true,
20 | HasP2PKeyStore: true,
21 | }
22 | // open non initiated store
23 | _, err := store.OpenStore(logger, path, options)
24 | assert.Error(t, err)
25 |
26 | // init directory
27 | err = store.Init(logger, path, store.InitOptions{
28 | NeedDataStore: true,
29 | NeedSignatureStore: true,
30 | NeedEVMKeyStore: true,
31 | NeedP2PKeyStore: true,
32 | })
33 | assert.NoError(t, err)
34 |
35 | // open the store again
36 | s, err := store.OpenStore(logger, path, options)
37 | assert.NoError(t, err)
38 | assert.NotNil(t, s.DataStore)
39 | assert.NotNil(t, s.P2PKeyStore)
40 | assert.NotNil(t, s.EVMKeyStore)
41 | assert.NotNil(t, s.SignatureStore)
42 |
43 | err = s.Close(logger, options)
44 | assert.NoError(t, err)
45 | }
46 |
--------------------------------------------------------------------------------
/testing/blobstream.go:
--------------------------------------------------------------------------------
1 | package testing
2 |
3 | import (
4 | "testing"
5 | "time"
6 |
7 | "github.com/celestiaorg/orchestrator-relayer/telemetry"
8 |
9 | "github.com/celestiaorg/orchestrator-relayer/store"
10 | badger "github.com/ipfs/go-ds-badger2"
11 |
12 | "github.com/ethereum/go-ethereum/accounts"
13 | "github.com/ethereum/go-ethereum/accounts/keystore"
14 |
15 | "github.com/stretchr/testify/require"
16 |
17 | "github.com/celestiaorg/orchestrator-relayer/helpers"
18 |
19 | "github.com/celestiaorg/celestia-app/app"
20 | "github.com/celestiaorg/celestia-app/app/encoding"
21 | "github.com/celestiaorg/orchestrator-relayer/orchestrator"
22 | "github.com/celestiaorg/orchestrator-relayer/p2p"
23 | "github.com/celestiaorg/orchestrator-relayer/relayer"
24 | "github.com/celestiaorg/orchestrator-relayer/rpc"
25 |
26 | "github.com/celestiaorg/orchestrator-relayer/evm"
27 | tmlog "github.com/tendermint/tendermint/libs/log"
28 | )
29 |
30 | func NewRelayer(
31 | t *testing.T,
32 | node *TestNode,
33 | ) *relayer.Relayer {
34 | logger := tmlog.NewNopLogger()
35 | node.CelestiaNetwork.GRPCClient.Close()
36 | appQuerier := rpc.NewAppQuerier(logger, node.CelestiaNetwork.GRPCAddr, encoding.MakeConfig(app.ModuleEncodingRegisters...))
37 | require.NoError(t, appQuerier.Start(true))
38 | t.Cleanup(func() {
39 | _ = appQuerier.Stop()
40 | })
41 | tmQuerier := rpc.NewTmQuerier(node.CelestiaNetwork.RPCAddr, logger)
42 | tmQuerier.WithClientConn(node.CelestiaNetwork.Client)
43 | p2pQuerier := p2p.NewQuerier(node.DHTNetwork.DHTs[0], logger)
44 | ks := keystore.NewKeyStore(t.TempDir(), keystore.LightScryptN, keystore.LightScryptP)
45 | acc, err := ks.ImportECDSA(NodeEVMPrivateKey, "123")
46 | require.NoError(t, err)
47 | err = ks.Unlock(acc, "123")
48 | require.NoError(t, err)
49 | evmClient := NewEVMClient(ks, &acc)
50 | retrier := helpers.NewRetrier(logger, 3, 500*time.Millisecond)
51 | tempDir := t.TempDir()
52 | sigStore, err := badger.NewDatastore(tempDir, store.DefaultBadgerOptions(tempDir))
53 | require.NoError(t, err)
54 | meters, err := telemetry.InitRelayerMeters()
55 | require.NoError(t, err)
56 | r := relayer.NewRelayer(tmQuerier, appQuerier, p2pQuerier, evmClient, logger, retrier, sigStore, 30*time.Second, false, 0, meters)
57 | return r
58 | }
59 |
60 | func NewEVMClient(ks *keystore.KeyStore, acc *accounts.Account) *evm.Client {
61 | logger := tmlog.NewNopLogger()
62 | // specifying an empty RPC endpoint as we will not be testing the methods that require it.
63 | // the simulated backend doesn't provide an RPC endpoint.
64 | return evm.NewClient(logger, nil, ks, acc, "", 100000000)
65 | }
66 |
67 | func NewOrchestrator(
68 | t *testing.T,
69 | node *TestNode,
70 | ) *orchestrator.Orchestrator {
71 | logger := tmlog.NewNopLogger()
72 | appQuerier := rpc.NewAppQuerier(logger, node.CelestiaNetwork.GRPCAddr, encoding.MakeConfig(app.ModuleEncodingRegisters...))
73 | require.NoError(t, appQuerier.Start(true))
74 | t.Cleanup(func() {
75 | _ = appQuerier.Stop()
76 | })
77 | tmQuerier := rpc.NewTmQuerier(node.CelestiaNetwork.RPCAddr, logger)
78 | tmQuerier.WithClientConn(node.CelestiaNetwork.Client)
79 | p2pQuerier := p2p.NewQuerier(node.DHTNetwork.DHTs[0], logger)
80 | broadcaster := orchestrator.NewBroadcaster(node.DHTNetwork.DHTs[0])
81 | retrier := helpers.NewRetrier(logger, 3, 500*time.Millisecond)
82 | ks := keystore.NewKeyStore(t.TempDir(), keystore.LightScryptN, keystore.LightScryptP)
83 | acc, err := ks.ImportECDSA(NodeEVMPrivateKey, "123")
84 | require.NoError(t, err)
85 | err = ks.Unlock(acc, "123")
86 | require.NoError(t, err)
87 | meters, err := telemetry.InitOrchestratorMeters()
88 | require.NoError(t, err)
89 | orch := orchestrator.New(logger, appQuerier, tmQuerier, p2pQuerier, broadcaster, retrier, ks, &acc, meters)
90 | return orch
91 | }
92 |
--------------------------------------------------------------------------------
/testing/dht_network.go:
--------------------------------------------------------------------------------
1 | package testing
2 |
3 | import (
4 | "context"
5 | "time"
6 |
7 | tmlog "github.com/tendermint/tendermint/libs/log"
8 |
9 | "github.com/celestiaorg/orchestrator-relayer/p2p"
10 | ds "github.com/ipfs/go-datastore"
11 | dssync "github.com/ipfs/go-datastore/sync"
12 | "github.com/libp2p/go-libp2p"
13 | "github.com/libp2p/go-libp2p/core/host"
14 | "github.com/libp2p/go-libp2p/core/peer"
15 | )
16 |
17 | // DHTNetwork is a test DHT network that can be used for tests.
18 | type DHTNetwork struct {
19 | Context context.Context
20 | Hosts []host.Host
21 | Stores []ds.Batching
22 | DHTs []*p2p.BlobstreamDHT
23 | }
24 |
25 | // NewDHTNetwork creates a new DHT test network running in-memory.
26 | // The stores are in-memory stores.
27 | // The hosts listen on real ports.
28 | // The nodes are all connected to `hosts[0]` node.
29 | // The `count` parameter specifies the number of nodes that the network will run.
30 | // This function doesn't return any errors, and panics in case any unexpected happened.
31 | func NewDHTNetwork(ctx context.Context, count int) *DHTNetwork {
32 | if count <= 1 {
33 | panic("can't create a test network with a negative nodes count or only 1 DHT node")
34 | }
35 | hosts := make([]host.Host, count)
36 | stores := make([]ds.Batching, count)
37 | dhts := make([]*p2p.BlobstreamDHT, count)
38 | for i := 0; i < count; i++ {
39 | if i == 0 {
40 | hosts[i], stores[i], dhts[i] = NewTestDHT(ctx, nil)
41 | } else {
42 | hosts[i], stores[i], dhts[i] = NewTestDHT(ctx, []peer.AddrInfo{{
43 | ID: hosts[0].ID(),
44 | Addrs: hosts[0].Addrs(),
45 | }})
46 | }
47 | }
48 | // to give time for the DHT to update its peer table
49 | err := WaitForPeerTableToUpdate(ctx, dhts, time.Minute)
50 | if err != nil {
51 | panic(err)
52 | }
53 | return &DHTNetwork{
54 | Context: ctx,
55 | Hosts: hosts,
56 | Stores: stores,
57 | DHTs: dhts,
58 | }
59 | }
60 |
61 | // NewTestDHT creates a test DHT not connected to any peers.
62 | func NewTestDHT(ctx context.Context, bootstrappers []peer.AddrInfo) (host.Host, ds.Batching, *p2p.BlobstreamDHT) {
63 | h, err := libp2p.New()
64 | if err != nil {
65 | panic(err)
66 | }
67 | dataStore := dssync.MutexWrap(ds.NewMapDatastore())
68 | dht, err := p2p.NewBlobstreamDHT(ctx, h, dataStore, bootstrappers, tmlog.NewNopLogger())
69 | if err != nil {
70 | panic(err)
71 | }
72 | return h, dataStore, dht
73 | }
74 |
75 | // WaitForPeerTableToUpdate waits for nodes to have updated their peers list
76 | func WaitForPeerTableToUpdate(ctx context.Context, dhts []*p2p.BlobstreamDHT, timeout time.Duration) error {
77 | withTimeout, cancel := context.WithTimeout(ctx, timeout)
78 | defer cancel()
79 | ticker := time.NewTicker(time.Millisecond)
80 | for {
81 | select {
82 | case <-withTimeout.Done():
83 | return ErrTimeout
84 | case <-ticker.C:
85 | allPeersConnected := func() bool {
86 | for _, dht := range dhts {
87 | if len(dht.RoutingTable().ListPeers()) == 0 {
88 | return false
89 | }
90 | }
91 | return true
92 | }
93 | if allPeersConnected() {
94 | return nil
95 | }
96 | }
97 | }
98 | }
99 |
100 | // Stop tears down the test network and stops all the services.
101 | // Panics if an error occurs.
102 | func (tn DHTNetwork) Stop() {
103 | for i := range tn.DHTs {
104 | err := tn.DHTs[i].Close()
105 | if err != nil {
106 | panic(err)
107 | }
108 | err = tn.Stores[i].Close()
109 | if err != nil {
110 | panic(err)
111 | }
112 | err = tn.Hosts[i].Close()
113 | if err != nil {
114 | panic(err)
115 | }
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/testing/errors.go:
--------------------------------------------------------------------------------
1 | package testing
2 |
3 | import "errors"
4 |
5 | var ErrTimeout = errors.New("timeout")
6 |
--------------------------------------------------------------------------------
/testing/evm_chain.go:
--------------------------------------------------------------------------------
1 | package testing
2 |
3 | import (
4 | "context"
5 | "crypto/ecdsa"
6 | "math/big"
7 | "time"
8 |
9 | "github.com/ethereum/go-ethereum/accounts/abi/bind"
10 | "github.com/ethereum/go-ethereum/accounts/abi/bind/backends"
11 | ethcmn "github.com/ethereum/go-ethereum/common"
12 | "github.com/ethereum/go-ethereum/core"
13 | )
14 |
15 | // EVMTestNetworkChainID the test EVM network chain ID.
16 | const EVMTestNetworkChainID = 1337
17 |
18 | // EVMChain is a wrapped Geth simulated backend which will be used to simulate an EVM chain.
19 | // The resulting test chain has always 1337 as a chain ID.
20 | type EVMChain struct {
21 | Auth *bind.TransactOpts
22 | GenesisAlloc core.GenesisAlloc
23 | Backend *backends.SimulatedBackend
24 | Key *ecdsa.PrivateKey // TODO provide the keystore directly here
25 | ChainID uint64
26 | }
27 |
28 | func NewEVMChain(key *ecdsa.PrivateKey) *EVMChain {
29 | auth, err := bind.NewKeyedTransactorWithChainID(key, big.NewInt(EVMTestNetworkChainID))
30 | if err != nil {
31 | panic(err)
32 | }
33 | auth.GasLimit = 10000000000000
34 | auth.GasPrice = big.NewInt(8750000000)
35 |
36 | genBal := &big.Int{}
37 | genBal.SetString("999999999999999999999999999999999999999999", 20)
38 | gAlloc := map[ethcmn.Address]core.GenesisAccount{
39 | auth.From: {Balance: genBal},
40 | }
41 |
42 | backend := backends.NewSimulatedBackend(gAlloc, 100000000000000)
43 |
44 | return &EVMChain{
45 | Auth: auth,
46 | GenesisAlloc: gAlloc,
47 | Backend: backend,
48 | Key: key,
49 | ChainID: EVMTestNetworkChainID,
50 | }
51 | }
52 |
53 | // DefaultPeriodicCommitDelay the default delay to running the commit function on the
54 | // simulated network.
55 | const DefaultPeriodicCommitDelay = time.Millisecond
56 |
57 | // PeriodicCommit periodically run `commit()` on the simulated network to mine
58 | // the hanging blocks.
59 | // If there are no hanging transactions, the chain will not advance.
60 | func (e *EVMChain) PeriodicCommit(ctx context.Context, delay time.Duration) {
61 | defer func() {
62 | if r := recover(); r != nil {
63 | // we want to exit when the simulated blockchain has stopped instead of panic.
64 | err, ok := r.(error)
65 | if !ok || err.Error() != "blockchain is stopped" {
66 | panic(r)
67 | }
68 | }
69 | }()
70 | ticker := time.NewTicker(delay)
71 | for {
72 | select {
73 | case <-ctx.Done():
74 | return
75 | case <-ticker.C:
76 | e.Backend.Commit()
77 | }
78 | }
79 | }
80 |
81 | // Close stops the EVM chain backend.
82 | func (e *EVMChain) Close() {
83 | err := e.Backend.Close()
84 | if err != nil {
85 | panic(err)
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/testing/testnode.go:
--------------------------------------------------------------------------------
1 | package testing
2 |
3 | import (
4 | "context"
5 | "testing"
6 | )
7 |
8 | // TestNode contains a DHTNetwork along with a test Celestia network and a simulated EVM chain.
9 | type TestNode struct {
10 | Context context.Context
11 | DHTNetwork *DHTNetwork
12 | CelestiaNetwork *CelestiaNetwork
13 | EVMChain *EVMChain
14 | }
15 |
16 | func NewTestNode(ctx context.Context, t *testing.T, celestiaParams CelestiaNetworkParams) *TestNode {
17 | celestiaNetwork := NewCelestiaNetwork(ctx, t, celestiaParams)
18 | dhtNetwork := NewDHTNetwork(ctx, 2)
19 |
20 | evmChain := NewEVMChain(NodeEVMPrivateKey)
21 |
22 | return &TestNode{
23 | Context: ctx,
24 | DHTNetwork: dhtNetwork,
25 | CelestiaNetwork: celestiaNetwork,
26 | EVMChain: evmChain,
27 | }
28 | }
29 |
30 | func (tn TestNode) Close() {
31 | tn.DHTNetwork.Stop()
32 | tn.EVMChain.Close()
33 | }
34 |
--------------------------------------------------------------------------------
/types/data_commitment_confirm.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "math/big"
7 |
8 | "github.com/celestiaorg/celestia-app/x/qgb/types"
9 | ethcmn "github.com/ethereum/go-ethereum/common"
10 | "github.com/ethereum/go-ethereum/crypto"
11 | )
12 |
13 | // DataCommitmentConfirm describes a data commitment for a set of blocks.
14 | type DataCommitmentConfirm struct {
15 | // Signature over the commitment, the range of blocks, the validator address
16 | // and the Ethereum address.
17 | Signature string
18 | // Hex `0x` encoded Ethereum public key that will be used by this validator on
19 | // Ethereum.
20 | EthAddress string
21 | }
22 |
23 | // NewDataCommitmentConfirm creates a new NewDataCommitmentConfirm.
24 | func NewDataCommitmentConfirm(
25 | signature string,
26 | ethAddress ethcmn.Address,
27 | ) *DataCommitmentConfirm {
28 | return &DataCommitmentConfirm{
29 | Signature: signature,
30 | EthAddress: ethAddress.Hex(),
31 | }
32 | }
33 |
34 | // MarshalDataCommitmentConfirm Encodes a data commitment confirm to Json bytes.
35 | func MarshalDataCommitmentConfirm(dcc DataCommitmentConfirm) ([]byte, error) {
36 | encoded, err := json.Marshal(dcc)
37 | if err != nil {
38 | return nil, err
39 | }
40 | return encoded, nil
41 | }
42 |
43 | // UnmarshalDataCommitmentConfirm Decodes a data commitment confirm from Json bytes.
44 | func UnmarshalDataCommitmentConfirm(encoded []byte) (DataCommitmentConfirm, error) {
45 | var dataCommitmentConfirm DataCommitmentConfirm
46 | err := json.Unmarshal(encoded, &dataCommitmentConfirm)
47 | if err != nil {
48 | return DataCommitmentConfirm{}, err
49 | }
50 | return dataCommitmentConfirm, nil
51 | }
52 |
53 | func IsEmptyMsgDataCommitmentConfirm(dcc DataCommitmentConfirm) bool {
54 | emptyDcc := DataCommitmentConfirm{}
55 | return dcc.EthAddress == emptyDcc.EthAddress &&
56 | dcc.Signature == emptyDcc.Signature
57 | }
58 |
59 | // DataCommitmentTupleRootSignBytes EncodeDomainSeparatedDataCommitment takes the required input data and
60 | // produces the required signature to confirm a validator set update on the Blobstream Ethereum contract.
61 | // This value will then be signed before being submitted to Cosmos, verified, and then relayed to Ethereum.
62 | func DataCommitmentTupleRootSignBytes(nonce *big.Int, commitment []byte) ethcmn.Hash {
63 | var dataCommitment [32]uint8
64 | copy(dataCommitment[:], commitment)
65 |
66 | // the word 'transactionBatch' needs to be the same as the 'name' above in the DataCommitmentConfirmABIJSON
67 | // but other than that it's a constant that has no impact on the output. This is because
68 | // it gets encoded as a function name which we must then discard.
69 | bytes, err := types.InternalQGBabi.Pack(
70 | "domainSeparateDataRootTupleRoot",
71 | types.DcDomainSeparator,
72 | nonce,
73 | dataCommitment,
74 | )
75 | // this should never happen outside of test since any case that could crash on encoding
76 | // should be filtered above.
77 | if err != nil {
78 | panic(fmt.Sprintf("Error packing checkpoint! %s/n", err))
79 | }
80 |
81 | // we hash the resulting encoded bytes discarding the first 4 bytes these 4 bytes are the constant
82 | // method name 'checkpoint'. If you where to replace the checkpoint constant in this code you would
83 | // then need to adjust how many bytes you truncate off the front to get the output of abi.encode()
84 | hash := crypto.Keccak256Hash(bytes[4:])
85 | return hash
86 | }
87 |
--------------------------------------------------------------------------------
/types/data_commitment_confirm_test.go:
--------------------------------------------------------------------------------
1 | package types_test
2 |
3 | import (
4 | "bytes"
5 | "encoding/hex"
6 | "fmt"
7 | "math/big"
8 | "strconv"
9 | "testing"
10 |
11 | celestiatypes "github.com/celestiaorg/celestia-app/x/qgb/types"
12 | "github.com/celestiaorg/orchestrator-relayer/types"
13 | "github.com/ethereum/go-ethereum/crypto"
14 | "github.com/stretchr/testify/assert"
15 | "github.com/stretchr/testify/require"
16 | )
17 |
18 | func TestDataCommitmentTupleRootSignBytes(t *testing.T) {
19 | nonce := int64(1)
20 | commitment := bytes.Repeat([]byte{2}, 32)
21 |
22 | hexRepresentation := strconv.FormatInt(nonce, 16)
23 | // Make sure hex representation has even length
24 | if len(hexRepresentation)%2 == 1 {
25 | hexRepresentation = "0" + hexRepresentation
26 | }
27 | hexBytes, err := hex.DecodeString(hexRepresentation)
28 | require.NoError(t, err)
29 | paddedNonce, err := padBytes(hexBytes, 32)
30 | require.NoError(t, err)
31 |
32 | expectedHash := crypto.Keccak256Hash(append(
33 | celestiatypes.DcDomainSeparator[:],
34 | append(
35 | paddedNonce,
36 | commitment...,
37 | )...,
38 | ))
39 |
40 | result := types.DataCommitmentTupleRootSignBytes(big.NewInt(nonce), commitment)
41 |
42 | assert.Equal(t, expectedHash, result)
43 | }
44 |
45 | func TestMarshalDataCommitmentConfirm(t *testing.T) {
46 | dataCommitmentConfirm := types.DataCommitmentConfirm{
47 | Signature: "signature",
48 | EthAddress: "eth_address",
49 | }
50 |
51 | jsonData, err := types.MarshalDataCommitmentConfirm(dataCommitmentConfirm)
52 | assert.NoError(t, err)
53 | expectedJSON := `{"Signature":"signature","EthAddress":"eth_address"}`
54 | assert.Equal(t, expectedJSON, string(jsonData))
55 | }
56 |
57 | func TestUnmarshalDataCommitmentConfirm(t *testing.T) {
58 | jsonData := []byte(`{"Signature":"signature","EthAddress":"eth_address"}`)
59 | expectedDataCommitmentConfirm := types.DataCommitmentConfirm{
60 | Signature: "signature",
61 | EthAddress: "eth_address",
62 | }
63 |
64 | dataCommitmentConfirm, err := types.UnmarshalDataCommitmentConfirm(jsonData)
65 | assert.NoError(t, err)
66 | assert.Equal(t, dataCommitmentConfirm, expectedDataCommitmentConfirm)
67 | }
68 |
69 | // padBytes Pad bytes to a given length
70 | func padBytes(buf []byte, length int) ([]byte, error) {
71 | l := len(buf)
72 | if l > length {
73 | return nil, fmt.Errorf(
74 | "cannot pad bytes because length of bytes array: %d is greater than given length: %d",
75 | l,
76 | length,
77 | )
78 | }
79 | if l == length {
80 | return buf, nil
81 | }
82 | tmp := make([]byte, length)
83 | copy(tmp[length-l:], buf)
84 | return tmp, nil
85 | }
86 |
--------------------------------------------------------------------------------
/types/errors.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | import (
4 | "errors"
5 | )
6 |
7 | var (
8 | ErrAttestationNotDataCommitmentRequest = errors.New("attestation is not a data commitment request")
9 | ErrUnknownAttestationType = errors.New("unknown attestation type")
10 | ErrInvalidCommitmentInConfirm = errors.New("confirm not carrying the right commitment for expected range")
11 | ErrInvalid = errors.New("invalid")
12 | ErrAttestationNotFound = errors.New("attestation not found")
13 | ErrUnmarshalValset = errors.New("couldn't unmarshal valset")
14 | ErrAttestationNotValsetRequest = errors.New("attestation is not a valset request")
15 | )
16 |
--------------------------------------------------------------------------------
/types/latest_valset.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | import (
4 | "encoding/json"
5 | "time"
6 |
7 | "github.com/celestiaorg/celestia-app/x/qgb/types"
8 | )
9 |
10 | // LatestValset a replica of the types.Valset to omit marshalling `time` as it bears different results on different machines.
11 | type LatestValset struct {
12 | // Universal nonce defined under:
13 | // https://github.com/celestiaorg/celestia-app/pull/464
14 | Nonce uint64 `json:"nonce,omitempty"`
15 | // List of BridgeValidator containing the current validator set.
16 | Members []types.BridgeValidator `json:"members"`
17 | // Current chain height
18 | Height uint64 `json:"height,omitempty"`
19 | }
20 |
21 | func (v LatestValset) ToValset() *types.Valset {
22 | return &types.Valset{
23 | Nonce: v.Nonce,
24 | Members: v.Members,
25 | Height: v.Height,
26 | Time: time.UnixMicro(1), // it's alright to put an arbitrary value in here since the time is not used in hash creation nor the threshold.
27 | }
28 | }
29 |
30 | func ToLatestValset(vs types.Valset) *LatestValset {
31 | return &LatestValset{
32 | Nonce: vs.Nonce,
33 | Members: vs.Members,
34 | Height: vs.Height,
35 | }
36 | }
37 |
38 | // MarshalLatestValset Encodes a valset to Json bytes.
39 | func MarshalLatestValset(lv LatestValset) ([]byte, error) {
40 | encoded, err := json.Marshal(lv)
41 | if err != nil {
42 | return nil, err
43 | }
44 | return encoded, nil
45 | }
46 |
47 | // UnmarshalLatestValset Decodes a valset from Json bytes.
48 | func UnmarshalLatestValset(encoded []byte) (LatestValset, error) {
49 | var valset LatestValset
50 | err := json.Unmarshal(encoded, &valset)
51 | if err != nil {
52 | return LatestValset{}, err
53 | }
54 | return valset, nil
55 | }
56 |
57 | // IsEmptyLatestValset takes a valset and checks if it is empty.
58 | func IsEmptyLatestValset(latestValset LatestValset) bool {
59 | emptyVs := types.Valset{}
60 | return latestValset.Nonce == emptyVs.Nonce &&
61 | latestValset.Height == emptyVs.Height &&
62 | len(latestValset.Members) == 0
63 | }
64 |
65 | func IsValsetEqualToLatestValset(vs types.Valset, lvs LatestValset) bool {
66 | for index, value := range vs.Members {
67 | if value.EvmAddress != lvs.Members[index].EvmAddress ||
68 | value.Power != lvs.Members[index].Power {
69 | return false
70 | }
71 | }
72 | return vs.Nonce == lvs.Nonce && vs.Height == lvs.Height
73 | }
74 |
--------------------------------------------------------------------------------
/types/latest_valset_test.go:
--------------------------------------------------------------------------------
1 | package types_test
2 |
3 | import (
4 | "testing"
5 | "time"
6 |
7 | celestiatypes "github.com/celestiaorg/celestia-app/x/qgb/types"
8 |
9 | "github.com/celestiaorg/orchestrator-relayer/types"
10 | "github.com/stretchr/testify/assert"
11 | )
12 |
13 | func TestMarshalValset(t *testing.T) {
14 | valset := types.LatestValset{
15 | Nonce: 10,
16 | Height: 5,
17 | Members: []celestiatypes.BridgeValidator{
18 | {
19 | Power: 100,
20 | EvmAddress: "evm_addr1",
21 | },
22 | {
23 | Power: 200,
24 | EvmAddress: "evm_addr2",
25 | },
26 | },
27 | }
28 |
29 | jsonData, err := types.MarshalLatestValset(valset)
30 | assert.NoError(t, err)
31 | expectedJSON := `{"nonce":10,"members":[{"power":100,"evm_address":"evm_addr1"},{"power":200,"evm_address":"evm_addr2"}],"height":5}`
32 | assert.Equal(t, expectedJSON, string(jsonData))
33 | }
34 |
35 | func TestUnmarshalValset(t *testing.T) {
36 | jsonData := []byte(`{"nonce":10,"members":[{"power":100,"evm_address":"evm_addr1"},{"power":200,"evm_address":"evm_addr2"}],"height":5}`)
37 | expectedValset := celestiatypes.Valset{
38 | Nonce: 10,
39 | Time: time.UnixMicro(10),
40 | Height: 5,
41 | Members: []celestiatypes.BridgeValidator{
42 | {
43 | Power: 100,
44 | EvmAddress: "evm_addr1",
45 | },
46 | {
47 | Power: 200,
48 | EvmAddress: "evm_addr2",
49 | },
50 | },
51 | }
52 |
53 | valset, err := types.UnmarshalLatestValset(jsonData)
54 | assert.NoError(t, err)
55 | assert.True(t, types.IsValsetEqualToLatestValset(expectedValset, valset))
56 | }
57 |
--------------------------------------------------------------------------------
/types/valset_confirm.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | import (
4 | "encoding/json"
5 |
6 | "github.com/ethereum/go-ethereum/common"
7 | )
8 |
9 | // ValsetConfirm
10 | // this is the message sent by the validators when they wish to submit their
11 | // signatures over the validator set at a given block height. A validators sign the validator set,
12 | // powers, and Ethereum addresses of the entire validator set at the height of a
13 | // ValsetRequest and submit that signature with this message.
14 | //
15 | // If a sufficient number of validators (66% of voting power) submit ValsetConfirm
16 | // messages with their signatures, it is then possible for anyone to query them from
17 | // the Blobstream P2P network and submit them to Ethereum to update the validator set.
18 | type ValsetConfirm struct {
19 | // Ethereum address, associated to the orchestrator, used to sign the `ValSet`
20 | // message.
21 | EthAddress string
22 | // The `ValSet` message signature.
23 | Signature string
24 | }
25 |
26 | // NewValsetConfirm returns a new msgValSetConfirm.
27 | func NewValsetConfirm(
28 | ethAddress common.Address,
29 | signature string,
30 | ) *ValsetConfirm {
31 | return &ValsetConfirm{
32 | EthAddress: ethAddress.Hex(),
33 | Signature: signature,
34 | }
35 | }
36 |
37 | // MarshalValsetConfirm Encodes a valset confirm to Json bytes.
38 | func MarshalValsetConfirm(vs ValsetConfirm) ([]byte, error) {
39 | encoded, err := json.Marshal(vs)
40 | if err != nil {
41 | return nil, err
42 | }
43 | return encoded, nil
44 | }
45 |
46 | // UnmarshalValsetConfirm Decodes a valset confirm from Json bytes.
47 | func UnmarshalValsetConfirm(encoded []byte) (ValsetConfirm, error) {
48 | var valsetConfirm ValsetConfirm
49 | err := json.Unmarshal(encoded, &valsetConfirm)
50 | if err != nil {
51 | return ValsetConfirm{}, err
52 | }
53 | return valsetConfirm, nil
54 | }
55 |
56 | // IsEmptyValsetConfirm takes a msg valset confirm and checks if it is an empty one.
57 | func IsEmptyValsetConfirm(vs ValsetConfirm) bool {
58 | emptyVsConfirm := ValsetConfirm{}
59 | return vs.EthAddress == emptyVsConfirm.EthAddress &&
60 | vs.Signature == emptyVsConfirm.Signature
61 | }
62 |
--------------------------------------------------------------------------------
/types/valset_confirm_test.go:
--------------------------------------------------------------------------------
1 | package types_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/celestiaorg/orchestrator-relayer/types"
7 | "github.com/stretchr/testify/assert"
8 | )
9 |
10 | func TestMarshalValsetConfirm(t *testing.T) {
11 | valsetConfirm := types.ValsetConfirm{
12 | EthAddress: "eth_address",
13 | Signature: "signature",
14 | }
15 |
16 | jsonData, err := types.MarshalValsetConfirm(valsetConfirm)
17 | assert.NoError(t, err)
18 | expectedJSON := `{"EthAddress":"eth_address","Signature":"signature"}`
19 | assert.Equal(t, string(jsonData), expectedJSON)
20 | }
21 |
22 | func TestUnmarshalValsetConfirm(t *testing.T) {
23 | jsonData := []byte(`{"EthAddress":"eth_address","Signature":"signature"}`)
24 | expectedValsetConfirm := types.ValsetConfirm{
25 | EthAddress: "eth_address",
26 | Signature: "signature",
27 | }
28 |
29 | valsetConfirm, err := types.UnmarshalValsetConfirm(jsonData)
30 | assert.NoError(t, err)
31 | assert.Equal(t, valsetConfirm, expectedValsetConfirm)
32 | }
33 |
--------------------------------------------------------------------------------