├── .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: "<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 | <!-- markdownlint-disable MD013 --> 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 | <!-- markdownlint-disable MD013 --> 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> [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 <test_name> 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 <<EOF > /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 <<EOF > ${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: "/<DataCommitmentConfirmNamespace>/<nonce>:<evm_account>:<data_root_tuple_root>": 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: "/<ValsetNamespace>/<nonce>:<evm_account>:<sign_bytes>": 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 | --------------------------------------------------------------------------------