├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── backlog-item.md │ ├── bug_report.md │ └── feature-request-or-epic.md ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── ci.yml │ └── docker-build-and-push.yml ├── .gitignore ├── .golangci.yml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── bindings ├── AccessControl │ └── AccessControl.go ├── Bridge │ └── Bridge.go ├── CentrifugeAsset │ └── CentrifugeAsset.go ├── Context │ └── Context.go ├── ERC165 │ └── ERC165.go ├── ERC20 │ └── ERC20.go ├── ERC20Burnable │ └── ERC20Burnable.go ├── ERC20Handler │ └── ERC20Handler.go ├── ERC20Pausable │ └── ERC20Pausable.go ├── ERC20PresetMinterPauser │ └── ERC20PresetMinterPauser.go ├── ERC20Safe │ └── ERC20Safe.go ├── ERC721 │ └── ERC721.go ├── ERC721Burnable │ └── ERC721Burnable.go ├── ERC721Handler │ └── ERC721Handler.go ├── ERC721MinterBurnerPauser │ └── ERC721MinterBurnerPauser.go ├── ERC721Pausable │ └── ERC721Pausable.go ├── ERC721Safe │ └── ERC721Safe.go ├── GenericHandler │ └── GenericHandler.go ├── HandlerHelpers │ └── HandlerHelpers.go ├── IBridge │ └── IBridge.go ├── IDepositExecute │ └── IDepositExecute.go ├── IERC165 │ └── IERC165.go ├── IERC20 │ └── IERC20.go ├── IERC721 │ └── IERC721.go ├── IERC721Enumerable │ └── IERC721Enumerable.go ├── IERC721Metadata │ └── IERC721Metadata.go ├── IERC721Receiver │ └── IERC721Receiver.go ├── IERCHandler │ └── IERCHandler.go ├── IGenericHandler │ └── IGenericHandler.go ├── Migrations │ └── Migrations.go ├── NoArgument │ └── NoArgument.go ├── OneArgument │ └── OneArgument.go ├── Pausable │ └── Pausable.go ├── ThreeArguments │ └── ThreeArguments.go └── TwoArguments │ └── TwoArguments.go ├── chains ├── ethereum │ ├── chain.go │ ├── chain_test.go │ ├── config.go │ ├── config_test.go │ ├── events.go │ ├── listener.go │ ├── listener_test.go │ ├── proposal_data.go │ ├── test_utils_test.go │ ├── writer.go │ ├── writer_methods.go │ └── writer_test.go ├── interfaces.go └── substrate │ ├── chain.go │ ├── config.go │ ├── config_test.go │ ├── connection.go │ ├── connection_test.go │ ├── events.go │ ├── listener.go │ ├── listener_test.go │ ├── test_helper_test.go │ ├── types.go │ ├── writer.go │ └── writer_test.go ├── cmd └── chainbridge │ ├── account.go │ ├── account_test.go │ └── main.go ├── config ├── config.go ├── config_test.go └── flags.go ├── connections └── ethereum │ ├── connection.go │ ├── connection_test.go │ └── egs │ ├── egs.go │ └── egs_test.go ├── docker-compose-e2e.yml ├── docker-compose.yml ├── docs ├── metrics.md └── test.md ├── e2e ├── e2e_test.go ├── ethereum │ └── ethereum.go ├── fungible_test.go ├── generic_test.go ├── nonfungible_test.go ├── parallel_test.go └── substrate │ └── substrate.go ├── go.mod ├── go.sum ├── scripts ├── ci_docker.sh ├── configs │ └── config1.json ├── docker │ └── start-docker.sh ├── geth │ ├── Dockerfile │ ├── docker_deploy.sh │ ├── entrypoint.sh │ ├── genesis.json │ ├── keystore │ │ ├── UTC--2020-04-07T13-50-35.447Z--ff93b45308fd417df303d6515ab04d9e89a750ca │ │ ├── UTC--2020-04-07T13-52-12.564Z--8e0a907331554af72563bd8d43051c2e64be5d35 │ │ ├── UTC--2020-04-07T13-53-49.003Z--24962717f8fa5ba3b931bacaf9ac03924eb475a0 │ │ ├── UTC--2020-04-07T13-55-20.258Z--148ffb2074a9e59ed58142822b3eb3fcbffb0cd7 │ │ └── UTC--2020-04-07T13-56-44.768Z--4ceef6139f00f9f4535ad19640ff7a0137708485 │ ├── password.txt │ └── run_geth.sh ├── header.txt ├── install_subkey.sh ├── setup_contracts.sh └── start_ganache.sh └── shared ├── ethereum ├── bridge.go ├── centrifuge.go ├── client.go ├── deploy.go ├── deposit.go ├── erc20.go ├── erc721.go ├── events.go ├── generic.go ├── hash.go └── testing │ ├── bridge.go │ ├── centrifuge.go │ ├── client.go │ ├── erc20.go │ ├── erc721.go │ ├── events.go │ └── generic.go ├── logs.go └── substrate ├── client.go ├── events.go ├── init.go ├── methods.go ├── query.go ├── submit.go ├── testing ├── client.go ├── events.go ├── events_test.go ├── init.go └── query.go └── types.go /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/backlog-item.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Template For Internal Use 3 | about: Speccing out the details of development for specific features/epics 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | ## Implementation details 13 | 14 | 15 | ## Testing details 16 | 17 | 18 | ## Acceptance Criteria 19 | 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | ## Expected Behavior 13 | 14 | 15 | 16 | ## Current Behavior 17 | 18 | 19 | 20 | ## Possible Solution 21 | 22 | 23 | 24 | ## Steps to Reproduce (for bugs) 25 | 26 | 27 | 1. 28 | 2. 29 | 3. 30 | 4. 31 | 32 | ## Versions 33 | ChainBridge commit (or docker tag): 34 | chainbridge-solidity version: 35 | chainbridge-substrate version: 36 | Go version: 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request-or-epic.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request or epic 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Story 11 | As a 12 | I want 13 | So that I can 14 | 15 | ## Background 16 | 17 | 18 | ## Details 19 | 20 | 21 | ## Scenarios 22 | Scenario: 23 | Given I am 24 | When 25 | And 26 | Then 27 | 28 | ## Implementation details 29 | 30 | 31 | ## Testing details 32 | 33 | 34 | ## Acceptance criteria 35 | 36 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 ChainSafe Systems 2 | # SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | version: 2 5 | updates: 6 | # Maintain dependencies for GitHub Actions 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "daily" 11 | 12 | - package-ecosystem: "gomod" 13 | directory: "/" 14 | schedule: 15 | interval: "daily" -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description 4 | 5 | 6 | ## Related Issue Or Context 7 | 8 | 9 | 10 | 11 | Closes: # 12 | 13 | ## How Has This Been Tested? Testing details. 14 | 15 | 16 | 17 | 18 | ## Types of changes 19 | 20 | - [ ] Bug fix (non-breaking change which fixes an issue) 21 | - [ ] New feature (non-breaking change which adds functionality) 22 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 23 | - [ ] Documentation 24 | 25 | ## Checklist: 26 | 27 | 28 | - [ ] I have commented my code, particularly in hard-to-understand areas. 29 | - [ ] I have ensured that all acceptance criteria (or expected behavior) from issue are met 30 | - [ ] I have updated the documentation locally and in chainbridge-docs. 31 | - [ ] I have added tests to cover my changes. 32 | - [ ] I have ensured that all the checks are passing and green, I've signed the CLA bot 33 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 ChainSafe Systems 2 | # SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | name: Test 5 | 6 | on: 7 | pull_request: 8 | types: [opened, synchronize, reopened] 9 | push: 10 | branches: 11 | - main 12 | tags: 13 | - "v*.*.*" 14 | 15 | jobs: 16 | test: 17 | name: Tests 18 | strategy: 19 | matrix: 20 | go-version: [1.15.x] 21 | platform: [ubuntu-latest] 22 | runs-on: ${{ matrix.platform }} 23 | steps: 24 | - name: Install Go 25 | uses: actions/setup-go@v2 26 | with: 27 | go-version: ${{ matrix.go-version }} 28 | - name: Checkout code 29 | uses: actions/checkout@v2 30 | - uses: actions/cache@v2.1.6 31 | with: 32 | path: ~/go/pkg/mod 33 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 34 | restore-keys: | 35 | ${{ runner.os }}-go- 36 | - name: Install Subkey 37 | run: | 38 | wget -P $HOME/.local/bin/ https://chainbridge.ams3.digitaloceanspaces.com/subkey-v2.0.0 39 | mv $HOME/.local/bin/subkey-v2.0.0 $HOME/.local/bin/subkey 40 | chmod +x $HOME/.local/bin/subkey 41 | echo "$HOME/.local/bin" >> $GITHUB_PATH 42 | - name: Test 43 | run: | 44 | docker-compose -f ./docker-compose-e2e.yml up -d 45 | sleep 3 46 | docker ps 47 | make test 48 | 49 | e2e: 50 | name: E2E Tests 51 | strategy: 52 | matrix: 53 | go-version: [ 1.15.x ] 54 | platform: [ ubuntu-latest ] 55 | runs-on: ${{ matrix.platform }} 56 | steps: 57 | - name: Install Go 58 | uses: actions/setup-go@v2 59 | with: 60 | go-version: ${{ matrix.go-version }} 61 | - name: Checkout code 62 | uses: actions/checkout@v2 63 | - uses: actions/cache@v2.1.6 64 | with: 65 | path: ~/go/pkg/mod 66 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 67 | restore-keys: | 68 | ${{ runner.os }}-go- 69 | - name: Install Subkey 70 | run: | 71 | wget -P $HOME/.local/bin/ https://chainbridge.ams3.digitaloceanspaces.com/subkey-v2.0.0 72 | mv $HOME/.local/bin/subkey-v2.0.0 $HOME/.local/bin/subkey 73 | chmod +x $HOME/.local/bin/subkey 74 | echo "$HOME/.local/bin" >> $GITHUB_PATH 75 | - name: Test 76 | run: | 77 | docker-compose -f ./docker-compose-e2e.yml up -d 78 | docker ps 79 | make test-e2e 80 | 81 | lint: 82 | name: Lint and License Headers 83 | runs-on: ubuntu-latest 84 | steps: 85 | - uses: actions/checkout@v2 86 | - name: golangci-lint 87 | uses: golangci/golangci-lint-action@v2 88 | with: 89 | version: v1.36 90 | args: --timeout=5m 91 | - name: License Check 92 | run: make license-check -------------------------------------------------------------------------------- /.github/workflows/docker-build-and-push.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 ChainSafe Systems 2 | # SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | name: Docker build and push 5 | 6 | on: 7 | push: 8 | branches: 9 | # when main branch receives a push 10 | - main 11 | # when any of these versions receive a push 12 | tags: 13 | - "v*.*.*" 14 | # when any releases are created 15 | release: 16 | types: 17 | - created 18 | # list jobs 19 | jobs: 20 | build-and-deploy: 21 | name: Docker Deployment 22 | runs-on: ubuntu-latest 23 | # requires these jobs to run first 24 | needs: [test, e2e, lint] 25 | # if check not passed, job will be skipped 26 | if: github.ref == 'refs/heads/main' || contains(github.ref, '/tags/v') 27 | environment: 28 | name: build 29 | # list steps 30 | steps: 31 | # prepares docker images 32 | - name: Prepare 33 | id: prep 34 | run: | 35 | # creates local variable of chainbridge docker image 36 | DOCKER_IMAGE=chainsafe/chainbridge 37 | 38 | # creates local variable of commit hash that triggered workflow 39 | COMMIT_HASH=$(echo $GITHUB_SHA | head -c7) 40 | 41 | # creates local variable to hold docker images 42 | TAGS="${DOCKER_IMAGE}:${COMMIT_HASH},${DOCKER_IMAGE}:latest" 43 | 44 | # check if branch/tag that triggered workflow was from tags 45 | if [[ $GITHUB_REF == refs/tags/* ]]; then 46 | 47 | # set version 48 | VERSION=${GITHUB_REF#refs/tags/} 49 | 50 | # append version to tags 51 | TAGS="${TAGS},${DOCKER_IMAGE}:${VERSION}" 52 | 53 | fi 54 | 55 | # sets output of step 56 | echo ::set-output name=tags::${TAGS} 57 | - name: Set up QEMU 58 | uses: docker/setup-qemu-action@v1 59 | - name: Set up Docker Buildx 60 | uses: docker/setup-buildx-action@v1 61 | - name: Login to DockerHub 62 | uses: docker/login-action@v1 63 | with: 64 | username: ${{ secrets.DOCKERHUB_USERNAME }} 65 | password: ${{ secrets.DOCKERHUB_TOKEN }} 66 | - name: Build and push 67 | id: docker_build 68 | uses: docker/build-push-action@v2 69 | with: 70 | push: ${{ github.event_name != 'pull_request' }} 71 | tags: ${{ steps.prep.outputs.tags }} 72 | - name: Image digest 73 | run: echo ${{ steps.docker_build.outputs.digest }} 74 | test: 75 | name: Tests 76 | strategy: 77 | matrix: 78 | go-version: [1.15.x] 79 | platform: [ubuntu-latest] 80 | runs-on: ${{ matrix.platform }} 81 | steps: 82 | - name: Install Go 83 | uses: actions/setup-go@v2 84 | with: 85 | go-version: ${{ matrix.go-version }} 86 | - name: Checkout code 87 | uses: actions/checkout@v2 88 | - uses: actions/cache@v2.1.6 89 | with: 90 | path: ~/go/pkg/mod 91 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 92 | restore-keys: | 93 | ${{ runner.os }}-go- 94 | - name: Install Subkey 95 | run: | 96 | wget -P $HOME/.local/bin/ https://chainbridge.ams3.digitaloceanspaces.com/subkey-v2.0.0 97 | mv $HOME/.local/bin/subkey-v2.0.0 $HOME/.local/bin/subkey 98 | chmod +x $HOME/.local/bin/subkey 99 | echo "$HOME/.local/bin" >> $GITHUB_PATH 100 | - name: Test 101 | run: | 102 | docker-compose -f ./docker-compose-e2e.yml up -d 103 | sleep 3 104 | docker ps 105 | make test 106 | 107 | e2e: 108 | name: E2E Tests 109 | strategy: 110 | matrix: 111 | go-version: [ 1.15.x ] 112 | platform: [ ubuntu-latest ] 113 | runs-on: ${{ matrix.platform }} 114 | steps: 115 | - name: Install Go 116 | uses: actions/setup-go@v2 117 | with: 118 | go-version: ${{ matrix.go-version }} 119 | - name: Checkout code 120 | uses: actions/checkout@v2 121 | - uses: actions/cache@v2.1.6 122 | with: 123 | path: ~/go/pkg/mod 124 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 125 | restore-keys: | 126 | ${{ runner.os }}-go- 127 | - name: Install Subkey 128 | run: | 129 | wget -P $HOME/.local/bin/ https://chainbridge.ams3.digitaloceanspaces.com/subkey-v2.0.0 130 | mv $HOME/.local/bin/subkey-v2.0.0 $HOME/.local/bin/subkey 131 | chmod +x $HOME/.local/bin/subkey 132 | echo "$HOME/.local/bin" >> $GITHUB_PATH 133 | - name: Test 134 | run: | 135 | docker-compose -f ./docker-compose-e2e.yml up -d 136 | docker ps 137 | make test-e2e 138 | 139 | lint: 140 | name: Lint and License Headers 141 | runs-on: ubuntu-latest 142 | steps: 143 | - uses: actions/checkout@v2 144 | - name: golangci-lint 145 | uses: golangci/golangci-lint-action@v2 146 | with: 147 | version: v1.36 148 | args: --timeout=5m 149 | - name: License Check 150 | run: make license-check -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | .DS_Store 8 | 9 | # Test binary, build with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | coverage.txt 15 | 16 | # IDE 17 | .idea/ 18 | .vscode/ 19 | .history 20 | 21 | # Builds 22 | build/ 23 | bin/ 24 | site/ 25 | 26 | # golang 27 | .env 28 | bridge 29 | keys/ 30 | *.key 31 | 32 | # Chainbridge 33 | config.toml 34 | config.json 35 | *.block 36 | gethdata1/ 37 | gethdata2/ 38 | centrifuge-chain/ 39 | # Solidity 40 | /solidity 41 | /cfgBuilder/*.toml 42 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2020 ChainSafe Systems 2 | # SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | FROM golang:1.13-stretch AS builder 5 | ADD . /src 6 | WORKDIR /src 7 | RUN go mod download 8 | RUN cd cmd/chainbridge && go build -o /bridge . 9 | 10 | # # final stage 11 | FROM debian:stretch-slim 12 | RUN apt-get -y update && apt-get -y upgrade && apt-get install ca-certificates wget -y 13 | RUN wget -P /usr/local/bin/ https://chainbridge.ams3.digitaloceanspaces.com/subkey-rc6 \ 14 | && mv /usr/local/bin/subkey-rc6 /usr/local/bin/subkey \ 15 | && chmod +x /usr/local/bin/subkey 16 | RUN subkey --version 17 | 18 | COPY --from=builder /bridge ./ 19 | RUN chmod +x ./bridge 20 | 21 | ENTRYPOINT ["./bridge"] 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROJECTNAME=$(shell basename "$(PWD)") 2 | VERSION=-ldflags="-X main.Version=$(shell git describe --tags)" 3 | SOL_DIR=./solidity 4 | 5 | CENT_EMITTER_ADDR?=0x1 6 | CENT_CHAIN_ID?=0x1 7 | CENT_TO?=0x1234567890 8 | CENT_TOKEN_ID?=0x5 9 | CENT_METADATA?=0x0 10 | 11 | .PHONY: help run build install license 12 | all: help 13 | 14 | help: Makefile 15 | @echo 16 | @echo "Choose a make command to run in "$(PROJECTNAME)":" 17 | @echo 18 | @$(MAKE) -pRrq -f $(lastword $(MAKEFILE_LIST)) : 2>/dev/null | awk -v RS= -F: '/^# File/,/^# Finished Make data base/ {if ($$1 !~ "^[#.]") {print $$1}}' | sort | egrep -v -e '^[^[:alnum:]]' -e '^$@$$' 19 | @echo 20 | 21 | get: 22 | @echo " > \033[32mDownloading & Installing all the modules...\033[0m " 23 | go mod tidy && go mod download 24 | 25 | get-lint: 26 | curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s v1.31.0 27 | 28 | .PHONY: lint 29 | lint: 30 | if [ ! -f ./bin/golangci-lint ]; then \ 31 | $(MAKE) get-lint; \ 32 | fi; 33 | ./bin/golangci-lint run ./... --timeout 5m0s 34 | 35 | lint-fix: 36 | if [ ! -f ./bin/golangci-lint ]; then \ 37 | $(MAKE) get-lint; \ 38 | fi; 39 | ./bin/golangci-lint run ./... --timeout 5m0s --fix 40 | 41 | build: 42 | @echo " > \033[32mBuilding binary...\033[0m " 43 | cd cmd/chainbridge && env GOARCH=amd64 go build -o ../../build/chainbridge $(VERSION) 44 | 45 | install: 46 | @echo " > \033[32mInstalling bridge...\033[0m " 47 | cd cmd/chainbridge && go install $(VERSION) 48 | 49 | build-mkdocs: 50 | docker run --rm -it -v ${PWD}:/docs squidfunk/mkdocs-material build 51 | 52 | setup-sol-cli: 53 | @echo " > \033[32mSetting up solidity cli... \033[0m " 54 | TARGET=cli-only ./scripts/setup_contracts.sh 55 | 56 | rebuild-contracts: 57 | rm -rf bindings/ solidity/ 58 | TARGET=build ./scripts/setup_contracts.sh 59 | 60 | ## license: Adds license header to missing files. 61 | license: 62 | @echo " > \033[32mAdding license headers...\033[0m " 63 | GO111MODULE=off go get -u github.com/google/addlicense 64 | addlicense -c "ChainSafe Systems" -f ./scripts/header.txt -y 2020 . 65 | 66 | ## license-check: Checks for missing license headers 67 | license-check: 68 | @echo " > \033[Checking for license headers...\033[0m " 69 | GO111MODULE=off go get -u github.com/google/addlicense 70 | addlicense -check -c "ChainSafe Systems" -f ./scripts/header.txt -y 2020 . 71 | 72 | ## Install dependency subkey 73 | install-subkey: 74 | curl https://getsubstrate.io -sSf | bash -s -- --fast 75 | cargo install --force --git https://github.com/paritytech/substrate subkey 76 | 77 | ## Runs go test for all packages except the solidity bindings 78 | test: 79 | @echo " > \033[32mRunning tests...\033[0m " 80 | go test -p 1 -coverprofile=cover.out -v `go list ./... | grep -v bindings | grep -v e2e` 81 | 82 | test-e2e: 83 | @echo " > \033[32mRunning e2e tests...\033[0m " 84 | go test -p 1 -v -timeout 0 ./e2e 85 | 86 | test-eth: 87 | @echo " > \033[32mRunning ethereum tests...\033[0m " 88 | go test ./chains/ethereum 89 | 90 | test-sub: 91 | @echo " > \033[32mRunning substrate tests...\033[0m " 92 | go test ./chains/substrate 93 | 94 | docker-start: 95 | ./scripts/docker/start-docker.sh 96 | 97 | docker-e2e: 98 | docker-compose -f ./docker-compose-e2e.yml up -V 99 | 100 | mkdocs: 101 | docker run --rm -it -p 8000:8000 -v ${PWD}:/docs squidfunk/mkdocs-material 102 | 103 | clean: 104 | rm -rf build/ solidity/ 105 | -------------------------------------------------------------------------------- /chains/ethereum/chain.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 ChainSafe Systems 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | /* 4 | The ethereum package contains the logic for interacting with ethereum chains. 5 | 6 | There are 3 major components: the connection, the listener, and the writer. 7 | The currently supported transfer types are Fungible (ERC20), Non-Fungible (ERC721), and generic. 8 | 9 | Connection 10 | 11 | The connection contains the ethereum RPC client and can be accessed by both the writer and listener. 12 | 13 | Listener 14 | 15 | The listener polls for each new block and looks for deposit events in the bridge contract. If a deposit occurs, the listener will fetch additional information from the handler before constructing a message and forwarding it to the router. 16 | 17 | Writer 18 | 19 | The writer recieves the message and creates a proposals on-chain. Once a proposal is made, the writer then watches for a finalization event and will attempt to execute the proposal if a matching event occurs. The writer skips over any proposals it has already seen. 20 | */ 21 | package ethereum 22 | 23 | import ( 24 | "fmt" 25 | "math/big" 26 | 27 | bridge "github.com/ChainSafe/ChainBridge/bindings/Bridge" 28 | erc20Handler "github.com/ChainSafe/ChainBridge/bindings/ERC20Handler" 29 | erc721Handler "github.com/ChainSafe/ChainBridge/bindings/ERC721Handler" 30 | "github.com/ChainSafe/ChainBridge/bindings/GenericHandler" 31 | connection "github.com/ChainSafe/ChainBridge/connections/ethereum" 32 | utils "github.com/ChainSafe/ChainBridge/shared/ethereum" 33 | "github.com/ChainSafe/chainbridge-utils/blockstore" 34 | "github.com/ChainSafe/chainbridge-utils/core" 35 | "github.com/ChainSafe/chainbridge-utils/crypto/secp256k1" 36 | "github.com/ChainSafe/chainbridge-utils/keystore" 37 | metrics "github.com/ChainSafe/chainbridge-utils/metrics/types" 38 | "github.com/ChainSafe/chainbridge-utils/msg" 39 | "github.com/ChainSafe/log15" 40 | "github.com/ethereum/go-ethereum/accounts/abi/bind" 41 | "github.com/ethereum/go-ethereum/common" 42 | "github.com/ethereum/go-ethereum/ethclient" 43 | ) 44 | 45 | var _ core.Chain = &Chain{} 46 | 47 | var _ Connection = &connection.Connection{} 48 | 49 | type Connection interface { 50 | Connect() error 51 | Keypair() *secp256k1.Keypair 52 | Opts() *bind.TransactOpts 53 | CallOpts() *bind.CallOpts 54 | LockAndUpdateOpts() error 55 | UnlockOpts() 56 | Client() *ethclient.Client 57 | EnsureHasBytecode(address common.Address) error 58 | LatestBlock() (*big.Int, error) 59 | WaitForBlock(block *big.Int, delay *big.Int) error 60 | Close() 61 | } 62 | 63 | type Chain struct { 64 | cfg *core.ChainConfig // The config of the chain 65 | conn Connection // THe chains connection 66 | listener *listener // The listener of this chain 67 | writer *writer // The writer of the chain 68 | stop chan<- int 69 | } 70 | 71 | // checkBlockstore queries the blockstore for the latest known block. If the latest block is 72 | // greater than cfg.startBlock, then cfg.startBlock is replaced with the latest known block. 73 | func setupBlockstore(cfg *Config, kp *secp256k1.Keypair) (*blockstore.Blockstore, error) { 74 | bs, err := blockstore.NewBlockstore(cfg.blockstorePath, cfg.id, kp.Address()) 75 | if err != nil { 76 | return nil, err 77 | } 78 | 79 | if !cfg.freshStart { 80 | latestBlock, err := bs.TryLoadLatestBlock() 81 | if err != nil { 82 | return nil, err 83 | } 84 | 85 | if latestBlock.Cmp(cfg.startBlock) == 1 { 86 | cfg.startBlock = latestBlock 87 | } 88 | } 89 | 90 | return bs, nil 91 | } 92 | 93 | func InitializeChain(chainCfg *core.ChainConfig, logger log15.Logger, sysErr chan<- error, m *metrics.ChainMetrics) (*Chain, error) { 94 | cfg, err := parseChainConfig(chainCfg) 95 | if err != nil { 96 | return nil, err 97 | } 98 | 99 | kpI, err := keystore.KeypairFromAddress(cfg.from, keystore.EthChain, cfg.keystorePath, chainCfg.Insecure) 100 | if err != nil { 101 | return nil, err 102 | } 103 | kp, _ := kpI.(*secp256k1.Keypair) 104 | 105 | bs, err := setupBlockstore(cfg, kp) 106 | if err != nil { 107 | return nil, err 108 | } 109 | 110 | stop := make(chan int) 111 | conn := connection.NewConnection(cfg.endpoint, cfg.http, kp, logger, cfg.gasLimit, cfg.maxGasPrice, cfg.minGasPrice, cfg.gasMultiplier, cfg.egsApiKey, cfg.egsSpeed) 112 | err = conn.Connect() 113 | if err != nil { 114 | return nil, err 115 | } 116 | err = conn.EnsureHasBytecode(cfg.bridgeContract) 117 | if err != nil { 118 | return nil, err 119 | } 120 | 121 | if cfg.erc20HandlerContract != utils.ZeroAddress { 122 | err = conn.EnsureHasBytecode(cfg.erc20HandlerContract) 123 | if err != nil { 124 | return nil, err 125 | } 126 | } 127 | 128 | if cfg.genericHandlerContract != utils.ZeroAddress { 129 | err = conn.EnsureHasBytecode(cfg.genericHandlerContract) 130 | if err != nil { 131 | return nil, err 132 | } 133 | } 134 | 135 | bridgeContract, err := bridge.NewBridge(cfg.bridgeContract, conn.Client()) 136 | if err != nil { 137 | return nil, err 138 | } 139 | 140 | chainId, err := bridgeContract.ChainID(conn.CallOpts()) 141 | if err != nil { 142 | return nil, err 143 | } 144 | 145 | if chainId != uint8(chainCfg.Id) { 146 | return nil, fmt.Errorf("chainId (%d) and configuration chainId (%d) do not match", chainId, chainCfg.Id) 147 | } 148 | 149 | erc20HandlerContract, err := erc20Handler.NewERC20Handler(cfg.erc20HandlerContract, conn.Client()) 150 | if err != nil { 151 | return nil, err 152 | } 153 | 154 | erc721HandlerContract, err := erc721Handler.NewERC721Handler(cfg.erc721HandlerContract, conn.Client()) 155 | if err != nil { 156 | return nil, err 157 | } 158 | 159 | genericHandlerContract, err := GenericHandler.NewGenericHandler(cfg.genericHandlerContract, conn.Client()) 160 | if err != nil { 161 | return nil, err 162 | } 163 | 164 | if chainCfg.LatestBlock { 165 | curr, err := conn.LatestBlock() 166 | if err != nil { 167 | return nil, err 168 | } 169 | cfg.startBlock = curr 170 | } 171 | 172 | listener := NewListener(conn, cfg, logger, bs, stop, sysErr, m) 173 | listener.setContracts(bridgeContract, erc20HandlerContract, erc721HandlerContract, genericHandlerContract) 174 | 175 | writer := NewWriter(conn, cfg, logger, stop, sysErr, m) 176 | writer.setContract(bridgeContract) 177 | 178 | return &Chain{ 179 | cfg: chainCfg, 180 | conn: conn, 181 | writer: writer, 182 | listener: listener, 183 | stop: stop, 184 | }, nil 185 | } 186 | 187 | func (c *Chain) SetRouter(r *core.Router) { 188 | r.Listen(c.cfg.Id, c.writer) 189 | c.listener.setRouter(r) 190 | } 191 | 192 | func (c *Chain) Start() error { 193 | err := c.listener.start() 194 | if err != nil { 195 | return err 196 | } 197 | 198 | err = c.writer.start() 199 | if err != nil { 200 | return err 201 | } 202 | 203 | c.writer.log.Debug("Successfully started chain") 204 | return nil 205 | } 206 | 207 | func (c *Chain) Id() msg.ChainId { 208 | return c.cfg.Id 209 | } 210 | 211 | func (c *Chain) Name() string { 212 | return c.cfg.Name 213 | } 214 | 215 | func (c *Chain) LatestBlock() metrics.LatestBlock { 216 | return c.listener.latestBlock 217 | } 218 | 219 | // Stop signals to any running routines to exit 220 | func (c *Chain) Stop() { 221 | close(c.stop) 222 | if c.conn != nil { 223 | c.conn.Close() 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /chains/ethereum/chain_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 ChainSafe Systems 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | package ethereum 5 | 6 | import ( 7 | "math/big" 8 | "testing" 9 | "time" 10 | 11 | ethtest "github.com/ChainSafe/ChainBridge/shared/ethereum/testing" 12 | "github.com/ChainSafe/chainbridge-utils/core" 13 | "github.com/ChainSafe/chainbridge-utils/keystore" 14 | "github.com/ChainSafe/chainbridge-utils/msg" 15 | "github.com/ethereum/go-ethereum/common" 16 | ethcrypto "github.com/ethereum/go-ethereum/crypto" 17 | ) 18 | 19 | func TestChain_ListenerShutdownOnFailure(t *testing.T) { 20 | client := ethtest.NewClient(t, TestEndpoint, AliceKp) 21 | contracts := deployTestContracts(t, client, msg.ChainId(1)) 22 | cfg := &core.ChainConfig{ 23 | Id: msg.ChainId(1), 24 | Name: "alice", 25 | Endpoint: TestEndpoint, 26 | From: keystore.AliceKey, 27 | Insecure: true, 28 | KeystorePath: keystore.AliceKey, 29 | BlockstorePath: "", 30 | FreshStart: true, 31 | Opts: map[string]string{ 32 | "bridge": contracts.BridgeAddress.Hex(), 33 | "erc20Handler": contracts.ERC20HandlerAddress.Hex(), 34 | "erc721Handler": contracts.ERC721HandlerAddress.Hex(), 35 | "genericHandler": contracts.GenericHandlerAddress.Hex(), 36 | "gasLimit": big.NewInt(DefaultGasLimit).String(), 37 | "maxGasPrice": big.NewInt(DefaultGasPrice).String(), 38 | }, 39 | } 40 | sysErr := make(chan error) 41 | chain, err := InitializeChain(cfg, TestLogger, sysErr, nil) 42 | if err != nil { 43 | t.Fatal(err) 44 | } 45 | 46 | err = chain.Start() 47 | if err != nil { 48 | t.Fatal(err) 49 | } 50 | 51 | // Cause critical failure to polling mechanism 52 | chain.conn.Client().Close() 53 | 54 | // Pull expected error 55 | select { 56 | case err := <-sysErr: 57 | if err.Error() != ErrFatalPolling.Error() { 58 | t.Fatalf("Unexpected error: %s", err) 59 | } 60 | case <-time.After(time.Second * 30): 61 | t.Fatal("Test timed out") 62 | } 63 | 64 | // Tell everyone to shutdown 65 | chain.Stop() 66 | } 67 | 68 | func TestChain_WriterShutdownOnFailure(t *testing.T) { 69 | // Setup contracts and params for erc20 transfer 70 | client := ethtest.NewClient(t, TestEndpoint, AliceKp) 71 | contracts := deployTestContracts(t, client, msg.ChainId(1)) 72 | erc20Contract := ethtest.DeployMintApproveErc20(t, client, contracts.ERC20HandlerAddress, big.NewInt(100)) 73 | src := msg.ChainId(5) // Not yet used, nonce should be 0 74 | dst := msg.ChainId(1) 75 | amount := big.NewInt(10) 76 | resourceId := msg.ResourceIdFromSlice(append(common.LeftPadBytes(erc20Contract.Bytes(), 31), uint8(src))) 77 | recipient := ethcrypto.PubkeyToAddress(BobKp.PrivateKey().PublicKey) 78 | ethtest.RegisterResource(t, client, contracts.BridgeAddress, contracts.ERC20HandlerAddress, resourceId, erc20Contract) 79 | 80 | // Start a chain 81 | cfg := &core.ChainConfig{ 82 | Id: dst, 83 | Name: "alice", 84 | Endpoint: TestEndpoint, 85 | From: keystore.AliceKey, 86 | Insecure: true, 87 | KeystorePath: keystore.AliceKey, 88 | BlockstorePath: "", 89 | FreshStart: true, 90 | Opts: map[string]string{ 91 | "bridge": contracts.BridgeAddress.Hex(), 92 | "erc20Handler": contracts.ERC20HandlerAddress.Hex(), 93 | "erc721Handler": contracts.ERC721HandlerAddress.Hex(), 94 | "genericHandler": contracts.GenericHandlerAddress.Hex(), 95 | "gasLimit": big.NewInt(DefaultGasLimit).String(), 96 | "maxGasPrice": big.NewInt(DefaultGasPrice).String(), 97 | }, 98 | } 99 | sysErr := make(chan error) 100 | chain, err := InitializeChain(cfg, TestLogger, sysErr, nil) 101 | if err != nil { 102 | t.Fatal(err) 103 | } 104 | 105 | r := core.NewRouter(TestLogger) 106 | chain.SetRouter(r) 107 | 108 | err = chain.Start() 109 | if err != nil { 110 | t.Fatal(err) 111 | } 112 | 113 | // Submit some messages 114 | message := msg.NewFungibleTransfer(src, dst, 1, amount, resourceId, recipient.Bytes()) 115 | 116 | for i := 0; i < 5; i++ { 117 | err = chain.listener.router.Send(message) 118 | if err != nil { 119 | t.Fatal(err) 120 | } 121 | 122 | message.DepositNonce++ 123 | } 124 | 125 | time.Sleep(time.Second) 126 | // Cause critical failure for submitting txs 127 | chain.conn.Client().Close() 128 | 129 | // Pull expected error 130 | select { 131 | case err := <-sysErr: 132 | if err.Error() != ErrFatalPolling.Error() && 133 | err.Error() != ErrFatalTx.Error() && 134 | err.Error() != ErrFatalQuery.Error() { 135 | t.Fatalf("Unexpected error: %s", err) 136 | } 137 | case <-time.After(time.Second * 30): 138 | t.Fatal("Test timed out") 139 | } 140 | 141 | // Tell everyone to shutdown 142 | chain.Stop() 143 | } 144 | -------------------------------------------------------------------------------- /chains/ethereum/config.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 ChainSafe Systems 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | package ethereum 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | "math/big" 10 | 11 | "github.com/ChainSafe/ChainBridge/connections/ethereum/egs" 12 | utils "github.com/ChainSafe/ChainBridge/shared/ethereum" 13 | "github.com/ChainSafe/chainbridge-utils/core" 14 | "github.com/ChainSafe/chainbridge-utils/msg" 15 | "github.com/ethereum/go-ethereum/common" 16 | ) 17 | 18 | const DefaultGasLimit = 6721975 19 | const DefaultGasPrice = 20000000000 20 | const DefaultMinGasPrice = 0 21 | const DefaultBlockConfirmations = 10 22 | const DefaultGasMultiplier = 1 23 | 24 | // Chain specific options 25 | var ( 26 | BridgeOpt = "bridge" 27 | Erc20HandlerOpt = "erc20Handler" 28 | Erc721HandlerOpt = "erc721Handler" 29 | GenericHandlerOpt = "genericHandler" 30 | MaxGasPriceOpt = "maxGasPrice" 31 | MinGasPriceOpt = "minGasPrice" 32 | GasLimitOpt = "gasLimit" 33 | GasMultiplier = "gasMultiplier" 34 | HttpOpt = "http" 35 | StartBlockOpt = "startBlock" 36 | BlockConfirmationsOpt = "blockConfirmations" 37 | EGSApiKey = "egsApiKey" 38 | EGSSpeed = "egsSpeed" 39 | ) 40 | 41 | // Config encapsulates all necessary parameters in ethereum compatible forms 42 | type Config struct { 43 | name string // Human-readable chain name 44 | id msg.ChainId // ChainID 45 | endpoint string // url for rpc endpoint 46 | from string // address of key to use 47 | keystorePath string // Location of keyfiles 48 | blockstorePath string 49 | freshStart bool // Disables loading from blockstore at start 50 | bridgeContract common.Address 51 | erc20HandlerContract common.Address 52 | erc721HandlerContract common.Address 53 | genericHandlerContract common.Address 54 | gasLimit *big.Int 55 | maxGasPrice *big.Int 56 | minGasPrice *big.Int 57 | gasMultiplier *big.Float 58 | http bool // Config for type of connection 59 | startBlock *big.Int 60 | blockConfirmations *big.Int 61 | egsApiKey string // API key for ethgasstation to query gas prices 62 | egsSpeed string // The speed which a transaction should be processed: average, fast, fastest. Default: fast 63 | } 64 | 65 | // parseChainConfig uses a core.ChainConfig to construct a corresponding Config 66 | func parseChainConfig(chainCfg *core.ChainConfig) (*Config, error) { 67 | 68 | config := &Config{ 69 | name: chainCfg.Name, 70 | id: chainCfg.Id, 71 | endpoint: chainCfg.Endpoint, 72 | from: chainCfg.From, 73 | keystorePath: chainCfg.KeystorePath, 74 | blockstorePath: chainCfg.BlockstorePath, 75 | freshStart: chainCfg.FreshStart, 76 | bridgeContract: utils.ZeroAddress, 77 | erc20HandlerContract: utils.ZeroAddress, 78 | erc721HandlerContract: utils.ZeroAddress, 79 | genericHandlerContract: utils.ZeroAddress, 80 | gasLimit: big.NewInt(DefaultGasLimit), 81 | maxGasPrice: big.NewInt(DefaultGasPrice), 82 | minGasPrice: big.NewInt(DefaultMinGasPrice), 83 | gasMultiplier: big.NewFloat(DefaultGasMultiplier), 84 | http: false, 85 | startBlock: big.NewInt(0), 86 | blockConfirmations: big.NewInt(0), 87 | egsApiKey: "", 88 | egsSpeed: "", 89 | } 90 | 91 | if contract, ok := chainCfg.Opts[BridgeOpt]; ok && contract != "" { 92 | config.bridgeContract = common.HexToAddress(contract) 93 | delete(chainCfg.Opts, BridgeOpt) 94 | } else { 95 | return nil, fmt.Errorf("must provide opts.bridge field for ethereum config") 96 | } 97 | 98 | if contract, ok := chainCfg.Opts[Erc20HandlerOpt]; ok { 99 | config.erc20HandlerContract = common.HexToAddress(contract) 100 | delete(chainCfg.Opts, Erc20HandlerOpt) 101 | } 102 | 103 | if contract, ok := chainCfg.Opts[Erc721HandlerOpt]; ok { 104 | config.erc721HandlerContract = common.HexToAddress(contract) 105 | delete(chainCfg.Opts, Erc721HandlerOpt) 106 | } 107 | 108 | if contract, ok := chainCfg.Opts[GenericHandlerOpt]; ok { 109 | config.genericHandlerContract = common.HexToAddress(contract) 110 | delete(chainCfg.Opts, GenericHandlerOpt) 111 | } 112 | 113 | if gasPrice, ok := chainCfg.Opts[MaxGasPriceOpt]; ok { 114 | price, parseErr := utils.ParseUint256OrHex(&gasPrice) 115 | if parseErr != nil { 116 | return nil, fmt.Errorf("unable to parse max gas price, %w", parseErr) 117 | } 118 | 119 | config.maxGasPrice = price 120 | delete(chainCfg.Opts, MaxGasPriceOpt) 121 | } 122 | 123 | if minGasPrice, ok := chainCfg.Opts[MinGasPriceOpt]; ok { 124 | price, parseErr := utils.ParseUint256OrHex(&minGasPrice) 125 | if parseErr != nil { 126 | return nil, fmt.Errorf("unable to parse min gas price, %w", parseErr) 127 | } 128 | 129 | config.minGasPrice = price 130 | delete(chainCfg.Opts, MinGasPriceOpt) 131 | } 132 | 133 | if gasLimit, ok := chainCfg.Opts[GasLimitOpt]; ok { 134 | limit, parseErr := utils.ParseUint256OrHex(&gasLimit) 135 | if parseErr != nil { 136 | return nil, fmt.Errorf("unable to parse gas limit, %w", parseErr) 137 | } 138 | 139 | config.gasLimit = limit 140 | delete(chainCfg.Opts, GasLimitOpt) 141 | } 142 | 143 | if gasMultiplier, ok := chainCfg.Opts[GasMultiplier]; ok { 144 | multilier := big.NewFloat(1) 145 | _, pass := multilier.SetString(gasMultiplier) 146 | if pass { 147 | config.gasMultiplier = multilier 148 | delete(chainCfg.Opts, GasMultiplier) 149 | } else { 150 | return nil, errors.New("unable to parse gasMultiplier to float") 151 | } 152 | } 153 | 154 | if HTTP, ok := chainCfg.Opts[HttpOpt]; ok && HTTP == "true" { 155 | config.http = true 156 | delete(chainCfg.Opts, HttpOpt) 157 | } else if HTTP, ok := chainCfg.Opts[HttpOpt]; ok && HTTP == "false" { 158 | config.http = false 159 | delete(chainCfg.Opts, HttpOpt) 160 | } 161 | 162 | if startBlock, ok := chainCfg.Opts[StartBlockOpt]; ok && startBlock != "" { 163 | block := big.NewInt(0) 164 | _, pass := block.SetString(startBlock, 10) 165 | if pass { 166 | config.startBlock = block 167 | delete(chainCfg.Opts, StartBlockOpt) 168 | } else { 169 | return nil, fmt.Errorf("unable to parse %s", StartBlockOpt) 170 | } 171 | } 172 | 173 | if blockConfirmations, ok := chainCfg.Opts[BlockConfirmationsOpt]; ok && blockConfirmations != "" { 174 | val := big.NewInt(DefaultBlockConfirmations) 175 | _, pass := val.SetString(blockConfirmations, 10) 176 | if pass { 177 | config.blockConfirmations = val 178 | delete(chainCfg.Opts, BlockConfirmationsOpt) 179 | } else { 180 | return nil, fmt.Errorf("unable to parse %s", BlockConfirmationsOpt) 181 | } 182 | } else { 183 | config.blockConfirmations = big.NewInt(DefaultBlockConfirmations) 184 | delete(chainCfg.Opts, BlockConfirmationsOpt) 185 | } 186 | 187 | if gsnApiKey, ok := chainCfg.Opts[EGSApiKey]; ok && gsnApiKey != "" { 188 | config.egsApiKey = gsnApiKey 189 | delete(chainCfg.Opts, EGSApiKey) 190 | } 191 | 192 | if speed, ok := chainCfg.Opts[EGSSpeed]; ok && speed == egs.Average || speed == egs.Fast || speed == egs.Fastest { 193 | config.egsSpeed = speed 194 | delete(chainCfg.Opts, EGSSpeed) 195 | } else { 196 | // Default to "fast" 197 | config.egsSpeed = egs.Fast 198 | delete(chainCfg.Opts, EGSSpeed) 199 | } 200 | 201 | if len(chainCfg.Opts) != 0 { 202 | return nil, fmt.Errorf("unknown Opts Encountered: %#v", chainCfg.Opts) 203 | } 204 | 205 | return config, nil 206 | } 207 | -------------------------------------------------------------------------------- /chains/ethereum/events.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 ChainSafe Systems 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | package ethereum 5 | 6 | import ( 7 | "github.com/ChainSafe/chainbridge-utils/msg" 8 | "github.com/ethereum/go-ethereum/accounts/abi/bind" 9 | ) 10 | 11 | func (l *listener) handleErc20DepositedEvent(destId msg.ChainId, nonce msg.Nonce) (msg.Message, error) { 12 | l.log.Info("Handling fungible deposit event", "dest", destId, "nonce", nonce) 13 | 14 | record, err := l.erc20HandlerContract.GetDepositRecord(&bind.CallOpts{From: l.conn.Keypair().CommonAddress()}, uint64(nonce), uint8(destId)) 15 | if err != nil { 16 | l.log.Error("Error Unpacking ERC20 Deposit Record", "err", err) 17 | return msg.Message{}, err 18 | } 19 | 20 | return msg.NewFungibleTransfer( 21 | l.cfg.id, 22 | destId, 23 | nonce, 24 | record.Amount, 25 | record.ResourceID, 26 | record.DestinationRecipientAddress, 27 | ), nil 28 | } 29 | 30 | func (l *listener) handleErc721DepositedEvent(destId msg.ChainId, nonce msg.Nonce) (msg.Message, error) { 31 | l.log.Info("Handling nonfungible deposit event") 32 | 33 | record, err := l.erc721HandlerContract.GetDepositRecord(&bind.CallOpts{From: l.conn.Keypair().CommonAddress()}, uint64(nonce), uint8(destId)) 34 | if err != nil { 35 | l.log.Error("Error Unpacking ERC721 Deposit Record", "err", err) 36 | return msg.Message{}, err 37 | } 38 | 39 | return msg.NewNonFungibleTransfer( 40 | l.cfg.id, 41 | destId, 42 | nonce, 43 | record.ResourceID, 44 | record.TokenID, 45 | record.DestinationRecipientAddress, 46 | record.MetaData, 47 | ), nil 48 | } 49 | 50 | func (l *listener) handleGenericDepositedEvent(destId msg.ChainId, nonce msg.Nonce) (msg.Message, error) { 51 | l.log.Info("Handling generic deposit event") 52 | 53 | record, err := l.genericHandlerContract.GetDepositRecord(&bind.CallOpts{From: l.conn.Keypair().CommonAddress()}, uint64(nonce), uint8(destId)) 54 | if err != nil { 55 | l.log.Error("Error Unpacking Generic Deposit Record", "err", err) 56 | return msg.Message{}, nil 57 | } 58 | 59 | return msg.NewGenericTransfer( 60 | l.cfg.id, 61 | destId, 62 | nonce, 63 | record.ResourceID, 64 | record.MetaData[:], 65 | ), nil 66 | } 67 | -------------------------------------------------------------------------------- /chains/ethereum/proposal_data.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 ChainSafe Systems 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | package ethereum 5 | 6 | import ( 7 | "math/big" 8 | 9 | "github.com/ethereum/go-ethereum/common" 10 | "github.com/ethereum/go-ethereum/common/math" 11 | ) 12 | 13 | // constructErc20ProposalData returns the bytes to construct a proposal suitable for Erc20 14 | func ConstructErc20ProposalData(amount []byte, recipient []byte) []byte { 15 | var data []byte 16 | data = append(data, common.LeftPadBytes(amount, 32)...) // amount (uint256) 17 | 18 | recipientLen := big.NewInt(int64(len(recipient))).Bytes() 19 | data = append(data, common.LeftPadBytes(recipientLen, 32)...) // length of recipient (uint256) 20 | data = append(data, recipient...) // recipient ([]byte) 21 | return data 22 | } 23 | 24 | // constructErc721ProposalData returns the bytes to construct a proposal suitable for Erc721 25 | func ConstructErc721ProposalData(tokenId []byte, recipient []byte, metadata []byte) []byte { 26 | var data []byte 27 | data = append(data, common.LeftPadBytes(tokenId, 32)...) // tokenId ([]byte) 28 | 29 | recipientLen := big.NewInt(int64(len(recipient))).Bytes() 30 | data = append(data, common.LeftPadBytes(recipientLen, 32)...) // length of recipient 31 | data = append(data, recipient...) // recipient ([]byte) 32 | 33 | metadataLen := big.NewInt(int64(len(metadata))).Bytes() 34 | data = append(data, common.LeftPadBytes(metadataLen, 32)...) // length of metadata (uint256) 35 | data = append(data, metadata...) // metadata ([]byte) 36 | return data 37 | } 38 | 39 | // constructGenericProposalData returns the bytes to construct a generic proposal 40 | func ConstructGenericProposalData(metadata []byte) []byte { 41 | var data []byte 42 | 43 | metadataLen := big.NewInt(int64(len(metadata))) 44 | data = append(data, math.PaddedBigBytes(metadataLen, 32)...) // length of metadata (uint256) 45 | data = append(data, metadata...) // metadata ([]byte) 46 | return data 47 | } 48 | -------------------------------------------------------------------------------- /chains/ethereum/test_utils_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 ChainSafe Systems 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | package ethereum 5 | 6 | import ( 7 | "fmt" 8 | "math/big" 9 | "testing" 10 | "time" 11 | 12 | "github.com/ChainSafe/ChainBridge/bindings/Bridge" 13 | connection "github.com/ChainSafe/ChainBridge/connections/ethereum" 14 | utils "github.com/ChainSafe/ChainBridge/shared/ethereum" 15 | "github.com/ChainSafe/chainbridge-utils/keystore" 16 | "github.com/ChainSafe/chainbridge-utils/msg" 17 | "github.com/ChainSafe/log15" 18 | "github.com/ethereum/go-ethereum/common" 19 | ) 20 | 21 | const TestEndpoint = "ws://localhost:8545" 22 | 23 | var TestLogger = newTestLogger("test") 24 | var TestTimeout = time.Second * 30 25 | 26 | var AliceKp = keystore.TestKeyRing.EthereumKeys[keystore.AliceKey] 27 | var BobKp = keystore.TestKeyRing.EthereumKeys[keystore.BobKey] 28 | 29 | var TestRelayerThreshold = big.NewInt(2) 30 | var TestChainId = msg.ChainId(0) 31 | 32 | var aliceTestConfig = createConfig("alice", nil, nil) 33 | 34 | func createConfig(name string, startBlock *big.Int, contracts *utils.DeployedContracts) *Config { 35 | cfg := &Config{ 36 | name: name, 37 | id: 0, 38 | endpoint: TestEndpoint, 39 | from: name, 40 | keystorePath: "", 41 | blockstorePath: "", 42 | freshStart: true, 43 | bridgeContract: common.Address{}, 44 | erc20HandlerContract: common.Address{}, 45 | erc721HandlerContract: common.Address{}, 46 | genericHandlerContract: common.Address{}, 47 | gasLimit: big.NewInt(DefaultGasLimit), 48 | maxGasPrice: big.NewInt(DefaultGasPrice), 49 | gasMultiplier: big.NewFloat(DefaultGasMultiplier), 50 | http: false, 51 | startBlock: startBlock, 52 | blockConfirmations: big.NewInt(3), 53 | } 54 | 55 | if contracts != nil { 56 | cfg.bridgeContract = contracts.BridgeAddress 57 | cfg.erc20HandlerContract = contracts.ERC20HandlerAddress 58 | cfg.erc721HandlerContract = contracts.ERC721HandlerAddress 59 | cfg.genericHandlerContract = contracts.GenericHandlerAddress 60 | } 61 | 62 | return cfg 63 | } 64 | 65 | func newTestLogger(name string) log15.Logger { 66 | tLog := log15.New("chain", name) 67 | tLog.SetHandler(log15.LvlFilterHandler(log15.LvlError, tLog.GetHandler())) 68 | return tLog 69 | } 70 | 71 | func newLocalConnection(t *testing.T, cfg *Config) *connection.Connection { 72 | kp := keystore.TestKeyRing.EthereumKeys[cfg.from] 73 | conn := connection.NewConnection(TestEndpoint, false, kp, TestLogger, big.NewInt(DefaultGasLimit), big.NewInt(DefaultGasPrice), big.NewInt(DefaultMinGasPrice), big.NewFloat(DefaultGasMultiplier), "", "") 74 | err := conn.Connect() 75 | if err != nil { 76 | t.Fatal(err) 77 | } 78 | 79 | return conn 80 | } 81 | 82 | func deployTestContracts(t *testing.T, client *utils.Client, id msg.ChainId) *utils.DeployedContracts { 83 | contracts, err := utils.DeployContracts( 84 | client, 85 | uint8(id), 86 | TestRelayerThreshold, 87 | ) 88 | if err != nil { 89 | t.Fatal(err) 90 | } 91 | 92 | fmt.Println("=======================================================") 93 | fmt.Printf("Bridge: %s\n", contracts.BridgeAddress.Hex()) 94 | fmt.Printf("Erc20Handler: %s\n", contracts.ERC20HandlerAddress.Hex()) 95 | fmt.Printf("ERC721Handler: %s\n", contracts.ERC721HandlerAddress.Hex()) 96 | fmt.Printf("GenericHandler: %s\n", contracts.GenericHandlerAddress.Hex()) 97 | fmt.Println("========================================================") 98 | 99 | return contracts 100 | } 101 | 102 | func createErc20Deposit( 103 | t *testing.T, 104 | contract *Bridge.Bridge, 105 | client *utils.Client, 106 | rId msg.ResourceId, 107 | destRecipient common.Address, 108 | destId msg.ChainId, 109 | amount *big.Int, 110 | ) { 111 | 112 | data := utils.ConstructErc20DepositData(destRecipient.Bytes(), amount) 113 | 114 | // Incrememnt Nonce by one 115 | client.Opts.Nonce = client.Opts.Nonce.Add(client.Opts.Nonce, big.NewInt(1)) 116 | if _, err := contract.Deposit( 117 | client.Opts, 118 | uint8(destId), 119 | rId, 120 | data, 121 | ); err != nil { 122 | t.Fatal(err) 123 | } 124 | } 125 | 126 | func createErc721Deposit( 127 | t *testing.T, 128 | bridge *Bridge.Bridge, 129 | client *utils.Client, 130 | rId msg.ResourceId, 131 | destRecipient common.Address, 132 | destId msg.ChainId, 133 | tokenId *big.Int, 134 | ) { 135 | 136 | data := utils.ConstructErc721DepositData(tokenId, destRecipient.Bytes()) 137 | 138 | // Incrememnt Nonce by one 139 | client.Opts.Nonce = client.Opts.Nonce.Add(client.Opts.Nonce, big.NewInt(1)) 140 | if _, err := bridge.Deposit( 141 | client.Opts, 142 | uint8(destId), 143 | rId, 144 | data, 145 | ); err != nil { 146 | t.Fatal(err) 147 | } 148 | } 149 | 150 | func createGenericDeposit( 151 | t *testing.T, 152 | bridge *Bridge.Bridge, 153 | client *utils.Client, 154 | rId msg.ResourceId, 155 | destId msg.ChainId, 156 | hash []byte) { 157 | 158 | data := utils.ConstructGenericDepositData(hash) 159 | 160 | // Incrememnt Nonce by one 161 | client.Opts.Nonce = client.Opts.Nonce.Add(client.Opts.Nonce, big.NewInt(1)) 162 | if _, err := bridge.Deposit( 163 | client.Opts, 164 | uint8(destId), 165 | rId, 166 | data, 167 | ); err != nil { 168 | t.Fatal(err) 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /chains/ethereum/writer.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 ChainSafe Systems 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | package ethereum 5 | 6 | import ( 7 | "github.com/ChainSafe/ChainBridge/bindings/Bridge" 8 | "github.com/ChainSafe/chainbridge-utils/core" 9 | metrics "github.com/ChainSafe/chainbridge-utils/metrics/types" 10 | "github.com/ChainSafe/chainbridge-utils/msg" 11 | "github.com/ChainSafe/log15" 12 | ) 13 | 14 | var _ core.Writer = &writer{} 15 | 16 | // https://github.com/ChainSafe/chainbridge-solidity/blob/b5ed13d9798feb7c340e737a726dd415b8815366/contracts/Bridge.sol#L20 17 | var PassedStatus uint8 = 2 18 | var TransferredStatus uint8 = 3 19 | var CancelledStatus uint8 = 4 20 | 21 | type writer struct { 22 | cfg Config 23 | conn Connection 24 | bridgeContract *Bridge.Bridge // instance of bound receiver bridgeContract 25 | log log15.Logger 26 | stop <-chan int 27 | sysErr chan<- error // Reports fatal error to core 28 | metrics *metrics.ChainMetrics 29 | } 30 | 31 | // NewWriter creates and returns writer 32 | func NewWriter(conn Connection, cfg *Config, log log15.Logger, stop <-chan int, sysErr chan<- error, m *metrics.ChainMetrics) *writer { 33 | return &writer{ 34 | cfg: *cfg, 35 | conn: conn, 36 | log: log, 37 | stop: stop, 38 | sysErr: sysErr, 39 | metrics: m, 40 | } 41 | } 42 | 43 | func (w *writer) start() error { 44 | w.log.Debug("Starting ethereum writer...") 45 | return nil 46 | } 47 | 48 | // setContract adds the bound receiver bridgeContract to the writer 49 | func (w *writer) setContract(bridge *Bridge.Bridge) { 50 | w.bridgeContract = bridge 51 | } 52 | 53 | // ResolveMessage handles any given message based on type 54 | // A bool is returned to indicate failure/success, this should be ignored except for within tests. 55 | func (w *writer) ResolveMessage(m msg.Message) bool { 56 | w.log.Info("Attempting to resolve message", "type", m.Type, "src", m.Source, "dst", m.Destination, "nonce", m.DepositNonce, "rId", m.ResourceId.Hex()) 57 | 58 | switch m.Type { 59 | case msg.FungibleTransfer: 60 | return w.createErc20Proposal(m) 61 | case msg.NonFungibleTransfer: 62 | return w.createErc721Proposal(m) 63 | case msg.GenericTransfer: 64 | return w.createGenericDepositProposal(m) 65 | default: 66 | w.log.Error("Unknown message type received", "type", m.Type) 67 | return false 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /chains/interfaces.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 ChainSafe Systems 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | package chains 5 | 6 | import ( 7 | "github.com/ChainSafe/chainbridge-utils/msg" 8 | ) 9 | 10 | type Router interface { 11 | Send(message msg.Message) error 12 | } 13 | 14 | //type Writer interface { 15 | // ResolveMessage(message msg.Message) bool 16 | //} 17 | -------------------------------------------------------------------------------- /chains/substrate/chain.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 ChainSafe Systems 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | /* 5 | The substrate package contains the logic for interacting with substrate chains. 6 | The current supported transfer types are Fungible, Nonfungible, and generic. 7 | 8 | There are 3 major components: the connection, the listener, and the writer. 9 | 10 | Connection 11 | 12 | The Connection handles connecting to the substrate client, and submitting transactions to the client. 13 | It also handles state queries. The connection is shared by the writer and listener. 14 | 15 | Listener 16 | 17 | The substrate listener polls blocks and parses the associated events for the three transfer types. It then forwards these into the router. 18 | 19 | Writer 20 | 21 | As the writer receives messages from the router, it constructs proposals. If a proposal is still active, the writer will attempt to vote on it. Resource IDs are resolved to method name on-chain, which are then used in the proposals when constructing the resulting Call struct. 22 | 23 | */ 24 | package substrate 25 | 26 | import ( 27 | "github.com/ChainSafe/chainbridge-utils/blockstore" 28 | "github.com/ChainSafe/chainbridge-utils/core" 29 | "github.com/ChainSafe/chainbridge-utils/crypto/sr25519" 30 | "github.com/ChainSafe/chainbridge-utils/keystore" 31 | metrics "github.com/ChainSafe/chainbridge-utils/metrics/types" 32 | "github.com/ChainSafe/chainbridge-utils/msg" 33 | "github.com/ChainSafe/log15" 34 | ) 35 | 36 | var _ core.Chain = &Chain{} 37 | 38 | type Chain struct { 39 | cfg *core.ChainConfig // The config of the chain 40 | conn *Connection // THe chains connection 41 | listener *listener // The listener of this chain 42 | writer *writer // The writer of the chain 43 | stop chan<- int 44 | } 45 | 46 | // checkBlockstore queries the blockstore for the latest known block. If the latest block is 47 | // greater than startBlock, then the latest block is returned, otherwise startBlock is. 48 | func checkBlockstore(bs *blockstore.Blockstore, startBlock uint64) (uint64, error) { 49 | latestBlock, err := bs.TryLoadLatestBlock() 50 | if err != nil { 51 | return 0, err 52 | } 53 | 54 | if latestBlock.Uint64() > startBlock { 55 | return latestBlock.Uint64(), nil 56 | } else { 57 | return startBlock, nil 58 | } 59 | } 60 | 61 | func InitializeChain(cfg *core.ChainConfig, logger log15.Logger, sysErr chan<- error, m *metrics.ChainMetrics) (*Chain, error) { 62 | kp, err := keystore.KeypairFromAddress(cfg.From, keystore.SubChain, cfg.KeystorePath, cfg.Insecure) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | krp := kp.(*sr25519.Keypair).AsKeyringPair() 68 | 69 | // Attempt to load latest block 70 | bs, err := blockstore.NewBlockstore(cfg.BlockstorePath, cfg.Id, kp.Address()) 71 | if err != nil { 72 | return nil, err 73 | } 74 | startBlock := parseStartBlock(cfg) 75 | if !cfg.FreshStart { 76 | startBlock, err = checkBlockstore(bs, startBlock) 77 | if err != nil { 78 | return nil, err 79 | } 80 | } 81 | 82 | stop := make(chan int) 83 | // Setup connection 84 | conn := NewConnection(cfg.Endpoint, cfg.Name, krp, logger, stop, sysErr) 85 | err = conn.Connect() 86 | if err != nil { 87 | return nil, err 88 | } 89 | 90 | err = conn.checkChainId(cfg.Id) 91 | if err != nil { 92 | return nil, err 93 | } 94 | 95 | if cfg.LatestBlock { 96 | curr, err := conn.api.RPC.Chain.GetHeaderLatest() 97 | if err != nil { 98 | return nil, err 99 | } 100 | startBlock = uint64(curr.Number) 101 | } 102 | 103 | ue := parseUseExtended(cfg) 104 | 105 | // Setup listener & writer 106 | l := NewListener(conn, cfg.Name, cfg.Id, startBlock, logger, bs, stop, sysErr, m) 107 | w := NewWriter(conn, logger, sysErr, m, ue) 108 | return &Chain{ 109 | cfg: cfg, 110 | conn: conn, 111 | listener: l, 112 | writer: w, 113 | stop: stop, 114 | }, nil 115 | } 116 | 117 | func (c *Chain) Start() error { 118 | err := c.listener.start() 119 | if err != nil { 120 | return err 121 | } 122 | c.conn.log.Debug("Successfully started chain", "chainId", c.cfg.Id) 123 | return nil 124 | } 125 | 126 | func (c *Chain) SetRouter(r *core.Router) { 127 | r.Listen(c.cfg.Id, c.writer) 128 | c.listener.setRouter(r) 129 | } 130 | 131 | func (c *Chain) LatestBlock() metrics.LatestBlock { 132 | return c.listener.latestBlock 133 | } 134 | 135 | func (c *Chain) Id() msg.ChainId { 136 | return c.cfg.Id 137 | } 138 | 139 | func (c *Chain) Name() string { 140 | return c.cfg.Name 141 | } 142 | 143 | func (c *Chain) Stop() { 144 | close(c.stop) 145 | } 146 | -------------------------------------------------------------------------------- /chains/substrate/config.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 ChainSafe Systems 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | package substrate 5 | 6 | import ( 7 | "strconv" 8 | 9 | "github.com/ChainSafe/chainbridge-utils/core" 10 | ) 11 | 12 | func parseStartBlock(cfg *core.ChainConfig) uint64 { 13 | if blk, ok := cfg.Opts["startBlock"]; ok { 14 | res, err := strconv.ParseUint(blk, 10, 32) 15 | if err != nil { 16 | panic(err) 17 | } 18 | return res 19 | } 20 | return 0 21 | } 22 | 23 | func parseUseExtended(cfg *core.ChainConfig) bool { 24 | if b, ok := cfg.Opts["useExtendedCall"]; ok { 25 | res, err := strconv.ParseBool(b) 26 | if err != nil { 27 | panic(err) 28 | } 29 | return res 30 | } 31 | return false 32 | } 33 | -------------------------------------------------------------------------------- /chains/substrate/config_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 ChainSafe Systems 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | package substrate 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/ChainSafe/chainbridge-utils/core" 10 | ) 11 | 12 | func TestParseStartBlock(t *testing.T) { 13 | // Valid option included in config 14 | cfg := &core.ChainConfig{Opts: map[string]string{"startBlock": "1000"}} 15 | 16 | blk := parseStartBlock(cfg) 17 | 18 | if blk != 1000 { 19 | t.Fatalf("Got: %d Expected: %d", blk, 1000) 20 | } 21 | 22 | // Not included in config 23 | cfg = &core.ChainConfig{Opts: map[string]string{}} 24 | 25 | blk = parseStartBlock(cfg) 26 | 27 | if blk != 0 { 28 | t.Fatalf("Got: %d Expected: %d", blk, 0) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /chains/substrate/connection.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 ChainSafe Systems 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | package substrate 5 | 6 | import ( 7 | "fmt" 8 | "sync" 9 | 10 | utils "github.com/ChainSafe/ChainBridge/shared/substrate" 11 | "github.com/ChainSafe/chainbridge-utils/msg" 12 | "github.com/ChainSafe/log15" 13 | gsrpc "github.com/centrifuge/go-substrate-rpc-client" 14 | "github.com/centrifuge/go-substrate-rpc-client/rpc/author" 15 | "github.com/centrifuge/go-substrate-rpc-client/signature" 16 | "github.com/centrifuge/go-substrate-rpc-client/types" 17 | ) 18 | 19 | type Connection struct { 20 | api *gsrpc.SubstrateAPI 21 | log log15.Logger 22 | url string // API endpoint 23 | name string // Chain name 24 | meta types.Metadata // Latest chain metadata 25 | metaLock sync.RWMutex // Lock metadata for updates, allows concurrent reads 26 | genesisHash types.Hash // Chain genesis hash 27 | key *signature.KeyringPair // Keyring used for signing 28 | nonce types.U32 // Latest account nonce 29 | nonceLock sync.Mutex // Locks nonce for updates 30 | stop <-chan int // Signals system shutdown, should be observed in all selects and loops 31 | sysErr chan<- error // Propagates fatal errors to core 32 | } 33 | 34 | func NewConnection(url string, name string, key *signature.KeyringPair, log log15.Logger, stop <-chan int, sysErr chan<- error) *Connection { 35 | return &Connection{url: url, name: name, key: key, log: log, stop: stop, sysErr: sysErr} 36 | } 37 | 38 | func (c *Connection) getMetadata() (meta types.Metadata) { 39 | c.metaLock.RLock() 40 | meta = c.meta 41 | c.metaLock.RUnlock() 42 | return meta 43 | } 44 | 45 | func (c *Connection) updateMetatdata() error { 46 | c.metaLock.Lock() 47 | meta, err := c.api.RPC.State.GetMetadataLatest() 48 | if err != nil { 49 | c.metaLock.Unlock() 50 | return err 51 | } 52 | c.meta = *meta 53 | c.metaLock.Unlock() 54 | return nil 55 | } 56 | 57 | func (c *Connection) Connect() error { 58 | c.log.Info("Connecting to substrate chain...", "url", c.url) 59 | api, err := gsrpc.NewSubstrateAPI(c.url) 60 | if err != nil { 61 | return err 62 | } 63 | c.api = api 64 | 65 | // Fetch metadata 66 | meta, err := api.RPC.State.GetMetadataLatest() 67 | if err != nil { 68 | return err 69 | } 70 | c.meta = *meta 71 | c.log.Debug("Fetched substrate metadata") 72 | 73 | // Fetch genesis hash 74 | genesisHash, err := c.api.RPC.Chain.GetBlockHash(0) 75 | if err != nil { 76 | return err 77 | } 78 | c.genesisHash = genesisHash 79 | c.log.Debug("Fetched substrate genesis hash", "hash", genesisHash.Hex()) 80 | return nil 81 | } 82 | 83 | // SubmitTx constructs and submits an extrinsic to call the method with the given arguments. 84 | // All args are passed directly into GSRPC. GSRPC types are recommended to avoid serialization inconsistencies. 85 | func (c *Connection) SubmitTx(method utils.Method, args ...interface{}) error { 86 | c.log.Debug("Submitting substrate call...", "method", method, "sender", c.key.Address) 87 | 88 | meta := c.getMetadata() 89 | 90 | // Create call and extrinsic 91 | call, err := types.NewCall( 92 | &meta, 93 | string(method), 94 | args..., 95 | ) 96 | if err != nil { 97 | return fmt.Errorf("failed to construct call: %w", err) 98 | } 99 | ext := types.NewExtrinsic(call) 100 | 101 | // Get latest runtime version 102 | rv, err := c.api.RPC.State.GetRuntimeVersionLatest() 103 | if err != nil { 104 | return err 105 | } 106 | 107 | c.nonceLock.Lock() 108 | latestNonce, err := c.getLatestNonce() 109 | if err != nil { 110 | c.nonceLock.Unlock() 111 | return err 112 | } 113 | if latestNonce > c.nonce { 114 | c.nonce = latestNonce 115 | } 116 | 117 | // Sign the extrinsic 118 | o := types.SignatureOptions{ 119 | BlockHash: c.genesisHash, 120 | Era: types.ExtrinsicEra{IsMortalEra: false}, 121 | GenesisHash: c.genesisHash, 122 | Nonce: types.NewUCompactFromUInt(uint64(c.nonce)), 123 | SpecVersion: rv.SpecVersion, 124 | Tip: types.NewUCompactFromUInt(0), 125 | TransactionVersion: rv.TransactionVersion, 126 | } 127 | 128 | err = ext.Sign(*c.key, o) 129 | if err != nil { 130 | c.nonceLock.Unlock() 131 | return err 132 | } 133 | 134 | // Submit and watch the extrinsic 135 | sub, err := c.api.RPC.Author.SubmitAndWatchExtrinsic(ext) 136 | c.nonce++ 137 | c.nonceLock.Unlock() 138 | if err != nil { 139 | return fmt.Errorf("submission of extrinsic failed: %w", err) 140 | } 141 | c.log.Trace("Extrinsic submission succeeded") 142 | defer sub.Unsubscribe() 143 | 144 | return c.watchSubmission(sub) 145 | } 146 | 147 | func (c *Connection) watchSubmission(sub *author.ExtrinsicStatusSubscription) error { 148 | for { 149 | select { 150 | case <-c.stop: 151 | return TerminatedError 152 | case status := <-sub.Chan(): 153 | switch { 154 | case status.IsInBlock: 155 | c.log.Trace("Extrinsic included in block", "block", status.AsInBlock.Hex()) 156 | return nil 157 | case status.IsRetracted: 158 | return fmt.Errorf("extrinsic retracted: %s", status.AsRetracted.Hex()) 159 | case status.IsDropped: 160 | return fmt.Errorf("extrinsic dropped from network") 161 | case status.IsInvalid: 162 | return fmt.Errorf("extrinsic invalid") 163 | } 164 | case err := <-sub.Err(): 165 | c.log.Trace("Extrinsic subscription error", "err", err) 166 | return err 167 | } 168 | } 169 | } 170 | 171 | // queryStorage performs a storage lookup. Arguments may be nil, result must be a pointer. 172 | func (c *Connection) queryStorage(prefix, method string, arg1, arg2 []byte, result interface{}) (bool, error) { 173 | // Fetch account nonce 174 | data := c.getMetadata() 175 | key, err := types.CreateStorageKey(&data, prefix, method, arg1, arg2) 176 | if err != nil { 177 | return false, err 178 | } 179 | return c.api.RPC.State.GetStorageLatest(key, result) 180 | } 181 | 182 | // TODO: Add this to GSRPC 183 | func getConst(meta *types.Metadata, prefix, name string, res interface{}) error { 184 | for _, mod := range meta.AsMetadataV12.Modules { 185 | if string(mod.Name) == prefix { 186 | for _, cons := range mod.Constants { 187 | if string(cons.Name) == name { 188 | return types.DecodeFromBytes(cons.Value, res) 189 | } 190 | } 191 | } 192 | } 193 | return fmt.Errorf("could not find constant %s.%s", prefix, name) 194 | } 195 | 196 | func (c *Connection) getConst(prefix, name string, res interface{}) error { 197 | meta := c.getMetadata() 198 | return getConst(&meta, prefix, name, res) 199 | } 200 | 201 | func (c *Connection) checkChainId(expected msg.ChainId) error { 202 | var actual msg.ChainId 203 | err := c.getConst(utils.BridgePalletName, "ChainIdentity", &actual) 204 | if err != nil { 205 | return err 206 | } 207 | 208 | if actual != expected { 209 | return fmt.Errorf("ChainID is incorrect, Expected chainId: %d, got chainId: %d", expected, actual) 210 | } 211 | 212 | return nil 213 | } 214 | 215 | func (c *Connection) getLatestNonce() (types.U32, error) { 216 | var acct types.AccountInfo 217 | exists, err := c.queryStorage("System", "Account", c.key.PublicKey, nil, &acct) 218 | if err != nil { 219 | return 0, err 220 | } 221 | if !exists { 222 | return 0, nil 223 | } 224 | 225 | return acct.Nonce, nil 226 | } 227 | func (c *Connection) Close() { 228 | // TODO: Anything required to shutdown GRPC? 229 | } 230 | -------------------------------------------------------------------------------- /chains/substrate/connection_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 ChainSafe Systems 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | package substrate 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/centrifuge/go-substrate-rpc-client/types" 10 | ) 11 | 12 | func TestConnect_QueryStorage(t *testing.T) { 13 | // Create connection with Alice key 14 | errs := make(chan error) 15 | conn := NewConnection(TestEndpoint, "Alice", AliceKey, AliceTestLogger, make(chan int), errs) 16 | err := conn.Connect() 17 | if err != nil { 18 | t.Fatal(err) 19 | } 20 | defer conn.Close() 21 | 22 | // Query storage 23 | var data types.AccountInfo 24 | _, err = conn.queryStorage("System", "Account", conn.key.PublicKey, nil, &data) 25 | if err != nil { 26 | t.Fatal(err) 27 | } 28 | 29 | // Ensure no errors were propagated 30 | select { 31 | case err := <-errs: 32 | t.Fatal(err) 33 | default: 34 | return 35 | } 36 | } 37 | 38 | func TestConnect_CheckChainId(t *testing.T) { 39 | // Create connection with Alice key 40 | errs := make(chan error) 41 | conn := NewConnection(TestEndpoint, "Alice", AliceKey, AliceTestLogger, make(chan int), errs) 42 | err := conn.Connect() 43 | if err != nil { 44 | t.Fatal(err) 45 | } 46 | defer conn.Close() 47 | 48 | err = conn.checkChainId(ThisChain) 49 | if err != nil { 50 | t.Fatal(err) 51 | } 52 | 53 | // Ensure no errors were propagated 54 | select { 55 | case err := <-errs: 56 | t.Fatal(err) 57 | default: 58 | return 59 | } 60 | } 61 | 62 | func TestConnect_SubmitTx(t *testing.T) { 63 | // Create connection with Alice key 64 | errs := make(chan error) 65 | conn := NewConnection(TestEndpoint, "Alice", AliceKey, AliceTestLogger, make(chan int), errs) 66 | err := conn.Connect() 67 | if err != nil { 68 | t.Fatal(err) 69 | } 70 | defer conn.Close() 71 | 72 | // Source: https://pkg.go.dev/github.com/centrifuge/go-substrate-rpc-client?tab=doc#example-package-MakeASimpleTransfer 73 | bob, err := types.NewAddressFromHexAccountID("0x8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48") 74 | if err != nil { 75 | t.Fatal(err) 76 | } 77 | err = conn.SubmitTx("Balances.transfer", bob.AsAccountID, types.NewUCompactFromUInt(10)) 78 | if err != nil { 79 | t.Fatal(err) 80 | } 81 | 82 | // Ensure no errors were propagated 83 | select { 84 | case err := <-errs: 85 | t.Fatal(err) 86 | default: 87 | return 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /chains/substrate/events.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 ChainSafe Systems 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | package substrate 5 | 6 | import ( 7 | "fmt" 8 | "math/big" 9 | 10 | events "github.com/ChainSafe/chainbridge-substrate-events" 11 | "github.com/ChainSafe/chainbridge-utils/msg" 12 | "github.com/ChainSafe/log15" 13 | ) 14 | 15 | type eventName string 16 | type eventHandler func(interface{}, log15.Logger) (msg.Message, error) 17 | 18 | const FungibleTransfer eventName = "FungibleTransfer" 19 | const NonFungibleTransfer eventName = "NonFungibleTransfer" 20 | const GenericTransfer eventName = "GenericTransfer" 21 | 22 | var Subscriptions = []struct { 23 | name eventName 24 | handler eventHandler 25 | }{ 26 | {FungibleTransfer, fungibleTransferHandler}, 27 | {NonFungibleTransfer, nonFungibleTransferHandler}, 28 | {GenericTransfer, genericTransferHandler}, 29 | } 30 | 31 | func fungibleTransferHandler(evtI interface{}, log log15.Logger) (msg.Message, error) { 32 | evt, ok := evtI.(events.EventFungibleTransfer) 33 | if !ok { 34 | return msg.Message{}, fmt.Errorf("failed to cast EventFungibleTransfer type") 35 | } 36 | 37 | resourceId := msg.ResourceId(evt.ResourceId) 38 | log.Info("Got fungible transfer event!", "destination", evt.Destination, "resourceId", resourceId.Hex(), "amount", evt.Amount) 39 | 40 | return msg.NewFungibleTransfer( 41 | 0, // Unset 42 | msg.ChainId(evt.Destination), 43 | msg.Nonce(evt.DepositNonce), 44 | evt.Amount.Int, 45 | resourceId, 46 | evt.Recipient, 47 | ), nil 48 | } 49 | 50 | func nonFungibleTransferHandler(evtI interface{}, log log15.Logger) (msg.Message, error) { 51 | evt, ok := evtI.(events.EventNonFungibleTransfer) 52 | if !ok { 53 | return msg.Message{}, fmt.Errorf("failed to cast EventNonFungibleTransfer type") 54 | } 55 | 56 | log.Info("Got non-fungible transfer event!", "destination", evt.Destination, "resourceId", evt.ResourceId) 57 | 58 | return msg.NewNonFungibleTransfer( 59 | 0, // Unset 60 | msg.ChainId(evt.Destination), 61 | msg.Nonce(evt.DepositNonce), 62 | msg.ResourceId(evt.ResourceId), 63 | big.NewInt(0).SetBytes(evt.TokenId[:]), 64 | evt.Recipient, 65 | evt.Metadata, 66 | ), nil 67 | } 68 | 69 | func genericTransferHandler(evtI interface{}, log log15.Logger) (msg.Message, error) { 70 | evt, ok := evtI.(events.EventGenericTransfer) 71 | if !ok { 72 | return msg.Message{}, fmt.Errorf("failed to cast EventGenericTransfer type") 73 | } 74 | 75 | log.Info("Got generic transfer event!", "destination", evt.Destination, "resourceId", evt.ResourceId) 76 | 77 | return msg.NewGenericTransfer( 78 | 0, // Unset 79 | msg.ChainId(evt.Destination), 80 | msg.Nonce(evt.DepositNonce), 81 | msg.ResourceId(evt.ResourceId), 82 | evt.Metadata, 83 | ), nil 84 | } 85 | -------------------------------------------------------------------------------- /chains/substrate/listener_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 ChainSafe Systems 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | package substrate 5 | 6 | import ( 7 | "fmt" 8 | "math/big" 9 | "reflect" 10 | "testing" 11 | "time" 12 | 13 | utils "github.com/ChainSafe/ChainBridge/shared/substrate" 14 | subtest "github.com/ChainSafe/ChainBridge/shared/substrate/testing" 15 | "github.com/ChainSafe/chainbridge-utils/blockstore" 16 | "github.com/ChainSafe/chainbridge-utils/msg" 17 | "github.com/centrifuge/go-substrate-rpc-client/types" 18 | ) 19 | 20 | const ListenerTimeout = time.Second * 30 21 | 22 | type mockRouter struct { 23 | msgs chan msg.Message 24 | } 25 | 26 | func (r *mockRouter) Send(message msg.Message) error { 27 | r.msgs <- message 28 | return nil 29 | } 30 | 31 | func newTestListener(client *utils.Client, conn *Connection) (*listener, chan error, *mockRouter, error) { 32 | r := &mockRouter{msgs: make(chan msg.Message)} 33 | 34 | startBlock, err := client.LatestBlock() 35 | if err != nil { 36 | return nil, nil, nil, err 37 | } 38 | 39 | errs := make(chan error) 40 | l := NewListener(conn, "Alice", 1, startBlock, AliceTestLogger, &blockstore.EmptyStore{}, make(chan int), errs, nil) 41 | l.setRouter(r) 42 | err = l.start() 43 | if err != nil { 44 | return nil, nil, nil, err 45 | } 46 | 47 | return l, errs, r, nil 48 | } 49 | 50 | func verifyResultingMessage(t *testing.T, r *mockRouter, sysErr chan error, expected msg.Message) { 51 | // Verify message 52 | select { 53 | case err := <-sysErr: 54 | t.Fatalf("System Error: %s", err) 55 | case m := <-r.msgs: 56 | if err := compareMessage(expected, m); err != nil { 57 | t.Fatal(err) 58 | } 59 | case <-time.After(ListenerTimeout): 60 | t.Fatalf("test timed out") 61 | 62 | } 63 | } 64 | 65 | func compareMessage(expected, actual msg.Message) error { 66 | if !reflect.DeepEqual(expected, actual) { 67 | if !reflect.DeepEqual(expected.Source, actual.Source) { 68 | return fmt.Errorf("Source doesn't match. \n\tExpected: %#v\n\tGot: %#v\n", expected.Source, actual.Source) 69 | } else if !reflect.DeepEqual(expected.Destination, actual.Destination) { 70 | return fmt.Errorf("Destination doesn't match. \n\tExpected: %#v\n\tGot: %#v\n", expected.Destination, actual.Destination) 71 | } else if !reflect.DeepEqual(expected.DepositNonce, actual.DepositNonce) { 72 | return fmt.Errorf("Deposit nonce doesn't match. \n\tExpected: %#v\n\tGot: %#v\n", expected.DepositNonce, actual.DepositNonce) 73 | } else if !reflect.DeepEqual(expected.Payload, actual.Payload) { 74 | return fmt.Errorf("Payload doesn't match. \n\tExpected: %#v\n\tGot: %#v\n", expected.Payload, actual.Payload) 75 | } 76 | } 77 | return nil 78 | } 79 | 80 | func Test_FungibleTransferEvent(t *testing.T) { 81 | // Construct our expected message 82 | var rId msg.ResourceId 83 | subtest.QueryConst(t, context.client, "Example", "NativeTokenId", &rId) 84 | amount := big.NewInt(1000000) 85 | recipient := BobKey.PublicKey 86 | context.latestOutNonce = context.latestOutNonce + 1 87 | expected := msg.NewFungibleTransfer(ThisChain, ForeignChain, context.latestOutNonce, amount, rId, recipient) 88 | 89 | subtest.InitiateNativeTransfer(t, context.client, types.NewU128(*amount), recipient, ForeignChain) 90 | 91 | verifyResultingMessage(t, context.router, context.lSysErr, expected) 92 | } 93 | 94 | func Test_NonFungibleTransferEvent(t *testing.T) { 95 | // First, mint a token to transfer 96 | tokenId := big.NewInt(1212) 97 | metadata := big.NewInt(0x808080808).Bytes() 98 | subtest.MintErc721(t, context.client, tokenId, metadata, context.client.Key) 99 | 100 | // Construct our expected message 101 | var rId msg.ResourceId 102 | subtest.QueryConst(t, context.client, "Example", "Erc721Id", &rId) 103 | recipient := BobKey.PublicKey 104 | context.latestOutNonce = context.latestOutNonce + 1 105 | expected := msg.NewNonFungibleTransfer(ThisChain, ForeignChain, context.latestOutNonce, rId, tokenId, recipient, metadata) 106 | 107 | subtest.InitiateNonFungibleTransfer(t, context.client, types.NewU256(*tokenId), recipient, ForeignChain) 108 | 109 | verifyResultingMessage(t, context.router, context.lSysErr, expected) 110 | } 111 | 112 | func Test_GenericTransferEvent(t *testing.T) { 113 | // Construct our expected message 114 | var rId msg.ResourceId 115 | subtest.QueryConst(t, context.client, "Example", "HashId", &rId) 116 | hashBz := types.MustHexDecodeString("0x16078eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f2") 117 | hash := types.NewHash(hashBz) 118 | context.latestOutNonce = context.latestOutNonce + 1 119 | expected := msg.NewGenericTransfer(ThisChain, ForeignChain, context.latestOutNonce, rId, hash[:]) 120 | 121 | subtest.InitiateHashTransfer(t, context.client, hash, ForeignChain) 122 | 123 | verifyResultingMessage(t, context.router, context.lSysErr, expected) 124 | 125 | // Repeat the process to assert nonce and hash change 126 | 127 | // Construct our expected message 128 | hashBz = types.MustHexDecodeString("0x16078eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f2") 129 | hash = types.NewHash(hashBz) 130 | context.latestOutNonce = context.latestOutNonce + 1 131 | expected = msg.NewGenericTransfer(ThisChain, ForeignChain, context.latestOutNonce, rId, hash[:]) 132 | 133 | subtest.InitiateHashTransfer(t, context.client, hash, ForeignChain) 134 | 135 | verifyResultingMessage(t, context.router, context.lSysErr, expected) 136 | } 137 | -------------------------------------------------------------------------------- /chains/substrate/test_helper_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 ChainSafe Systems 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | package substrate 5 | 6 | import ( 7 | "os" 8 | "testing" 9 | 10 | utils "github.com/ChainSafe/ChainBridge/shared/substrate" 11 | "github.com/ChainSafe/chainbridge-utils/keystore" 12 | "github.com/ChainSafe/chainbridge-utils/msg" 13 | "github.com/ChainSafe/log15" 14 | "github.com/centrifuge/go-substrate-rpc-client/types" 15 | "github.com/ethereum/go-ethereum/common/hexutil" 16 | ) 17 | 18 | const TestEndpoint = "ws://127.0.0.1:9944" 19 | 20 | const TestRelayerThreshold = 2 21 | const TestChainId = 1 22 | 23 | var AliceKey = keystore.TestKeyRing.SubstrateKeys[keystore.AliceKey].AsKeyringPair() 24 | var BobKey = keystore.TestKeyRing.SubstrateKeys[keystore.BobKey].AsKeyringPair() 25 | 26 | var TestLogLevel = log15.LvlTrace 27 | var AliceTestLogger = newTestLogger("Alice") 28 | var BobTestLogger = newTestLogger("Bob") 29 | 30 | var ThisChain msg.ChainId = 1 31 | var ForeignChain msg.ChainId = 2 32 | 33 | var relayers = []types.AccountID{ 34 | types.NewAccountID(AliceKey.PublicKey), 35 | types.NewAccountID(BobKey.PublicKey), 36 | } 37 | 38 | var resources = map[msg.ResourceId]utils.Method{ 39 | // These are taken from the Polkadot JS UI (Chain State -> Constants) 40 | msg.ResourceIdFromSlice(hexutil.MustDecode("0x000000000000000000000000000000c76ebe4a02bbc34786d860b355f5a5ce00")): utils.ExampleTransferMethod, 41 | msg.ResourceIdFromSlice(hexutil.MustDecode("0x000000000000000000000000000000e389d61c11e5fe32ec1735b3cd38c69501")): utils.ExampleMintErc721Method, 42 | msg.ResourceIdFromSlice(hexutil.MustDecode("0x000000000000000000000000000000f44be64d2de895454c3467021928e55e01")): utils.ExampleRemarkMethod, 43 | } 44 | 45 | const relayerThreshold = 2 46 | 47 | type testContext struct { 48 | client *utils.Client 49 | listener *listener 50 | router *mockRouter 51 | writerAlice *writer 52 | writerBob *writer 53 | latestOutNonce msg.Nonce 54 | latestInNonce msg.Nonce 55 | lSysErr chan error 56 | wSysErr chan error 57 | } 58 | 59 | var context testContext 60 | 61 | func TestMain(m *testing.M) { 62 | client, err := utils.CreateClient(AliceKey, TestEndpoint) 63 | if err != nil { 64 | panic(err) 65 | } 66 | 67 | var nativeTokenId, hashId, nftTokenId []byte 68 | 69 | err = utils.QueryConst(client, "Example", "NativeTokenId", &nativeTokenId) 70 | if err != nil { 71 | panic(err) 72 | } 73 | 74 | err = utils.QueryConst(client, "Example", "HashId", &hashId) 75 | if err != nil { 76 | panic(err) 77 | } 78 | err = utils.QueryConst(client, "Example", "Erc721Id", &nftTokenId) 79 | if err != nil { 80 | panic(err) 81 | } 82 | 83 | err = utils.InitializeChain(client, relayers, []msg.ChainId{ForeignChain}, resources, relayerThreshold) 84 | if err != nil { 85 | panic(err) 86 | } 87 | 88 | aliceConn, bobConn, wSysErr, err := createAliceAndBobConnections() 89 | if err != nil { 90 | panic(err) 91 | } 92 | l, lSysErr, r, err := newTestListener(client, aliceConn) 93 | if err != nil { 94 | panic(err) 95 | } 96 | alice := NewWriter(aliceConn, AliceTestLogger, wSysErr, nil, true) 97 | bob := NewWriter(bobConn, BobTestLogger, wSysErr, nil, true) 98 | context = testContext{ 99 | client: client, 100 | listener: l, 101 | router: r, 102 | writerAlice: alice, 103 | writerBob: bob, 104 | latestInNonce: 0, 105 | latestOutNonce: 0, 106 | lSysErr: lSysErr, 107 | wSysErr: wSysErr, 108 | } 109 | 110 | os.Exit(m.Run()) 111 | } 112 | 113 | func newTestLogger(name string) log15.Logger { 114 | tLog := log15.Root().New("chain", name) 115 | tLog.SetHandler(log15.LvlFilterHandler(TestLogLevel, tLog.GetHandler())) 116 | return tLog 117 | } 118 | 119 | // createAliceConnection creates and starts a connection with the Alice keypair 120 | func createAliceConnection() (*Connection, chan error, error) { 121 | sysErr := make(chan error) 122 | alice := NewConnection(TestEndpoint, "Alice", AliceKey, AliceTestLogger, make(chan int), sysErr) 123 | err := alice.Connect() 124 | if err != nil { 125 | return nil, nil, err 126 | } 127 | return alice, sysErr, err 128 | } 129 | 130 | // createAliceAndBobConnections creates and calls `Connect()` on two Connections using the Alice and Bob keypairs 131 | func createAliceAndBobConnections() (*Connection, *Connection, chan error, error) { 132 | alice, sysErr, err := createAliceConnection() 133 | if err != nil { 134 | return nil, nil, nil, err 135 | } 136 | 137 | bob := NewConnection(TestEndpoint, "Bob", BobKey, AliceTestLogger, make(chan int), sysErr) 138 | err = bob.Connect() 139 | if err != nil { 140 | return nil, nil, nil, err 141 | } 142 | 143 | return alice, bob, sysErr, nil 144 | } 145 | 146 | // getFreeBalance queries the balance for an account, storing the result in `res` 147 | func getFreeBalance(c *Connection, res *types.U128) { 148 | var acct types.AccountInfo 149 | 150 | ok, err := c.queryStorage("System", "Account", c.key.PublicKey, nil, &acct) 151 | if err != nil { 152 | panic(err) 153 | } else if !ok { 154 | panic("no account data") 155 | } 156 | *res = acct.Data.Free 157 | } 158 | -------------------------------------------------------------------------------- /chains/substrate/types.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 ChainSafe Systems 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | package substrate 5 | 6 | import ( 7 | "math/big" 8 | 9 | "github.com/ChainSafe/chainbridge-utils/msg" 10 | "github.com/centrifuge/go-substrate-rpc-client/scale" 11 | "github.com/centrifuge/go-substrate-rpc-client/types" 12 | ) 13 | 14 | type voteState struct { 15 | VotesFor []types.AccountID 16 | VotesAgainst []types.AccountID 17 | Status voteStatus 18 | } 19 | 20 | type voteStatus struct { 21 | IsActive bool 22 | IsApproved bool 23 | IsRejected bool 24 | } 25 | 26 | func (m *voteStatus) Decode(decoder scale.Decoder) error { 27 | b, err := decoder.ReadOneByte() 28 | 29 | if err != nil { 30 | return err 31 | } 32 | 33 | if b == 0 { 34 | m.IsActive = true 35 | } else if b == 1 { 36 | m.IsApproved = true 37 | } else if b == 2 { 38 | m.IsRejected = true 39 | } 40 | 41 | return nil 42 | } 43 | 44 | // proposal represents an on-chain proposal 45 | type proposal struct { 46 | depositNonce types.U64 47 | call types.Call 48 | sourceId types.U8 49 | resourceId types.Bytes32 50 | method string 51 | } 52 | 53 | // encode takes only nonce and call and encodes them for storage queries 54 | func (p *proposal) encode() ([]byte, error) { 55 | return types.EncodeToBytes(struct { 56 | types.U64 57 | types.Call 58 | }{p.depositNonce, p.call}) 59 | } 60 | 61 | func (w *writer) createFungibleProposal(m msg.Message) (*proposal, error) { 62 | bigAmt := big.NewInt(0).SetBytes(m.Payload[0].([]byte)) 63 | amount := types.NewU128(*bigAmt) 64 | recipient := types.NewAccountID(m.Payload[1].([]byte)) 65 | depositNonce := types.U64(m.DepositNonce) 66 | 67 | meta := w.conn.getMetadata() 68 | method, err := w.resolveResourceId(m.ResourceId) 69 | if err != nil { 70 | return nil, err 71 | } 72 | call, err := types.NewCall( 73 | &meta, 74 | method, 75 | recipient, 76 | amount, 77 | ) 78 | if err != nil { 79 | return nil, err 80 | } 81 | if w.extendCall { 82 | eRID, err := types.EncodeToBytes(m.ResourceId) 83 | if err != nil { 84 | return nil, err 85 | } 86 | call.Args = append(call.Args, eRID...) 87 | } 88 | 89 | return &proposal{ 90 | depositNonce: depositNonce, 91 | call: call, 92 | sourceId: types.U8(m.Source), 93 | resourceId: types.NewBytes32(m.ResourceId), 94 | method: method, 95 | }, nil 96 | } 97 | 98 | func (w *writer) createNonFungibleProposal(m msg.Message) (*proposal, error) { 99 | tokenId := types.NewU256(*big.NewInt(0).SetBytes(m.Payload[0].([]byte))) 100 | recipient := types.NewAccountID(m.Payload[1].([]byte)) 101 | metadata := types.Bytes(m.Payload[2].([]byte)) 102 | depositNonce := types.U64(m.DepositNonce) 103 | 104 | meta := w.conn.getMetadata() 105 | method, err := w.resolveResourceId(m.ResourceId) 106 | if err != nil { 107 | return nil, err 108 | } 109 | 110 | call, err := types.NewCall( 111 | &meta, 112 | method, 113 | recipient, 114 | tokenId, 115 | metadata, 116 | ) 117 | if err != nil { 118 | return nil, err 119 | } 120 | if w.extendCall { 121 | eRID, err := types.EncodeToBytes(m.ResourceId) 122 | if err != nil { 123 | return nil, err 124 | } 125 | call.Args = append(call.Args, eRID...) 126 | } 127 | 128 | return &proposal{ 129 | depositNonce: depositNonce, 130 | call: call, 131 | sourceId: types.U8(m.Source), 132 | resourceId: types.NewBytes32(m.ResourceId), 133 | method: method, 134 | }, nil 135 | } 136 | 137 | func (w *writer) createGenericProposal(m msg.Message) (*proposal, error) { 138 | meta := w.conn.getMetadata() 139 | method, err := w.resolveResourceId(m.ResourceId) 140 | if err != nil { 141 | return nil, err 142 | } 143 | 144 | call, err := types.NewCall( 145 | &meta, 146 | method, 147 | types.NewHash(m.Payload[0].([]byte)), 148 | ) 149 | if err != nil { 150 | return nil, err 151 | } 152 | if w.extendCall { 153 | eRID, err := types.EncodeToBytes(m.ResourceId) 154 | if err != nil { 155 | return nil, err 156 | } 157 | 158 | call.Args = append(call.Args, eRID...) 159 | } 160 | return &proposal{ 161 | depositNonce: types.U64(m.DepositNonce), 162 | call: call, 163 | sourceId: types.U8(m.Source), 164 | resourceId: types.NewBytes32(m.ResourceId), 165 | method: method, 166 | }, nil 167 | } 168 | -------------------------------------------------------------------------------- /chains/substrate/writer.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 ChainSafe Systems 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | package substrate 5 | 6 | import ( 7 | "bytes" 8 | "errors" 9 | "fmt" 10 | "time" 11 | 12 | "github.com/ChainSafe/chainbridge-utils/core" 13 | 14 | utils "github.com/ChainSafe/ChainBridge/shared/substrate" 15 | metrics "github.com/ChainSafe/chainbridge-utils/metrics/types" 16 | "github.com/ChainSafe/chainbridge-utils/msg" 17 | "github.com/ChainSafe/log15" 18 | "github.com/centrifuge/go-substrate-rpc-client/types" 19 | ) 20 | 21 | var _ core.Writer = &writer{} 22 | 23 | var AcknowledgeProposal utils.Method = utils.BridgePalletName + ".acknowledge_proposal" 24 | var TerminatedError = errors.New("terminated") 25 | 26 | type writer struct { 27 | conn *Connection 28 | log log15.Logger 29 | sysErr chan<- error 30 | metrics *metrics.ChainMetrics 31 | extendCall bool // Extend extrinsic calls to substrate with ResourceID.Used for backward compatibility with example pallet. 32 | } 33 | 34 | func NewWriter(conn *Connection, log log15.Logger, sysErr chan<- error, m *metrics.ChainMetrics, extendCall bool) *writer { 35 | return &writer{ 36 | conn: conn, 37 | log: log, 38 | sysErr: sysErr, 39 | metrics: m, 40 | extendCall: extendCall, 41 | } 42 | } 43 | 44 | func (w *writer) ResolveMessage(m msg.Message) bool { 45 | var prop *proposal 46 | var err error 47 | 48 | // Construct the proposal 49 | switch m.Type { 50 | case msg.FungibleTransfer: 51 | prop, err = w.createFungibleProposal(m) 52 | case msg.NonFungibleTransfer: 53 | prop, err = w.createNonFungibleProposal(m) 54 | case msg.GenericTransfer: 55 | prop, err = w.createGenericProposal(m) 56 | default: 57 | w.sysErr <- fmt.Errorf("unrecognized message type received (chain=%d, name=%s)", m.Destination, w.conn.name) 58 | return false 59 | } 60 | 61 | if err != nil { 62 | w.sysErr <- fmt.Errorf("failed to construct proposal (chain=%d, name=%s) Error: %w", m.Destination, w.conn.name, err) 63 | return false 64 | } 65 | 66 | for i := 0; i < BlockRetryLimit; i++ { 67 | // Ensure we only submit a vote if the proposal hasn't completed 68 | valid, reason, err := w.proposalValid(prop) 69 | if err != nil { 70 | w.log.Error("Failed to assert proposal state", "err", err) 71 | time.Sleep(BlockRetryInterval) 72 | continue 73 | } 74 | 75 | // If active submit call, otherwise skip it. Retry on failure. 76 | if valid { 77 | w.log.Info("Acknowledging proposal on chain", "nonce", prop.depositNonce, "source", prop.sourceId, "resource", fmt.Sprintf("%x", prop.resourceId), "method", prop.method) 78 | 79 | err = w.conn.SubmitTx(AcknowledgeProposal, prop.depositNonce, prop.sourceId, prop.resourceId, prop.call) 80 | if err != nil && err.Error() == TerminatedError.Error() { 81 | return false 82 | } else if err != nil { 83 | w.log.Error("Failed to execute extrinsic", "err", err) 84 | time.Sleep(BlockRetryInterval) 85 | continue 86 | } 87 | if w.metrics != nil { 88 | w.metrics.VotesSubmitted.Inc() 89 | } 90 | return true 91 | } else { 92 | w.log.Info("Ignoring proposal", "reason", reason, "nonce", prop.depositNonce, "source", prop.sourceId, "resource", prop.resourceId) 93 | return true 94 | } 95 | } 96 | return true 97 | } 98 | 99 | func (w *writer) resolveResourceId(id [32]byte) (string, error) { 100 | var res []byte 101 | exists, err := w.conn.queryStorage(utils.BridgeStoragePrefix, "Resources", id[:], nil, &res) 102 | if err != nil { 103 | return "", err 104 | } 105 | if !exists { 106 | return "", fmt.Errorf("resource %x not found on chain", id) 107 | } 108 | return string(res), nil 109 | } 110 | 111 | // proposalValid asserts the state of a proposal. If the proposal is active and this relayer 112 | // has not voted, it will return true. Otherwise, it will return false with a reason string. 113 | func (w *writer) proposalValid(prop *proposal) (bool, string, error) { 114 | var voteRes voteState 115 | srcId, err := types.EncodeToBytes(prop.sourceId) 116 | if err != nil { 117 | return false, "", err 118 | } 119 | propBz, err := prop.encode() 120 | if err != nil { 121 | return false, "", err 122 | } 123 | exists, err := w.conn.queryStorage(utils.BridgeStoragePrefix, "Votes", srcId, propBz, &voteRes) 124 | if err != nil { 125 | return false, "", err 126 | } 127 | 128 | if !exists { 129 | return true, "", nil 130 | } else if voteRes.Status.IsActive { 131 | if containsVote(voteRes.VotesFor, types.NewAccountID(w.conn.key.PublicKey)) || 132 | containsVote(voteRes.VotesAgainst, types.NewAccountID(w.conn.key.PublicKey)) { 133 | return false, "already voted", nil 134 | } else { 135 | return true, "", nil 136 | } 137 | } else { 138 | return false, "proposal complete", nil 139 | } 140 | } 141 | 142 | func containsVote(votes []types.AccountID, voter types.AccountID) bool { 143 | for _, v := range votes { 144 | if bytes.Equal(v[:], voter[:]) { 145 | return true 146 | } 147 | } 148 | return false 149 | } 150 | -------------------------------------------------------------------------------- /cmd/chainbridge/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 ChainSafe Systems 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | /* 4 | Provides the command-line interface for the chainbridge application. 5 | 6 | For configuration and CLI commands see the README: https://github.com/ChainSafe/ChainBridge. 7 | */ 8 | package main 9 | 10 | import ( 11 | "errors" 12 | "fmt" 13 | "net/http" 14 | "os" 15 | 16 | "strconv" 17 | 18 | "github.com/ChainSafe/ChainBridge/chains/ethereum" 19 | "github.com/ChainSafe/ChainBridge/chains/substrate" 20 | "github.com/ChainSafe/ChainBridge/config" 21 | "github.com/ChainSafe/chainbridge-utils/core" 22 | "github.com/ChainSafe/chainbridge-utils/metrics/health" 23 | metrics "github.com/ChainSafe/chainbridge-utils/metrics/types" 24 | "github.com/ChainSafe/chainbridge-utils/msg" 25 | log "github.com/ChainSafe/log15" 26 | "github.com/prometheus/client_golang/prometheus/promhttp" 27 | "github.com/urfave/cli/v2" 28 | ) 29 | 30 | var app = cli.NewApp() 31 | 32 | var cliFlags = []cli.Flag{ 33 | config.ConfigFileFlag, 34 | config.VerbosityFlag, 35 | config.KeystorePathFlag, 36 | config.BlockstorePathFlag, 37 | config.FreshStartFlag, 38 | config.LatestBlockFlag, 39 | config.MetricsFlag, 40 | config.MetricsPort, 41 | } 42 | 43 | var generateFlags = []cli.Flag{ 44 | config.PasswordFlag, 45 | config.Sr25519Flag, 46 | config.Secp256k1Flag, 47 | config.SubkeyNetworkFlag, 48 | } 49 | 50 | var devFlags = []cli.Flag{ 51 | config.TestKeyFlag, 52 | } 53 | 54 | var importFlags = []cli.Flag{ 55 | config.EthereumImportFlag, 56 | config.PrivateKeyFlag, 57 | config.Sr25519Flag, 58 | config.Secp256k1Flag, 59 | config.PasswordFlag, 60 | config.SubkeyNetworkFlag, 61 | } 62 | 63 | var accountCommand = cli.Command{ 64 | Name: "accounts", 65 | Usage: "manage bridge keystore", 66 | Description: "The accounts command is used to manage the bridge keystore.\n" + 67 | "\tTo generate a new account (key type generated is determined on the flag passed in): chainbridge accounts generate\n" + 68 | "\tTo import a keystore file: chainbridge accounts import path/to/file\n" + 69 | "\tTo import a geth keystore file: chainbridge accounts import --ethereum path/to/file\n" + 70 | "\tTo import a private key file: chainbridge accounts import --privateKey private_key\n" + 71 | "\tTo list keys: chainbridge accounts list", 72 | Subcommands: []*cli.Command{ 73 | { 74 | Action: wrapHandler(handleGenerateCmd), 75 | Name: "generate", 76 | Usage: "generate bridge keystore, key type determined by flag", 77 | Flags: generateFlags, 78 | Description: "The generate subcommand is used to generate the bridge keystore.\n" + 79 | "\tIf no options are specified, a secp256k1 key will be made.", 80 | }, 81 | { 82 | Action: wrapHandler(handleImportCmd), 83 | Name: "import", 84 | Usage: "import bridge keystore", 85 | Flags: importFlags, 86 | Description: "The import subcommand is used to import a keystore for the bridge.\n" + 87 | "\tA path to the keystore must be provided\n" + 88 | "\tUse --ethereum to import an ethereum keystore from external sources such as geth\n" + 89 | "\tUse --privateKey to create a keystore from a provided private key.", 90 | }, 91 | { 92 | Action: wrapHandler(handleListCmd), 93 | Name: "list", 94 | Usage: "list bridge keystore", 95 | Description: "The list subcommand is used to list all of the bridge keystores.\n", 96 | }, 97 | }, 98 | } 99 | 100 | var ( 101 | Version = "0.0.1" 102 | ) 103 | 104 | // init initializes CLI 105 | func init() { 106 | app.Action = run 107 | app.Copyright = "Copyright 2019 ChainSafe Systems Authors" 108 | app.Name = "chainbridge" 109 | app.Usage = "ChainBridge" 110 | app.Authors = []*cli.Author{{Name: "ChainSafe Systems 2019"}} 111 | app.Version = Version 112 | app.EnableBashCompletion = true 113 | app.Commands = []*cli.Command{ 114 | &accountCommand, 115 | } 116 | 117 | app.Flags = append(app.Flags, cliFlags...) 118 | app.Flags = append(app.Flags, devFlags...) 119 | } 120 | 121 | func main() { 122 | if err := app.Run(os.Args); err != nil { 123 | log.Error(err.Error()) 124 | os.Exit(1) 125 | } 126 | } 127 | 128 | func startLogger(ctx *cli.Context) error { 129 | logger := log.Root() 130 | handler := logger.GetHandler() 131 | var lvl log.Lvl 132 | 133 | if lvlToInt, err := strconv.Atoi(ctx.String(config.VerbosityFlag.Name)); err == nil { 134 | lvl = log.Lvl(lvlToInt) 135 | } else if lvl, err = log.LvlFromString(ctx.String(config.VerbosityFlag.Name)); err != nil { 136 | return err 137 | } 138 | log.Root().SetHandler(log.LvlFilterHandler(lvl, handler)) 139 | 140 | return nil 141 | } 142 | 143 | func run(ctx *cli.Context) error { 144 | err := startLogger(ctx) 145 | if err != nil { 146 | return err 147 | } 148 | 149 | log.Info("Starting ChainBridge...") 150 | 151 | cfg, err := config.GetConfig(ctx) 152 | if err != nil { 153 | return err 154 | } 155 | 156 | log.Debug("Config on initialization...", "config", *cfg) 157 | 158 | // Check for test key flag 159 | var ks string 160 | var insecure bool 161 | if key := ctx.String(config.TestKeyFlag.Name); key != "" { 162 | ks = key 163 | insecure = true 164 | } else { 165 | ks = cfg.KeystorePath 166 | } 167 | 168 | // Used to signal core shutdown due to fatal error 169 | sysErr := make(chan error) 170 | c := core.NewCore(sysErr) 171 | 172 | for _, chain := range cfg.Chains { 173 | chainId, errr := strconv.Atoi(chain.Id) 174 | if errr != nil { 175 | return errr 176 | } 177 | chainConfig := &core.ChainConfig{ 178 | Name: chain.Name, 179 | Id: msg.ChainId(chainId), 180 | Endpoint: chain.Endpoint, 181 | From: chain.From, 182 | KeystorePath: ks, 183 | Insecure: insecure, 184 | BlockstorePath: ctx.String(config.BlockstorePathFlag.Name), 185 | FreshStart: ctx.Bool(config.FreshStartFlag.Name), 186 | LatestBlock: ctx.Bool(config.LatestBlockFlag.Name), 187 | Opts: chain.Opts, 188 | } 189 | var newChain core.Chain 190 | var m *metrics.ChainMetrics 191 | 192 | logger := log.Root().New("chain", chainConfig.Name) 193 | 194 | if ctx.Bool(config.MetricsFlag.Name) { 195 | m = metrics.NewChainMetrics(chain.Name) 196 | } 197 | 198 | if chain.Type == "ethereum" { 199 | newChain, err = ethereum.InitializeChain(chainConfig, logger, sysErr, m) 200 | } else if chain.Type == "substrate" { 201 | newChain, err = substrate.InitializeChain(chainConfig, logger, sysErr, m) 202 | } else { 203 | return errors.New("unrecognized Chain Type") 204 | } 205 | 206 | if err != nil { 207 | return err 208 | } 209 | c.AddChain(newChain) 210 | 211 | } 212 | 213 | // Start prometheus and health server 214 | if ctx.Bool(config.MetricsFlag.Name) { 215 | port := ctx.Int(config.MetricsPort.Name) 216 | blockTimeoutStr := os.Getenv(config.HealthBlockTimeout) 217 | blockTimeout := config.DefaultBlockTimeout 218 | if blockTimeoutStr != "" { 219 | blockTimeout, err = strconv.ParseInt(blockTimeoutStr, 10, 0) 220 | if err != nil { 221 | return err 222 | } 223 | } 224 | h := health.NewHealthServer(port, c.Registry, int(blockTimeout)) 225 | 226 | go func() { 227 | http.Handle("/metrics", promhttp.Handler()) 228 | http.HandleFunc("/health", h.HealthStatus) 229 | err := http.ListenAndServe(fmt.Sprintf(":%d", port), nil) 230 | if errors.Is(err, http.ErrServerClosed) { 231 | log.Info("Health status server is shutting down", err) 232 | } else { 233 | log.Error("Error serving metrics", "err", err) 234 | } 235 | }() 236 | } 237 | 238 | c.Start() 239 | 240 | return nil 241 | } 242 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 ChainSafe Systems 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | package config 5 | 6 | import ( 7 | "encoding/json" 8 | "fmt" 9 | "os" 10 | "path/filepath" 11 | 12 | "github.com/ethereum/go-ethereum/log" 13 | "github.com/urfave/cli/v2" 14 | ) 15 | 16 | const DefaultConfigPath = "./config.json" 17 | const DefaultKeystorePath = "./keys" 18 | const DefaultBlockTimeout = int64(180) // 3 minutes 19 | 20 | type Config struct { 21 | Chains []RawChainConfig `json:"chains"` 22 | KeystorePath string `json:"keystorePath,omitempty"` 23 | } 24 | 25 | // RawChainConfig is parsed directly from the config file and should be using to construct the core.ChainConfig 26 | type RawChainConfig struct { 27 | Name string `json:"name"` 28 | Type string `json:"type"` 29 | Id string `json:"id"` // ChainID 30 | Endpoint string `json:"endpoint"` // url for rpc endpoint 31 | From string `json:"from"` // address of key to use 32 | Opts map[string]string `json:"opts"` 33 | } 34 | 35 | func NewConfig() *Config { 36 | return &Config{ 37 | Chains: []RawChainConfig{}, 38 | } 39 | } 40 | 41 | func (c *Config) ToJSON(file string) *os.File { 42 | var ( 43 | newFile *os.File 44 | err error 45 | ) 46 | 47 | var raw []byte 48 | if raw, err = json.Marshal(*c); err != nil { 49 | log.Warn("error marshalling json", "err", err) 50 | os.Exit(1) 51 | } 52 | 53 | newFile, err = os.Create(file) 54 | if err != nil { 55 | log.Warn("error creating config file", "err", err) 56 | } 57 | _, err = newFile.Write(raw) 58 | if err != nil { 59 | log.Warn("error writing to config file", "err", err) 60 | } 61 | 62 | if err := newFile.Close(); err != nil { 63 | log.Warn("error closing file", "err", err) 64 | } 65 | return newFile 66 | } 67 | 68 | func (c *Config) validate() error { 69 | for _, chain := range c.Chains { 70 | if chain.Type == "" { 71 | return fmt.Errorf("required field chain.Type empty for chain %s", chain.Id) 72 | } 73 | if chain.Endpoint == "" { 74 | return fmt.Errorf("required field chain.Endpoint empty for chain %s", chain.Id) 75 | } 76 | if chain.Name == "" { 77 | return fmt.Errorf("required field chain.Name empty for chain %s", chain.Id) 78 | } 79 | if chain.Id == "" { 80 | return fmt.Errorf("required field chain.Id empty for chain %s", chain.Id) 81 | } 82 | if chain.From == "" { 83 | return fmt.Errorf("required field chain.From empty for chain %s", chain.Id) 84 | } 85 | } 86 | return nil 87 | } 88 | 89 | func GetConfig(ctx *cli.Context) (*Config, error) { 90 | var fig Config 91 | path := DefaultConfigPath 92 | if file := ctx.String(ConfigFileFlag.Name); file != "" { 93 | path = file 94 | } 95 | err := loadConfig(path, &fig) 96 | if err != nil { 97 | log.Warn("err loading json file", "err", err.Error()) 98 | return &fig, err 99 | } 100 | if ksPath := ctx.String(KeystorePathFlag.Name); ksPath != "" { 101 | fig.KeystorePath = ksPath 102 | } 103 | log.Debug("Loaded config", "path", path) 104 | err = fig.validate() 105 | if err != nil { 106 | return nil, err 107 | } 108 | return &fig, nil 109 | } 110 | 111 | func loadConfig(file string, config *Config) error { 112 | ext := filepath.Ext(file) 113 | fp, err := filepath.Abs(file) 114 | if err != nil { 115 | return err 116 | } 117 | 118 | log.Debug("Loading configuration", "path", filepath.Clean(fp)) 119 | 120 | f, err := os.Open(filepath.Clean(fp)) 121 | if err != nil { 122 | return err 123 | } 124 | 125 | if ext == ".json" { 126 | if err = json.NewDecoder(f).Decode(&config); err != nil { 127 | return err 128 | } 129 | } else { 130 | return fmt.Errorf("unrecognized extention: %s", ext) 131 | } 132 | 133 | return nil 134 | } 135 | -------------------------------------------------------------------------------- /config/config_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 ChainSafe Systems 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | package config 5 | 6 | import ( 7 | "flag" 8 | "fmt" 9 | "io/ioutil" 10 | "os" 11 | "reflect" 12 | "testing" 13 | 14 | "github.com/urfave/cli/v2" 15 | ) 16 | 17 | func createTempConfigFile() (*os.File, *Config) { 18 | testConfig := NewConfig() 19 | ethCfg := RawChainConfig{ 20 | Name: "chain", 21 | Type: "ethereum", 22 | Id: "1", 23 | Endpoint: "endpoint", 24 | From: "0x0", 25 | Opts: map[string]string{"key": "value"}, 26 | } 27 | testConfig.Chains = []RawChainConfig{ethCfg} 28 | tmpFile, err := ioutil.TempFile(os.TempDir(), "*.json") 29 | if err != nil { 30 | fmt.Println("Cannot create temporary file", "err", err) 31 | os.Exit(1) 32 | } 33 | 34 | f := testConfig.ToJSON(tmpFile.Name()) 35 | return f, testConfig 36 | } 37 | 38 | // Creates a cli context for a test given a set of flags and values 39 | func createCliContext(description string, flags []string, values []interface{}) (*cli.Context, error) { 40 | set := flag.NewFlagSet(description, 0) 41 | for i := range values { 42 | switch v := values[i].(type) { 43 | case bool: 44 | set.Bool(flags[i], v, "") 45 | case string: 46 | set.String(flags[i], v, "") 47 | case uint: 48 | set.Uint(flags[i], v, "") 49 | default: 50 | return nil, fmt.Errorf("unexpected cli value type: %T", values[i]) 51 | } 52 | } 53 | context := cli.NewContext(nil, set, nil) 54 | return context, nil 55 | } 56 | 57 | func TestLoadJSONConfig(t *testing.T) { 58 | file, cfg := createTempConfigFile() 59 | ctx, err := createCliContext("", []string{"config"}, []interface{}{file.Name()}) 60 | if err != nil { 61 | t.Fatal(err) 62 | } 63 | 64 | res, err := GetConfig(ctx) 65 | if err != nil { 66 | t.Fatal(err) 67 | } 68 | 69 | if !reflect.DeepEqual(res, cfg) { 70 | t.Errorf("did not match\ngot: %+v\nexpected: %+v", res.Chains[0], cfg.Chains[0]) 71 | } 72 | } 73 | 74 | func TestValdiateConfig(t *testing.T) { 75 | valid := RawChainConfig{ 76 | Name: "chain", 77 | Type: "ethereum", 78 | Id: "1", 79 | Endpoint: "endpoint", 80 | From: "0x0", 81 | Opts: nil, 82 | } 83 | 84 | missingType := RawChainConfig{ 85 | Name: "chain", 86 | Type: "", 87 | Id: "1", 88 | Endpoint: "endpoint", 89 | From: "0x0", 90 | Opts: nil, 91 | } 92 | 93 | missingEndpoint := RawChainConfig{ 94 | Name: "chain", 95 | Type: "ethereum", 96 | Id: "1", 97 | Endpoint: "", 98 | From: "0x0", 99 | Opts: nil, 100 | } 101 | 102 | missingName := RawChainConfig{ 103 | Name: "", 104 | Type: "ethereum", 105 | Id: "1", 106 | Endpoint: "endpoint", 107 | From: "0x0", 108 | Opts: nil, 109 | } 110 | 111 | cfg := Config{ 112 | Chains: []RawChainConfig{valid}, 113 | KeystorePath: "", 114 | } 115 | 116 | err := cfg.validate() 117 | if err != nil { 118 | t.Fatal(err) 119 | } 120 | 121 | cfg = Config{ 122 | Chains: []RawChainConfig{missingType}, 123 | KeystorePath: "", 124 | } 125 | 126 | err = cfg.validate() 127 | if err == nil { 128 | t.Fatal("must require type field") 129 | } 130 | 131 | cfg = Config{ 132 | Chains: []RawChainConfig{missingEndpoint}, 133 | KeystorePath: "", 134 | } 135 | 136 | err = cfg.validate() 137 | if err == nil { 138 | t.Fatal("must require endpoint field") 139 | } 140 | 141 | cfg = Config{ 142 | Chains: []RawChainConfig{missingName}, 143 | KeystorePath: "", 144 | } 145 | 146 | err = cfg.validate() 147 | if err == nil { 148 | t.Fatal("must require name field") 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /config/flags.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 ChainSafe Systems 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | package config 5 | 6 | import ( 7 | log "github.com/ChainSafe/log15" 8 | "github.com/urfave/cli/v2" 9 | ) 10 | 11 | // Env vars 12 | var ( 13 | HealthBlockTimeout = "BLOCK_TIMEOUT" 14 | ) 15 | 16 | var ( 17 | ConfigFileFlag = &cli.StringFlag{ 18 | Name: "config", 19 | Usage: "JSON configuration file", 20 | } 21 | 22 | VerbosityFlag = &cli.StringFlag{ 23 | Name: "verbosity", 24 | Usage: "Supports levels crit (silent) to trce (trace)", 25 | Value: log.LvlInfo.String(), 26 | } 27 | 28 | KeystorePathFlag = &cli.StringFlag{ 29 | Name: "keystore", 30 | Usage: "Path to keystore directory", 31 | Value: DefaultKeystorePath, 32 | } 33 | 34 | BlockstorePathFlag = &cli.StringFlag{ 35 | Name: "blockstore", 36 | Usage: "Specify path for blockstore", 37 | Value: "", // Empty will use home dir 38 | } 39 | 40 | FreshStartFlag = &cli.BoolFlag{ 41 | Name: "fresh", 42 | Usage: "Disables loading from blockstore at start. Opts will still be used if specified.", 43 | } 44 | 45 | LatestBlockFlag = &cli.BoolFlag{ 46 | Name: "latest", 47 | Usage: "Overrides blockstore and start block, starts from latest block", 48 | } 49 | ) 50 | 51 | // Metrics flags 52 | var ( 53 | MetricsFlag = &cli.BoolFlag{ 54 | Name: "metrics", 55 | Usage: "Enables metric server", 56 | } 57 | 58 | MetricsPort = &cli.IntFlag{ 59 | Name: "metricsPort", 60 | Usage: "Port to serve metrics on", 61 | Value: 8001, 62 | } 63 | ) 64 | 65 | // Generate subcommand flags 66 | var ( 67 | PasswordFlag = &cli.StringFlag{ 68 | Name: "password", 69 | Usage: "Password used to encrypt the keystore. Used with --generate, --import, or --unlock", 70 | } 71 | Sr25519Flag = &cli.BoolFlag{ 72 | Name: "sr25519", 73 | Usage: "Specify account/key type as sr25519.", 74 | } 75 | Secp256k1Flag = &cli.BoolFlag{ 76 | Name: "secp256k1", 77 | Usage: "Specify account/key type as secp256k1.", 78 | } 79 | ) 80 | 81 | var ( 82 | EthereumImportFlag = &cli.BoolFlag{ 83 | Name: "ethereum", 84 | Usage: "Import an existing ethereum keystore, such as from geth.", 85 | } 86 | PrivateKeyFlag = &cli.StringFlag{ 87 | Name: "privateKey", 88 | Usage: "Import a hex representation of a private key into a keystore.", 89 | } 90 | SubkeyNetworkFlag = &cli.StringFlag{ 91 | Name: "network", 92 | Usage: "Specify the network to use for the address encoding (substrate/polkadot/centrifuge)", 93 | DefaultText: "substrate", 94 | } 95 | ) 96 | 97 | // Test Setting Flags 98 | var ( 99 | TestKeyFlag = &cli.StringFlag{ 100 | Name: "testkey", 101 | Usage: "Applies a predetermined test keystore to the chains.", 102 | } 103 | ) 104 | -------------------------------------------------------------------------------- /connections/ethereum/egs/egs.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 ChainSafe Systems 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | package egs 5 | 6 | import ( 7 | "context" 8 | "encoding/json" 9 | "errors" 10 | "io/ioutil" 11 | "math/big" 12 | "net/http" 13 | "time" 14 | ) 15 | 16 | const ( 17 | // Retries is the amount of times to reattempt fetching price data before giving up 18 | Retries = 5 19 | // Timeout is the duration in seconds for http timeouts 20 | Timeout = 10 21 | Fast = "fast" 22 | Fastest = "fastest" 23 | Average = "average" 24 | ) 25 | 26 | type gasPriceResponse struct { 27 | Fast float32 `json:"fast"` 28 | Fastest float32 `json:"fastest"` 29 | SafeLow float32 `json:"safeLow"` 30 | Average float32 `json:"average"` 31 | BlockTime float32 `json:"block_time"` 32 | BlockNum int64 `json:"blockNum"` 33 | Speed float32 `json:"speed"` 34 | SafeLowWait float32 `json:"safeLowWait"` 35 | AvgWait float32 `json:"avgWait"` 36 | FastWait float32 `json:"fastWait"` 37 | FastestWait float32 `json:"fastestWait"` 38 | GasPriceRange interface{} `json:"gasPriceRange"` 39 | } 40 | 41 | // FetchGasPrice will query EGS for the current gas prices and return the price for the specified speed. 42 | func FetchGasPrice(apiKey, speed string) (*big.Int, error) { 43 | var gsnURL = "https://ethgasstation.info/api/ethgasAPI.json?api-key=" + apiKey 44 | 45 | for i := 0; i < Retries; i++ { 46 | res, err := queryAPI(gsnURL) 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | if res != nil { 52 | return parsePrice(res, speed), nil 53 | } 54 | 55 | time.Sleep(1 * time.Second) 56 | } 57 | 58 | return nil, errors.New("failed to fetch GSN gas price - retries exceeded") 59 | } 60 | 61 | func queryAPI(url string) (*gasPriceResponse, error) { 62 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*Timeout) 63 | defer cancel() 64 | 65 | req, err := http.NewRequestWithContext(ctx, "GET", url, nil) 66 | if err != nil { 67 | return nil, err 68 | } 69 | client := &http.Client{} 70 | res, err := client.Do(req) 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | defer func() { _ = res.Body.Close() }() 76 | 77 | body, err := ioutil.ReadAll(res.Body) 78 | if err != nil { 79 | return nil, err 80 | } 81 | 82 | var data gasPriceResponse 83 | err = json.Unmarshal(body, &data) 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | return &data, nil 89 | } 90 | 91 | func parsePrice(result *gasPriceResponse, speed string) *big.Int { 92 | var res *big.Int 93 | switch speed { 94 | case Fastest: 95 | res = big.NewInt(int64(result.Fastest)) 96 | case Fast: 97 | res = big.NewInt(int64(result.Fast)) 98 | case Average: 99 | res = big.NewInt(int64(result.Average)) 100 | default: 101 | res = big.NewInt(int64(result.Fast)) 102 | } 103 | // Make sure we get at least 10 gwei to avoid prices of 0 104 | if res.Cmp(big.NewInt(1)) == -1 { 105 | res = big.NewInt(1) 106 | } 107 | 108 | base := big.NewInt(8) // we are using 8 here but not 9 bcs ethgas station returns values in Gwei * 10 109 | return res.Mul(res, big.NewInt(0).Exp(big.NewInt(10), base, nil)) 110 | } 111 | -------------------------------------------------------------------------------- /connections/ethereum/egs/egs_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 ChainSafe Systems 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | package egs 5 | 6 | import ( 7 | "encoding/json" 8 | "math/big" 9 | "net/http" 10 | "net/http/httptest" 11 | "testing" 12 | 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | var exampleResponse = &gasPriceResponse{ 17 | Fast: 1, 18 | Fastest: 2, 19 | Average: 4, 20 | } 21 | 22 | // Captured by querying endpoint manually 23 | var rawResponse = "{\"fast\":590,\"fastest\":700,\"safeLow\":490,\"average\":0.5,\"block_time\":15.9,\"blockNum\":12389059,\"speed\":0.995538884040233,\"safeLowWait\":13.6,\"avgWait\":2.1,\"fastWait\":0.9,\"fastestWait\":0.6,\"gasPriceRange\":{\"4\":265,\"6\":265,\"8\":265,\"10\":265,\"20\":265,\"30\":265,\"40\":265,\"50\":265,\"60\":265,\"70\":265,\"80\":265,\"90\":265,\"100\":265,\"110\":265,\"120\":265,\"130\":265,\"140\":265,\"150\":265,\"160\":265,\"170\":265,\"180\":265,\"190\":265,\"200\":265,\"220\":265,\"240\":265,\"260\":265,\"280\":265,\"300\":265,\"320\":265,\"340\":265,\"360\":265,\"380\":265,\"400\":265,\"420\":265,\"440\":265,\"460\":265,\"480\":15,\"490\":13.6,\"500\":2.1,\"520\":1.7,\"540\":1.2,\"560\":1,\"580\":0.9,\"590\":0.9,\"600\":0.8,\"620\":0.7,\"640\":0.7,\"660\":0.7,\"680\":0.7,\"700\":0.6}}" 24 | 25 | func TestParsePrice(t *testing.T) { 26 | assert.Equal(t, parsePrice(exampleResponse, Fastest), big.NewInt(200000000)) 27 | assert.Equal(t, parsePrice(exampleResponse, Fast), big.NewInt(100000000)) 28 | assert.Equal(t, parsePrice(exampleResponse, Average), big.NewInt(400000000)) 29 | } 30 | 31 | func TestFetchPrice(t *testing.T) { 32 | // Start a local HTTP server 33 | server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 34 | _, _ = rw.Write([]byte(rawResponse)) 35 | })) 36 | // Close the server when test finishes 37 | defer server.Close() 38 | 39 | res, err := queryAPI(server.URL) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | var expected gasPriceResponse 45 | err = json.Unmarshal([]byte(rawResponse), &expected) 46 | if err != nil { 47 | t.Fatal(err) 48 | } 49 | 50 | assert.Equal(t, *res, expected) 51 | assert.Equal(t, parsePrice(res, Fastest), big.NewInt(70000000000)) 52 | } 53 | -------------------------------------------------------------------------------- /docker-compose-e2e.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 ChainSafe Systems 2 | # SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | version: '3' 5 | services: 6 | geth1: 7 | image: "chainsafe/chainbridge-geth:20200505131100-5586a65" 8 | container_name: geth1 9 | ports: 10 | - "8545:8545" 11 | 12 | geth2: 13 | image: "chainsafe/chainbridge-geth:20200505131100-5586a65" 14 | container_name: geth2 15 | ports: 16 | - "8546:8545" 17 | 18 | sub-chain: 19 | image: "chainsafe/chainbridge-substrate-chain:v1.3.0" 20 | container_name: sub-chain 21 | command: chainbridge-substrate-chain --dev --alice --ws-external --rpc-external 22 | ports: 23 | - "9944:9944" 24 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 ChainSafe Systems 2 | # SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | version: '3' 5 | services: 6 | bridge: 7 | build: 8 | context: . 9 | container_name: bridge 10 | environment: 11 | - KEYSTORE_PASSWORD=password 12 | command: --config /config/config.json 13 | volumes: 14 | - ./config:/config 15 | - ./keys:/keys/ 16 | network_mode: host -------------------------------------------------------------------------------- /docs/metrics.md: -------------------------------------------------------------------------------- 1 | # Metrics 2 | 3 | Basic metrics and a health status check can be enabled with the `--metrics` flag (default port `8001`, use `--metricsPort` to specify). 4 | 5 | ## Prometheus 6 | Prometheus metrics are served on `/metrics`. For each chain that exists, this provides: 7 | - `_blocks_processed`: the number of blocks processed by the chains listener. 8 | - `_latest_processed_block`: most recent block that has been processed by the listener. 9 | - `_latest_known_block`: most recent block that exists on the chain. 10 | - `_votes_submitted`: number of votes submitted by the relayer. 11 | 12 | ## Health Check 13 | The endpoint `/health` will return the current known block height, and a timestamp of when it was first seen for every chain: 14 | ```json 15 | { 16 | "chains": [ 17 | { 18 | "chainId": "Number", 19 | "height": "Number", 20 | "lastUpdated": "Date" 21 | } 22 | ] 23 | } 24 | ``` 25 | 26 | If the timestamp is at least 120 seconds old an error will be returned instead: 27 | ```json 28 | { 29 | "error": "String" 30 | } 31 | ``` -------------------------------------------------------------------------------- /docs/test.md: -------------------------------------------------------------------------------- 1 | # Testing 2 | 3 | Unit tests require an ethereum node running on `localhost:8545` and a substrate node running on `localhost:9944`. E2E tests require an additional ethereum node on `localhost:8546`. 4 | 5 | A docker-compose file is provided to run two Geth nodes and a chainbridge-substrate-chain node in isolated environments: 6 | ``` 7 | $ docker-compose -f ./docker-compose-e2e.yml up 8 | ``` 9 | 10 | See [chainbridge-solidity](https://github.com/chainsafe/chainbridge-solidity) and [chainbridge-substrate-chain](https://github.com/ChainSafe/chainbridge-substrate-chain) for more information on testing facilities. 11 | 12 | All Go tests can be run with: 13 | ``` 14 | $ make test 15 | ``` 16 | Go tests specifically for ethereum, substrate and E2E can be run with 17 | ``` 18 | $ make test-eth 19 | $ make test-sub 20 | $ make test-e2e 21 | ``` 22 | 23 | The bindings for the solidity contracts live in `bindings/`. To update the bindings modify `scripts/setup-contracts.sh` and then run `make clean && make setup-contracts` 24 | -------------------------------------------------------------------------------- /e2e/generic_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 ChainSafe Systems 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | package e2e 5 | 6 | import ( 7 | "fmt" 8 | "testing" 9 | 10 | eth "github.com/ChainSafe/ChainBridge/e2e/ethereum" 11 | sub "github.com/ChainSafe/ChainBridge/e2e/substrate" 12 | ethtest "github.com/ChainSafe/ChainBridge/shared/ethereum/testing" 13 | subtest "github.com/ChainSafe/ChainBridge/shared/substrate/testing" 14 | log "github.com/ChainSafe/log15" 15 | ) 16 | 17 | func testSubstrateHashToGenericHandler(t *testing.T, ctx *testContext) { 18 | numberOfTxs := 5 19 | nonce := subtest.GetDepositNonce(t, ctx.subClient, EthAChainId) + 1 20 | for i := 1; i <= numberOfTxs; i++ { 21 | i := i // for scope 22 | ok := t.Run(fmt.Sprintf("Transfer %d", i), func(t *testing.T) { 23 | // Execute transfer 24 | hash := sub.HashInt(i) 25 | subtest.InitiateHashTransfer(t, ctx.subClient, hash, EthAChainId) 26 | 27 | // Wait for event 28 | eth.WaitForProposalActive(t, ctx.ethA.Client, ctx.ethA.BaseContracts.BridgeAddress, nonce) 29 | eth.WaitForProposalExecutedEvent(t, ctx.ethA.Client, ctx.ethA.BaseContracts.BridgeAddress, nonce) 30 | nonce++ 31 | 32 | // Verify hash is available 33 | ethtest.AssertHashExistence(t, ctx.ethA.Client, hash, ctx.ethA.TestContracts.AssetStoreSub) 34 | }) 35 | if !ok { 36 | return 37 | } 38 | } 39 | } 40 | 41 | func testEthereumHashToGenericHandler(t *testing.T, ctx *testContext) { 42 | numberOfTxs := 5 43 | nonce := ethtest.GetDepositNonce(t, ctx.ethA.Client, ctx.ethA.BaseContracts.BridgeAddress, EthBChainId) + 1 44 | //ethtest.AssertGenericResourceAddress(t, ctx.ethA.Client, ctx.ethA.BaseContracts.GenericHandlerAddress, ctx.EthGenericResourceId, ctx.ethA.TestContracts.AssetStoreEth) 45 | for i := 1; i <= numberOfTxs; i++ { 46 | i := i // for scope 47 | ok := t.Run(fmt.Sprintf("Transfer %d", i), func(t *testing.T) { 48 | // Execute transfer 49 | hash := sub.HashInt(i) 50 | log.Info("Submitting transaction", "number", i, "hash", hash.Hex(), "resourceId", ctx.EthGenericResourceId.Hex(), "from", ctx.ethA.Client.Opts.From, "handler", ctx.ethA.BaseContracts.GenericHandlerAddress) 51 | eth.CreateGenericDeposit(t, ctx.ethA.Client, EthBChainId, hash[:], ctx.ethB.BaseContracts, ctx.EthGenericResourceId) 52 | 53 | // Wait for event 54 | eth.WaitForProposalActive(t, ctx.ethB.Client, ctx.ethB.BaseContracts.BridgeAddress, nonce) 55 | eth.WaitForProposalExecutedEvent(t, ctx.ethB.Client, ctx.ethB.BaseContracts.BridgeAddress, nonce) 56 | nonce++ 57 | 58 | // Verify hash is available 59 | ethtest.AssertHashExistence(t, ctx.ethB.Client, hash, ctx.ethB.TestContracts.AssetStoreEth) 60 | }) 61 | if !ok { 62 | return 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /e2e/nonfungible_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 ChainSafe Systems 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | package e2e 5 | 6 | import ( 7 | "fmt" 8 | "testing" 9 | 10 | eth "github.com/ChainSafe/ChainBridge/e2e/ethereum" 11 | sub "github.com/ChainSafe/ChainBridge/e2e/substrate" 12 | ethtest "github.com/ChainSafe/ChainBridge/shared/ethereum/testing" 13 | subtest "github.com/ChainSafe/ChainBridge/shared/substrate/testing" 14 | log "github.com/ChainSafe/log15" 15 | "github.com/centrifuge/go-substrate-rpc-client/types" 16 | ) 17 | 18 | func testErc721ToSubstrateRoundTrip(t *testing.T, ctx *testContext) { 19 | numberOfTxs := 5 20 | subRecipient := sub.AliceKp.AsKeyringPair().PublicKey // use alice so we can send back 21 | ethRecipient := eth.AliceKp.CommonAddress() 22 | tokens := ethtest.GenerateErc721Tokens(1, numberOfTxs) 23 | 24 | ethtest.Erc721MintMany(t, ctx.ethA.Client, ctx.ethA.TestContracts.Erc721Sub, tokens) 25 | ethtest.Erc721ApproveMany(t, ctx.ethA.Client, ctx.ethA.TestContracts.Erc721Sub, ctx.ethA.BaseContracts.ERC721HandlerAddress, tokens) 26 | nonce := ethtest.GetDepositNonce(t, ctx.ethA.Client, ctx.ethA.BaseContracts.BridgeAddress, SubChainId) + 1 27 | for i := 1; i <= numberOfTxs; i++ { 28 | i := i // for scope 29 | ok := t.Run(fmt.Sprintf("Transfer %d", i), func(t *testing.T) { 30 | tok := tokens[i-1] 31 | // Verify ownership and intact metadata 32 | ethtest.Erc721AssertOwner(t, ctx.ethA.Client, ctx.ethA.TestContracts.Erc721Sub, tok.Id, ctx.ethA.Client.Opts.From) 33 | ethtest.Erc721AssertMetadata(t, ctx.ethA.Client, ctx.ethA.TestContracts.Erc721Sub, tok.Id, string(tok.Metadata[:])) 34 | 35 | // Initiate transfer 36 | log.Info("Submitting transaction", "number", i, "from", ctx.ethA.Client.Opts.From, "handler", ctx.ethA.BaseContracts.ERC721HandlerAddress.String(), "tokenId", tok.Id.String()) 37 | eth.CreateErc721Deposit(t, ctx.ethA.Client, SubChainId, subRecipient, tok.Id, ctx.ethA.BaseContracts, ctx.EthSubErc721ResourceId) 38 | 39 | // Wait for event 40 | sub.WaitForProposalSuccessOrFail(t, ctx.subClient, types.NewU64(nonce), types.U8(EthAChainId)) 41 | nonce++ 42 | 43 | // Verify substrate ownership and metadata 44 | subtest.AssertOwnerOf(t, ctx.subClient, tok.Id, types.NewAccountID(subRecipient)) 45 | subtest.AssertErc721Metadata(t, ctx.subClient, tok.Id, tok.Metadata[:]) 46 | 47 | // Verify token no longer exists on ethA 48 | ethtest.Erc721AssertNonExistence(t, ctx.ethA.Client, ctx.ethA.TestContracts.Erc721Sub, tok.Id) 49 | }) 50 | if !ok { 51 | return 52 | } 53 | } 54 | 55 | nonce = subtest.GetDepositNonce(t, ctx.subClient, EthAChainId) + 1 56 | for i := 1; i <= numberOfTxs; i++ { 57 | i := i // for scope 58 | ok := t.Run(fmt.Sprintf("Transfer %d", i), func(t *testing.T) { 59 | // Get latest eth block 60 | tok := tokens[i-1] 61 | 62 | // Execute transfer 63 | subtest.InitiateNonFungibleTransfer(t, ctx.subClient, types.NewU256(*tok.Id), ethRecipient.Bytes(), EthAChainId) 64 | 65 | // Wait for event 66 | eth.WaitForProposalActive(t, ctx.ethA.Client, ctx.ethA.BaseContracts.BridgeAddress, nonce) 67 | eth.WaitForProposalExecutedEvent(t, ctx.ethA.Client, ctx.ethA.BaseContracts.BridgeAddress, nonce) 68 | nonce++ 69 | 70 | // Verify ownership and intact metadata 71 | ethtest.Erc721AssertOwner(t, ctx.ethA.Client, ctx.ethA.TestContracts.Erc721Sub, tok.Id, ethRecipient) 72 | ethtest.Erc721AssertMetadata(t, ctx.ethA.Client, ctx.ethA.TestContracts.Erc721Sub, tok.Id, string(tok.Metadata[:])) 73 | 74 | // Verify token no longer exists on substrate 75 | subtest.AssertErc721NonExistence(t, ctx.subClient, tok.Id) 76 | }) 77 | if !ok { 78 | return 79 | } 80 | } 81 | 82 | } 83 | 84 | func testErc721EthToEthRoundTrip(t *testing.T, ctx *testContext) { 85 | ethARecipient := eth.AliceKp.CommonAddress() 86 | ethBRecipient := eth.AliceKp.CommonAddress() 87 | numberOfTxs := 5 88 | tokens := ethtest.GenerateErc721Tokens(11, numberOfTxs) 89 | ethtest.Erc721MintMany(t, ctx.ethA.Client, ctx.ethA.TestContracts.Erc721Eth, tokens) 90 | ethtest.Erc721ApproveMany(t, ctx.ethA.Client, ctx.ethA.TestContracts.Erc721Eth, ctx.ethA.BaseContracts.ERC721HandlerAddress, tokens) 91 | 92 | nonce := ethtest.GetDepositNonce(t, ctx.ethA.Client, ctx.ethA.BaseContracts.BridgeAddress, EthBChainId) + 1 93 | for i := 1; i <= numberOfTxs; i++ { 94 | i := i // for scope 95 | ok := t.Run(fmt.Sprintf("Transfer ethA to ethB %d", i), func(t *testing.T) { 96 | tok := tokens[i-1] 97 | 98 | // Verify ownership and intact metadata on ethA 99 | ethtest.Erc721AssertOwner(t, ctx.ethA.Client, ctx.ethA.TestContracts.Erc721Eth, tok.Id, ctx.ethA.Client.Opts.From) 100 | ethtest.Erc721AssertMetadata(t, ctx.ethA.Client, ctx.ethA.TestContracts.Erc721Eth, tok.Id, string(tok.Metadata[:])) 101 | 102 | // Verify non-existence on ethB 103 | ethtest.Erc721AssertNonExistence(t, ctx.ethB.Client, ctx.ethB.TestContracts.Erc721Eth, tok.Id) 104 | 105 | // Initiate transfer 106 | log.Info("Submitting transaction", "number", i, "recipient", ethBRecipient, "resourcId", ctx.EthEthErc721ResourceId.Hex(), "tokenId", tok.Id.String(), "from", ctx.ethA.Client.Opts.From, "handler", ctx.ethA.BaseContracts.ERC721HandlerAddress) 107 | eth.CreateErc721Deposit(t, ctx.ethA.Client, EthBChainId, ethBRecipient.Bytes(), tok.Id, ctx.ethA.BaseContracts, ctx.EthEthErc721ResourceId) 108 | 109 | // Wait for proposal events 110 | eth.WaitForProposalActive(t, ctx.ethB.Client, ctx.ethB.BaseContracts.BridgeAddress, nonce) 111 | eth.WaitForProposalExecutedEvent(t, ctx.ethB.Client, ctx.ethB.BaseContracts.BridgeAddress, nonce) 112 | nonce++ 113 | 114 | // Verify ownership on ethB 115 | ethtest.Erc721AssertOwner(t, ctx.ethB.Client, ctx.ethB.TestContracts.Erc721Eth, tok.Id, ethBRecipient) 116 | ethtest.Erc721AssertMetadata(t, ctx.ethB.Client, ctx.ethB.TestContracts.Erc721Eth, tok.Id, string(tok.Metadata[:])) 117 | 118 | // Verify non-existence on ethA 119 | ethtest.Erc721AssertNonExistence(t, ctx.ethA.Client, ctx.ethA.TestContracts.Erc721Eth, tok.Id) 120 | }) 121 | if !ok { 122 | return 123 | } 124 | } 125 | 126 | // Aprove handler to now move the tokens 127 | ethtest.Erc721ApproveMany(t, ctx.ethB.Client, ctx.ethB.TestContracts.Erc721Eth, ctx.ethB.BaseContracts.ERC721HandlerAddress, tokens) 128 | 129 | nonce = ethtest.GetDepositNonce(t, ctx.ethB.Client, ctx.ethB.BaseContracts.BridgeAddress, EthAChainId) + 1 130 | for i := 1; i <= numberOfTxs; i++ { 131 | i := i // for scope 132 | ok := t.Run(fmt.Sprintf("Transfer ethB to ethA %d", i), func(t *testing.T) { 133 | tok := tokens[i-1] 134 | 135 | // Initiate transfer 136 | log.Info("Submitting transaction", "number", i, "recipient", ethBRecipient, "resourceId", ctx.EthEthErc721ResourceId.Hex(), "tokenId", tok.Id.String(), "from", ctx.ethB.Client.Opts.From, "handler", ctx.ethB.BaseContracts.ERC721HandlerAddress) 137 | eth.CreateErc721Deposit(t, ctx.ethB.Client, EthAChainId, ethARecipient.Bytes(), tok.Id, ctx.ethB.BaseContracts, ctx.EthEthErc721ResourceId) 138 | 139 | // Wait for proposal events 140 | eth.WaitForProposalActive(t, ctx.ethA.Client, ctx.ethA.BaseContracts.BridgeAddress, nonce) 141 | eth.WaitForProposalExecutedEvent(t, ctx.ethA.Client, ctx.ethA.BaseContracts.BridgeAddress, nonce) 142 | nonce++ 143 | 144 | // Verify ownership and metadata on ethA 145 | ethtest.Erc721AssertOwner(t, ctx.ethA.Client, ctx.ethA.TestContracts.Erc721Eth, tok.Id, ethARecipient) 146 | //ethtest.Erc721AssertMetadata(t, ctx.ethA.Client, ctx.ethA.TestContracts.Erc721Eth, tok.Id, string(tok.Metadata[:])) 147 | 148 | // Verfy non-existence on ethB 149 | ethtest.Erc721AssertNonExistence(t, ctx.ethB.Client, ctx.ethB.TestContracts.Erc721Eth, tok.Id) 150 | }) 151 | if !ok { 152 | return 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /e2e/parallel_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 ChainSafe Systems 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | package e2e 5 | 6 | import ( 7 | "fmt" 8 | "math/big" 9 | "testing" 10 | "time" 11 | 12 | eth "github.com/ChainSafe/ChainBridge/e2e/ethereum" 13 | ethutils "github.com/ChainSafe/ChainBridge/shared/ethereum" 14 | ethtest "github.com/ChainSafe/ChainBridge/shared/ethereum/testing" 15 | subutils "github.com/ChainSafe/ChainBridge/shared/substrate" 16 | subtest "github.com/ChainSafe/ChainBridge/shared/substrate/testing" 17 | "github.com/ChainSafe/chainbridge-utils/msg" 18 | log "github.com/ChainSafe/log15" 19 | "github.com/centrifuge/go-substrate-rpc-client/signature" 20 | "github.com/centrifuge/go-substrate-rpc-client/types" 21 | "github.com/ethereum/go-ethereum/common" 22 | ) 23 | 24 | // Random recipient 25 | var SteveSubKp = signature.KeyringPair{ 26 | URI: "//Steve", 27 | Address: "5F7UdYV6noJeqbMfiiJKfHDTGYnBiqhMgY2EBi3nFAqcL33w", 28 | PublicKey: []byte{0x86, 0xd1, 0xe9, 0xb3, 0x79, 0x37, 0x23, 0x39, 0x02, 0xca, 0xe0, 0x62, 0xf5, 0xd0, 0x1c, 0xae, 0x46, 0x38, 0x58, 0x42, 0xe7, 0xec, 0x9d, 0x1c, 0xeb, 0x6b, 0x1b, 0x10, 0x67, 0x0e, 0x30, 0x27}, 29 | } 30 | var SteveEthAddr = common.HexToAddress("0x880fd09782C3183c489595111637bb3bc906f215") 31 | 32 | // Final recipients 33 | var subRecipient = SteveSubKp.PublicKey 34 | var ethRecipient = SteveEthAddr 35 | 36 | // Tx's per configuration 37 | var numberOfTxs = 5 38 | 39 | // Value per transaction 40 | var amountPerTest = types.NewU128(*big.NewInt(500)) 41 | 42 | // Expected overall balance change for recipients 43 | var balanceDelta = big.NewInt(0).Mul(amountPerTest.Int, big.NewInt(int64(numberOfTxs))) 44 | 45 | // Substrate block time 46 | var blockTime = time.Second * 5 47 | 48 | // Delay between tx's to allow more interleaving 49 | var txInterval = time.Millisecond * 200 50 | 51 | func testThreeChainsParallel(t *testing.T, ctx *testContext) { 52 | // Creat some additional clients for thread safety 53 | subClient := ctx.subClient 54 | ethAClientA := ethtest.NewClient(t, eth.EthAEndpoint, eth.AliceKp) 55 | ethAClientB := ethtest.NewClient(t, eth.EthAEndpoint, eth.EveKp) 56 | ethBClientA := ethtest.NewClient(t, eth.EthBEndpoint, eth.AliceKp) 57 | 58 | // Mint tokens to eve and dave, approve handlers 59 | ethtest.Erc20Mint(t, ctx.ethA.Client, ctx.ethA.TestContracts.Erc20Sub, eth.AliceKp.CommonAddress(), balanceDelta) 60 | ethtest.Erc20Mint(t, ctx.ethA.Client, ctx.ethA.TestContracts.Erc20Eth, eth.EveKp.CommonAddress(), balanceDelta) 61 | ethtest.Erc20Mint(t, ctx.ethB.Client, ctx.ethB.TestContracts.Erc20Eth, eth.AliceKp.CommonAddress(), balanceDelta) 62 | ethtest.Erc20Approve(t, ethAClientA, ctx.ethA.TestContracts.Erc20Sub, ctx.ethA.BaseContracts.ERC20HandlerAddress, balanceDelta) 63 | ethtest.Erc20Approve(t, ethAClientB, ctx.ethA.TestContracts.Erc20Eth, ctx.ethA.BaseContracts.ERC20HandlerAddress, balanceDelta) 64 | ethtest.Erc20Approve(t, ethBClientA, ctx.ethB.TestContracts.Erc20Eth, ctx.ethB.BaseContracts.ERC20HandlerAddress, balanceDelta) 65 | 66 | // Get current balances 67 | subRecipientBalance := subtest.BalanceOf(t, subClient, subRecipient) 68 | ethASubRecipientBalance := ethtest.Erc20BalanceOf(t, ethAClientA, ctx.ethA.TestContracts.Erc20Sub, ethRecipient) 69 | ethAEthRecipientBalance := ethtest.Erc20BalanceOf(t, ethAClientA, ctx.ethA.TestContracts.Erc20Eth, ethRecipient) 70 | ethBRecipientBalance := ethtest.Erc20BalanceOf(t, ethBClientA, ctx.ethB.TestContracts.Erc20Eth, ethRecipient) 71 | 72 | // Execute several sub-tests that call t.Paralell 73 | t.Run("Parallel tx submission", func(t *testing.T) { 74 | 75 | // Substrate -> EthA 76 | t.Run("Submit Sub to Eth", func(t *testing.T) { 77 | // TODO: Need to run this in sequence, doesn't return if parallel 78 | //t.Parallel() 79 | submitSubToEth(t, subClient, EthAChainId, ethRecipient.Bytes(), amountPerTest, ctx.EthSubErc20ResourceId) 80 | 81 | }) 82 | // EthA -> Substrate 83 | t.Run("Submit Eth to Sub", func(t *testing.T) { 84 | t.Parallel() 85 | submitEthDeposit("Eth to Sub", t, ctx.ethA, ethAClientA, SubChainId, subRecipient, amountPerTest.Int, ctx.EthSubErc20ResourceId) 86 | }) 87 | // EthA -> EthB 88 | t.Run("Submit EthA to EthB", func(t *testing.T) { 89 | t.Parallel() 90 | submitEthDeposit("EthA to EthB", t, ctx.ethA, ethAClientB, EthBChainId, ethRecipient.Bytes(), amountPerTest.Int, ctx.EthEthErc20ResourceId) 91 | }) 92 | // EthB -> EthA 93 | t.Run("Submit EthB to EthA", func(t *testing.T) { 94 | t.Parallel() 95 | submitEthDeposit("EthB to EthA", t, ctx.ethB, ethBClientA, EthAChainId, ethRecipient.Bytes(), amountPerTest.Int, ctx.EthEthErc20ResourceId) 96 | }) 97 | }) 98 | 99 | // Must wait long enough for processing to complete 100 | time.Sleep(blockTime * 10) 101 | 102 | // Calculate and verify expected results 103 | t.Run("Assert Sub balance", func(t *testing.T) { 104 | subtest.AssertBalanceOf(t, subClient, subRecipient, subRecipientBalance.Add(subRecipientBalance, balanceDelta)) 105 | }) 106 | t.Run("Assert EthA Sub balance", func(t *testing.T) { 107 | ethtest.Erc20AssertBalance(t, ethAClientA, ethASubRecipientBalance.Add(ethASubRecipientBalance, balanceDelta), ctx.ethA.TestContracts.Erc20Sub, ethRecipient) 108 | }) 109 | t.Run("Assert EthB balance", func(t *testing.T) { 110 | ethtest.Erc20AssertBalance(t, ethBClientA, ethBRecipientBalance.Add(ethBRecipientBalance, balanceDelta), ctx.ethB.TestContracts.Erc20Eth, ethRecipient) 111 | }) 112 | t.Run("Assert EthA Eth balance", func(t *testing.T) { 113 | ethtest.Erc20AssertBalance(t, ethAClientA, ethAEthRecipientBalance.Add(ethAEthRecipientBalance, balanceDelta), ctx.ethA.TestContracts.Erc20Eth, ethRecipient) 114 | }) 115 | 116 | } 117 | 118 | func submitEthDeposit(name string, t *testing.T, ethCtx *eth.TestContext, client *ethutils.Client, destId msg.ChainId, recipient []byte, amount *big.Int, rId msg.ResourceId) { 119 | for i := 1; i <= numberOfTxs; i++ { 120 | i := i // for scope 121 | t.Run(fmt.Sprintf("%s Transfer %d", name, i), func(t *testing.T) { 122 | // Initiate transfer 123 | log.Info("Submitting transaction", "number", i, "recipient", recipient, "amount", amount, "rId", rId.Hex()) 124 | ethtest.LockNonceAndUpdate(t, client) 125 | eth.CreateErc20Deposit(t, client, destId, recipient, amount, ethCtx.BaseContracts, rId) 126 | client.UnlockNonce() 127 | time.Sleep(txInterval) 128 | }) 129 | } 130 | } 131 | 132 | func submitSubToEth(t *testing.T, client *subutils.Client, destId msg.ChainId, recipient []byte, amount types.U128, rId msg.ResourceId) { 133 | var calls []types.Call 134 | for i := 1; i <= numberOfTxs; i++ { 135 | i := i // for scope 136 | t.Run(fmt.Sprintf("Substrate to Eth Transfer %d", i), func(t *testing.T) { 137 | // Execute transfer 138 | log.Info("Creating transaction", "number", i, "recipient", recipient, "amount", amount, "rId", rId.Hex()) 139 | call := subtest.NewNativeTransferCall(t, client, amount, recipient, destId) 140 | calls = append(calls, call) 141 | }) 142 | } 143 | log.Info("Submitting transactions", "numberOfTxs", numberOfTxs, "amount/tx", amountPerTest) 144 | 145 | err := subutils.BatchSubmit(client, calls) 146 | if err != nil { 147 | t.Fatal(err) 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /e2e/substrate/substrate.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 ChainSafe Systems 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | package substrate 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "testing" 10 | "time" 11 | 12 | utils "github.com/ChainSafe/ChainBridge/shared/substrate" 13 | "github.com/ChainSafe/chainbridge-utils/core" 14 | "github.com/ChainSafe/chainbridge-utils/keystore" 15 | "github.com/ChainSafe/chainbridge-utils/msg" 16 | "github.com/ChainSafe/log15" 17 | "github.com/centrifuge/go-substrate-rpc-client/types" 18 | ) 19 | 20 | const TestSubEndpoint = "ws://localhost:9944" 21 | 22 | var TestTimeout = time.Second * 30 23 | 24 | var log = log15.New("e2e", "substrate") 25 | 26 | var AliceKp = keystore.TestKeyRing.SubstrateKeys[keystore.AliceKey] 27 | var BobKp = keystore.TestKeyRing.SubstrateKeys[keystore.BobKey] 28 | var CharlieKp = keystore.TestKeyRing.SubstrateKeys[keystore.CharlieKey] 29 | var DaveKp = keystore.TestKeyRing.SubstrateKeys[keystore.DaveKey] 30 | var EveKp = keystore.TestKeyRing.SubstrateKeys[keystore.EveKey] 31 | 32 | var RelayerSet = []types.AccountID{ 33 | types.NewAccountID(BobKp.AsKeyringPair().PublicKey), 34 | types.NewAccountID(CharlieKp.AsKeyringPair().PublicKey), 35 | types.NewAccountID(DaveKp.AsKeyringPair().PublicKey), 36 | } 37 | 38 | func CreateConfig(key string, chain msg.ChainId) *core.ChainConfig { 39 | return &core.ChainConfig{ 40 | Name: fmt.Sprintf("substrate(%s)", key), 41 | Id: chain, 42 | Endpoint: TestSubEndpoint, 43 | From: "", 44 | KeystorePath: key, 45 | Insecure: true, 46 | FreshStart: true, 47 | BlockstorePath: os.TempDir(), 48 | Opts: map[string]string{"useExtendedCall": "true"}, 49 | } 50 | } 51 | 52 | func WaitForProposalSuccessOrFail(t *testing.T, client *utils.Client, nonce types.U64, chain types.U8) { 53 | key, err := types.CreateStorageKey(client.Meta, "System", "Events", nil, nil) 54 | if err != nil { 55 | t.Fatal(err) 56 | } 57 | 58 | sub, err := client.Api.RPC.State.SubscribeStorageRaw([]types.StorageKey{key}) 59 | if err != nil { 60 | t.Fatal(err) 61 | } 62 | defer sub.Unsubscribe() 63 | 64 | timeout := time.After(TestTimeout) 65 | for { 66 | select { 67 | case <-timeout: 68 | t.Fatalf("Timed out waiting for proposal success/fail event") 69 | case set := <-sub.Chan(): 70 | for _, chng := range set.Changes { 71 | if !types.Eq(chng.StorageKey, key) || !chng.HasStorageData { 72 | // skip, we are only interested in events with content 73 | continue 74 | } 75 | 76 | // Decode the event records 77 | events := utils.Events{} 78 | err = types.EventRecordsRaw(chng.StorageData).DecodeEventRecords(client.Meta, &events) 79 | if err != nil { 80 | t.Fatal(err) 81 | } 82 | 83 | for _, evt := range events.ChainBridge_ProposalSucceeded { 84 | if evt.DepositNonce == nonce && evt.SourceId == chain { 85 | log.Info("Proposal succeeded", "depositNonce", evt.DepositNonce, "source", evt.SourceId) 86 | return 87 | } else { 88 | log.Info("Found mismatched success event", "depositNonce", evt.DepositNonce, "source", evt.SourceId) 89 | } 90 | } 91 | 92 | for _, evt := range events.ChainBridge_ProposalFailed { 93 | if evt.DepositNonce == nonce && evt.SourceId == chain { 94 | log.Info("Proposal failed", "depositNonce", evt.DepositNonce, "source", evt.SourceId) 95 | t.Fatalf("Proposal failed. Nonce: %d Source: %d", evt.DepositNonce, evt.SourceId) 96 | } else { 97 | log.Info("Found mismatched fail event", "depositNonce", evt.DepositNonce, "source", evt.SourceId) 98 | } 99 | } 100 | } 101 | } 102 | 103 | } 104 | } 105 | 106 | func HashInt(i int) types.Hash { 107 | hash, err := types.GetHash(types.NewI64(int64(i))) 108 | if err != nil { 109 | panic(err) 110 | } 111 | return hash 112 | } 113 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ChainSafe/ChainBridge 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/ChainSafe/chainbridge-substrate-events v0.0.0-20200715141113-87198532025e 7 | github.com/ChainSafe/chainbridge-utils v1.0.6 8 | github.com/ChainSafe/log15 v1.0.0 9 | github.com/centrifuge/go-substrate-rpc-client v2.0.0+incompatible 10 | github.com/deckarep/golang-set v1.7.1 // indirect 11 | github.com/ethereum/go-ethereum v1.10.11 12 | github.com/prometheus/client_golang v1.4.1 13 | github.com/stretchr/testify v1.7.0 14 | github.com/urfave/cli/v2 v2.3.0 15 | ) 16 | -------------------------------------------------------------------------------- /scripts/ci_docker.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright 2020 ChainSafe Systems 3 | # SPDX-License-Identifier: LGPL-3.0-only 4 | 5 | set -ex 6 | 7 | GIT_SHORT_COMMIT=`git rev-parse --short HEAD` 8 | TIMESTAMP=`date -u +%Y%m%d%H%M%S` 9 | IMAGE_NAME="chainsafe/chainbridge" 10 | TAG=${TAG:-"${TIMESTAMP}-${GIT_SHORT_COMMIT}"} 11 | 12 | case $TARGET in 13 | "default") 14 | echo "Pushing image with tags \"latest\" and \"$TAG\"" 15 | docker build $BUILD_ARGS -t ${IMAGE_NAME}:${TAG} . 16 | docker tag "${IMAGE_NAME}:${TAG}" "${IMAGE_NAME}:latest" 17 | echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin 18 | docker push ${IMAGE_NAME}:latest 19 | docker push ${IMAGE_NAME}:${TAG} 20 | ;; 21 | 22 | "release") 23 | echo "Pushing image with tag $TAG" 24 | docker build $BUILD_ARGS -t ${IMAGE_NAME}:${TAG} . 25 | docker tag "${IMAGE_NAME}:${TAG}" "${IMAGE_NAME}:latest" 26 | echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin 27 | docker push ${IMAGE_NAME}:${TAG} 28 | ;; 29 | esac 30 | -------------------------------------------------------------------------------- /scripts/configs/config1.json: -------------------------------------------------------------------------------- 1 | { 2 | "chains": [ 3 | { 4 | "name": "eth", 5 | "type": "ethereum", 6 | "id": "0", 7 | "endpoint": "ws://localhost:8545", 8 | "from": "0xA3d69D9A1BD03cAE65f15C0a453bb716d8912f10", 9 | "opts": { 10 | "bridge": "0x62877dDCd49aD22f5eDfc6ac108e9a4b5D2bD88B", 11 | "erc20Handler": "0x3167776db165D8eA0f51790CA2bbf44Db5105ADF", 12 | "erc721Handler": "0x3f709398808af36ADBA86ACC617FeB7F5B7B193E", 13 | "genericHandler": "0x2B6Ab4b880A45a07d83Cf4d664Df4Ab85705Bc07", 14 | "gasLimit": "1000000", 15 | "maxGasPrice": "20000000" 16 | } 17 | }, 18 | { 19 | "name": "sub", 20 | "type": "substrate", 21 | "id": "1", 22 | "endpoint": "ws://localhost:9944", 23 | "from": "5fe3qieCV3GJu2n7F8uGKZCjPiMZaATzEo8Czu9eKuLUBfjK", 24 | "opts": { 25 | "useExtendedCall": "true" 26 | } 27 | } 28 | ] 29 | } -------------------------------------------------------------------------------- /scripts/docker/start-docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright 2020 ChainSafe Systems 3 | # SPDX-License-Identifier: LGPL-3.0-only 4 | 5 | 6 | set -e 7 | 8 | docker build -f ./Docker/Ganache.Dockerfile -t 'chainbridge_ganache:latest' . 9 | docker run -d -p 8545:8545 -p 8546:8546 chainbridge_ganache 10 | 11 | # docker exec -it chainbridge_ganache node ./scripts/cli/index.js --relayers 3 12 | 13 | dip=`ifconfig | grep 'inet 192'| awk '{ print $2}'` 14 | 15 | sed "s/0.0.0.0/$dip/g" config.json > docker.json 16 | 17 | cp docker.json config.json 18 | 19 | docker build -f ./Docker/Bridge.Dockerfile -t 'chainbridge_bridge:latest' . 20 | docker run chainbridge_bridge 21 | -------------------------------------------------------------------------------- /scripts/geth/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2020 ChainSafe Systems 2 | # SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | FROM ethereum/client-go:stable 5 | 6 | WORKDIR /root 7 | 8 | COPY ./genesis.json . 9 | COPY password.txt . 10 | COPY ./keystore /root/keystore 11 | COPY entrypoint.sh . 12 | 13 | RUN chmod +x entrypoint.sh 14 | 15 | ENTRYPOINT ["/root/entrypoint.sh"] -------------------------------------------------------------------------------- /scripts/geth/docker_deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright 2020 ChainSafe Systems 3 | # SPDX-License-Identifier: LGPL-3.0-only 4 | 5 | set -ex 6 | 7 | GIT_SHORT_COMMIT=`git rev-parse --short HEAD` 8 | TIMESTAMP=`date -u +%Y%m%d%H%M%S` 9 | IMAGE_NAME="chainsafe/chainbridge-geth" 10 | TAG="${TIMESTAMP}-${GIT_SHORT_COMMIT}" 11 | 12 | docker build $BUILD_ARGS -t ${IMAGE_NAME}:${TAG} . 13 | docker tag "${IMAGE_NAME}:${TAG}" "${IMAGE_NAME}:latest" 14 | #echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin 15 | docker push ${IMAGE_NAME}:latest 16 | docker push ${IMAGE_NAME}:${TAG} -------------------------------------------------------------------------------- /scripts/geth/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # Copyright 2020 ChainSafe Systems 3 | # SPDX-License-Identifier: LGPL-3.0-only 4 | 5 | # Exit on failure 6 | set -ex 7 | 8 | geth init /root/genesis.json 9 | 10 | rm -rf /root/.ethereum/keystore 11 | cp -r /root/keystore /root/.ethereum/ 12 | 13 | exec geth \ 14 | --nodiscover \ 15 | --unlock "0xff93B45308FD417dF303D6515aB04D9e89a750Ca","0x8e0a907331554AF72563Bd8D43051C2E64Be5d35","0x24962717f8fA5BA3b931bACaF9ac03924EB475a0","0x148FfB2074A9e59eD58142822b3eB3fcBffb0cd7","0x4CEEf6139f00F9F4535Ad19640Ff7A0137708485" \ 16 | --password /root/password.txt \ 17 | --ws \ 18 | --wsport 8545 \ 19 | --wsorigins="*" \ 20 | --wsaddr 0.0.0.0 \ 21 | --rpc \ 22 | --rpcport 8545 \ 23 | --rpccorsdomain="*" \ 24 | --rpcaddr 0.0.0.0 \ 25 | --networkid 5 \ 26 | --targetgaslimit 8000000 \ 27 | --allow-insecure-unlock \ 28 | --mine -------------------------------------------------------------------------------- /scripts/geth/genesis.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "chainId": 5, 4 | "homesteadBlock": 0, 5 | "eip150Block": 0, 6 | "eip155Block": 0, 7 | "eip158Block": 0, 8 | "byzantiumBlock": 0, 9 | "constantinopleBlock": 0, 10 | "petersburgBlock": 0, 11 | "clique": { 12 | "period": 1, 13 | "epoch": 30000 14 | } 15 | }, 16 | "difficulty": "1", 17 | "gasLimit": "8000000", 18 | "extradata": "0x0000000000000000000000000000000000000000000000000000000000000000ff93B45308FD417dF303D6515aB04D9e89a750Ca0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 19 | "alloc": { 20 | "0xff93B45308FD417dF303D6515aB04D9e89a750Ca": { "balance": "20000000000000000000"}, 21 | "0x8e0a907331554AF72563Bd8D43051C2E64Be5d35": { "balance": "20000000000000000000"}, 22 | "0x24962717f8fA5BA3b931bACaF9ac03924EB475a0": { "balance": "20000000000000000000"}, 23 | "0x148FfB2074A9e59eD58142822b3eB3fcBffb0cd7": { "balance": "20000000000000000000"}, 24 | "0x4CEEf6139f00F9F4535Ad19640Ff7A0137708485": { "balance": "20000000000000000000"} 25 | } 26 | } -------------------------------------------------------------------------------- /scripts/geth/keystore/UTC--2020-04-07T13-50-35.447Z--ff93b45308fd417df303d6515ab04d9e89a750ca: -------------------------------------------------------------------------------- 1 | {"version":3,"id":"369ea1d8-73ac-440b-932c-0722491eb9a2","address":"ff93b45308fd417df303d6515ab04d9e89a750ca","crypto":{"ciphertext":"095e1ad5c02249880243cfba01c79297b3a58315d5819d79c5349db7e65fcb13","cipherparams":{"iv":"589cfe4b3f5c935ea4847dd233980f0a"},"cipher":"aes-128-ctr","kdf":"scrypt","kdfparams":{"dklen":32,"salt":"f01da06f89421cdc88d7b896844ca3b0295f01208cff36f89fa2a6706d248107","n":8192,"r":8,"p":1},"mac":"60267143aa71c8124156ac2dafb5e771cb1d0a9862634c78606549418573b907"}} -------------------------------------------------------------------------------- /scripts/geth/keystore/UTC--2020-04-07T13-52-12.564Z--8e0a907331554af72563bd8d43051c2e64be5d35: -------------------------------------------------------------------------------- 1 | {"version":3,"id":"1bdffff9-241c-4cda-b9b1-a2afffa73a3a","address":"8e0a907331554af72563bd8d43051c2e64be5d35","crypto":{"ciphertext":"d1af2dc6976cabe7b390799c908940150315285515622c3e5332412564c57d6d","cipherparams":{"iv":"87c6ffc252c435c3b612fcca5e9a10a0"},"cipher":"aes-128-ctr","kdf":"scrypt","kdfparams":{"dklen":32,"salt":"e85eebb3570913942908be7201003435f9541beb26668ed275a1f2a6d4bcd932","n":8192,"r":8,"p":1},"mac":"dc8e177a84ac62c8395ecda5b7680f7ad7b0278499beddaa2e8c71965477b07b"}} -------------------------------------------------------------------------------- /scripts/geth/keystore/UTC--2020-04-07T13-53-49.003Z--24962717f8fa5ba3b931bacaf9ac03924eb475a0: -------------------------------------------------------------------------------- 1 | {"version":3,"id":"fca758b1-8782-4e54-9ede-96ca22931b72","address":"24962717f8fa5ba3b931bacaf9ac03924eb475a0","crypto":{"ciphertext":"5120712ba214043947befd835e3d86a38fce60c8d830f878ca1a053120d37056","cipherparams":{"iv":"71168d1cbf61b9e65ea02b5db38de7e7"},"cipher":"aes-128-ctr","kdf":"scrypt","kdfparams":{"dklen":32,"salt":"2010ca09bebf3988fb95997279ad7d761ab0d4b8323c647470dd680bdfc2f202","n":8192,"r":8,"p":1},"mac":"920f27b77fcdaaa3c322a33e91e088a7c30f82b0e3a9bfd1072058198b367f07"}} -------------------------------------------------------------------------------- /scripts/geth/keystore/UTC--2020-04-07T13-55-20.258Z--148ffb2074a9e59ed58142822b3eb3fcbffb0cd7: -------------------------------------------------------------------------------- 1 | {"version":3,"id":"2995e9ab-3ac6-4814-bc40-03165cf23f1b","address":"148ffb2074a9e59ed58142822b3eb3fcbffb0cd7","crypto":{"ciphertext":"c986b6930bd7814092d613c607e7f4df0758d895f6216294fc0452bb18348d91","cipherparams":{"iv":"527bb46169ca968a6821d75c845bfd3f"},"cipher":"aes-128-ctr","kdf":"scrypt","kdfparams":{"dklen":32,"salt":"71f9c2041a9a6b5476393331ef6924335f5485cf4c175cbc33c83c685c3f4394","n":8192,"r":8,"p":1},"mac":"c10a5aaf7a34a9d08377a02a517339b3afaa4ed7e6ec9a6a70dd1bcf3b4e1fba"}} -------------------------------------------------------------------------------- /scripts/geth/keystore/UTC--2020-04-07T13-56-44.768Z--4ceef6139f00f9f4535ad19640ff7a0137708485: -------------------------------------------------------------------------------- 1 | {"version":3,"id":"a163ef8f-b883-4474-a79f-9ecddf079683","address":"4ceef6139f00f9f4535ad19640ff7a0137708485","crypto":{"ciphertext":"fa612d9881ab9863b830daea365acaac6643469d0f02189b82fb84948f59d643","cipherparams":{"iv":"b91f79de31d0a9bdc8a0670a129efea8"},"cipher":"aes-128-ctr","kdf":"scrypt","kdfparams":{"dklen":32,"salt":"ddb540564be1bf1546245fafafd1b952431213b4d742a9af3d4fbedc54846ae3","n":8192,"r":8,"p":1},"mac":"ffbc5fab063f361c8dfa52f91be661d08a1e9119c9a81aac83fa81af4a2f5909"}} -------------------------------------------------------------------------------- /scripts/geth/password.txt: -------------------------------------------------------------------------------- 1 | passwordpassword 2 | passwordpassword 3 | passwordpassword 4 | passwordpassword 5 | passwordpassword -------------------------------------------------------------------------------- /scripts/geth/run_geth.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright 2020 ChainSafe Systems 3 | # SPDX-License-Identifier: LGPL-3.0-only 4 | 5 | # Exit on failure 6 | set -ex 7 | 8 | #QUIET=${QUIET:-"false"} 9 | #FORK=${FORK:-"false"} 10 | 11 | run_geth() { 12 | # Delete old chain data 13 | rm -rf $DATADIR 14 | # Init genesis 15 | geth init ./scripts/geth/genesis.json --datadir $DATADIR 16 | # Copy keystore 17 | rm -rf $DATADIR/keystore 18 | cp -r ./scripts/geth/keystore $DATADIR 19 | # Start geth with rpc, mining and unlocked accounts 20 | 21 | if [[ $FORK = "true" ]]; then 22 | geth --verbosity $VERBOSITY \ 23 | --datadir $DATADIR \ 24 | --port $P2P_PORT \ 25 | --nodiscover \ 26 | --unlock "0xff93B45308FD417dF303D6515aB04D9e89a750Ca","0x8e0a907331554AF72563Bd8D43051C2E64Be5d35","0x24962717f8fA5BA3b931bACaF9ac03924EB475a0","0x148FfB2074A9e59eD58142822b3eB3fcBffb0cd7","0x4CEEf6139f00F9F4535Ad19640Ff7A0137708485" \ 27 | --password ./scripts/geth/password.txt \ 28 | --ws \ 29 | --wsport $RPC_PORT \ 30 | --wsorigins="*" \ 31 | --rpc \ 32 | --rpcport $RPC_PORT \ 33 | --rpccorsdomain="*" \ 34 | --networkid 5 \ 35 | --targetgaslimit 8000000 \ 36 | --allow-insecure-unlock \ 37 | --mine & 38 | else 39 | geth --verbosity $VERBOSITY \ 40 | --datadir $DATADIR \ 41 | --port $P2P_PORT \ 42 | --nodiscover \ 43 | --unlock "0xff93B45308FD417dF303D6515aB04D9e89a750Ca","0x8e0a907331554AF72563Bd8D43051C2E64Be5d35","0x24962717f8fA5BA3b931bACaF9ac03924EB475a0","0x148FfB2074A9e59eD58142822b3eB3fcBffb0cd7","0x4CEEf6139f00F9F4535Ad19640Ff7A0137708485" \ 44 | --password ./scripts/geth/password.txt \ 45 | --ws \ 46 | --wsport $RPC_PORT \ 47 | --wsorigins="*" \ 48 | --rpc \ 49 | --rpcport $RPC_PORT \ 50 | --rpccorsdomain="*" \ 51 | --networkid 5 \ 52 | --targetgaslimit 8000000 \ 53 | --allow-insecure-unlock \ 54 | --mine 55 | fi 56 | } 57 | 58 | # Setup verbosity 59 | if [[ $QUIET = "true" ]]; then 60 | VERBOSITY=2 61 | else 62 | VERBOSITY=3 63 | fi 64 | 65 | case $INSTANCE in 66 | "1") 67 | DATADIR="./gethdata1" 68 | P2P_PORT=30303 69 | RPC_PORT=8545 70 | run_geth 71 | ;; 72 | 73 | "2") 74 | DATADIR="./gethdata2" 75 | P2P_PORT=30304 76 | RPC_PORT=8546 77 | run_geth 78 | ;; 79 | 80 | esac -------------------------------------------------------------------------------- /scripts/header.txt: -------------------------------------------------------------------------------- 1 | Copyright 2020 ChainSafe Systems 2 | SPDX-License-Identifier: LGPL-3.0-only -------------------------------------------------------------------------------- /scripts/install_subkey.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright 2020 ChainSafe Systems 3 | # SPDX-License-Identifier: LGPL-3.0-only 4 | 5 | 6 | # Exit on failure 7 | set -e 8 | 9 | # Setup cargo & rustc with nightly toolchain 10 | curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain nightly -y 11 | source $HOME/.cargo/env 12 | rustup target add wasm32-unknown-unknown --toolchain nightly 13 | 14 | # Clone and install subkey 15 | git clone --depth 1 https://github.com/paritytech/substrate/ 16 | cd substrate 17 | cargo install --force --path ./bin/utils/subkey subkey -------------------------------------------------------------------------------- /scripts/setup_contracts.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright 2020 ChainSafe Systems 3 | # SPDX-License-Identifier: LGPL-3.0-only 4 | 5 | CONTRACTS_REPO="https://github.com/ChainSafe/chainbridge-solidity" 6 | CONTRACTS_TAG="v1.0.0" 7 | CONTRACTS_DIR="./solidity" 8 | DEST_DIR="./bindings" 9 | 10 | set -eux 11 | 12 | case $TARGET in 13 | "build") 14 | git clone $CONTRACTS_REPO $CONTRACTS_DIR 15 | pushd $CONTRACTS_DIR 16 | git checkout $CONTRACTS_TAG 17 | 18 | make install-deps 19 | make bindings 20 | 21 | popd 22 | 23 | mkdir $DEST_DIR 24 | cp -r $CONTRACTS_DIR/build/bindings/go/* $DEST_DIR 25 | ;; 26 | 27 | "cli-only") 28 | git clone -b $CONTRACTS_BRANCH $CONTRACTS_REPO $CONTRACTS_DIR 29 | pushd $CONTRACTS_DIR 30 | git checkout $CONTRACTS_COMMIT 31 | ;; 32 | 33 | esac 34 | -------------------------------------------------------------------------------- /scripts/start_ganache.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright 2020 ChainSafe Systems 3 | # SPDX-License-Identifier: LGPL-3.0-only 4 | 5 | 6 | # Exit on failure 7 | set -ex 8 | 9 | PORT=${PORT:-8545} 10 | 11 | echo "Running ganache..." 12 | if [[ $SILENT ]]; then 13 | ganache-cli -q --blockTime 1 -p $PORT --account "0x000000000000000000000000000000000000000000000000000000616c696365,100000000000000000000" --account "0x0000000000000000000000000000000000000000000000000000000000626f62,100000000000000000000" --account "0x00000000000000000000000000000000000000000000000000636861726c6965,100000000000000000000" --account "0x0000000000000000000000000000000000000000000000000000000064617665,100000000000000000000" --account "0x0000000000000000000000000000000000000000000000000000000000657665,100000000000000000000" & 14 | # Otherwise CI will run tests before ganache has started 15 | sleep 3 16 | else 17 | ganache-cli --blockTime 1 -p $PORT --account "0x000000000000000000000000000000000000000000000000000000616c696365,100000000000000000000" --account "0x0000000000000000000000000000000000000000000000000000000000626f62,100000000000000000000" --account "0x00000000000000000000000000000000000000000000000000636861726c6965,100000000000000000000" --account "0x0000000000000000000000000000000000000000000000000000000064617665,100000000000000000000" --account "0x0000000000000000000000000000000000000000000000000000000000657665,100000000000000000000" 18 | fi -------------------------------------------------------------------------------- /shared/ethereum/bridge.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 ChainSafe Systems 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | package utils 5 | 6 | import ( 7 | "math/big" 8 | 9 | "github.com/ChainSafe/ChainBridge/bindings/Bridge" 10 | "github.com/ChainSafe/chainbridge-utils/msg" 11 | "github.com/ethereum/go-ethereum/common" 12 | ) 13 | 14 | func RegisterResource(client *Client, bridge, handler common.Address, rId msg.ResourceId, addr common.Address) error { 15 | instance, err := Bridge.NewBridge(bridge, client.Client) 16 | if err != nil { 17 | return err 18 | } 19 | 20 | err = client.LockNonceAndUpdate() 21 | if err != nil { 22 | return err 23 | } 24 | 25 | tx, err := instance.AdminSetResource(client.Opts, handler, rId, addr) 26 | if err != nil { 27 | return err 28 | } 29 | 30 | err = WaitForTx(client, tx) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | client.UnlockNonce() 36 | 37 | return nil 38 | } 39 | 40 | func RegisterGenericResource(client *Client, bridge, handler common.Address, rId msg.ResourceId, addr common.Address, depositSig, executeSig [4]byte) error { 41 | instance, err := Bridge.NewBridge(bridge, client.Client) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | err = client.LockNonceAndUpdate() 47 | if err != nil { 48 | return err 49 | } 50 | 51 | tx, err := instance.AdminSetGenericResource(client.Opts, handler, rId, addr, depositSig, executeSig) 52 | if err != nil { 53 | return err 54 | } 55 | 56 | err = WaitForTx(client, tx) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | client.UnlockNonce() 62 | 63 | return nil 64 | } 65 | 66 | func SetBurnable(client *Client, bridge, handler, contract common.Address) error { 67 | instance, err := Bridge.NewBridge(bridge, client.Client) 68 | if err != nil { 69 | return err 70 | } 71 | 72 | err = client.LockNonceAndUpdate() 73 | if err != nil { 74 | return err 75 | } 76 | 77 | tx, err := instance.AdminSetBurnable(client.Opts, handler, contract) 78 | if err != nil { 79 | return err 80 | } 81 | 82 | err = WaitForTx(client, tx) 83 | if err != nil { 84 | return err 85 | } 86 | 87 | client.UnlockNonce() 88 | 89 | return nil 90 | } 91 | 92 | func GetDepositNonce(client *Client, bridge common.Address, chain msg.ChainId) (uint64, error) { 93 | instance, err := Bridge.NewBridge(bridge, client.Client) 94 | if err != nil { 95 | return 0, err 96 | } 97 | 98 | count, err := instance.DepositCounts(client.CallOpts, uint8(chain)) 99 | if err != nil { 100 | return 0, err 101 | } 102 | 103 | return count, nil 104 | } 105 | 106 | func IDAndNonce(srcId msg.ChainId, nonce msg.Nonce) *big.Int { 107 | var data []byte 108 | data = append(data, nonce.Big().Bytes()...) 109 | data = append(data, uint8(srcId)) 110 | return big.NewInt(0).SetBytes(data) 111 | } 112 | -------------------------------------------------------------------------------- /shared/ethereum/centrifuge.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 ChainSafe Systems 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | package utils 5 | 6 | import ( 7 | "github.com/ChainSafe/ChainBridge/bindings/CentrifugeAsset" 8 | "github.com/ethereum/go-ethereum/common" 9 | ) 10 | 11 | func DeployAssetStore(client *Client) (common.Address, error) { 12 | err := client.LockNonceAndUpdate() 13 | if err != nil { 14 | return ZeroAddress, err 15 | } 16 | 17 | addr, tx, _, err := CentrifugeAsset.DeployCentrifugeAsset(client.Opts, client.Client) 18 | if err != nil { 19 | return ZeroAddress, err 20 | } 21 | 22 | err = WaitForTx(client, tx) 23 | if err != nil { 24 | return ZeroAddress, err 25 | } 26 | 27 | client.UnlockNonce() 28 | 29 | return addr, nil 30 | } 31 | 32 | func HashExists(client *Client, hash [32]byte, contract common.Address) (bool, error) { 33 | instance, err := CentrifugeAsset.NewCentrifugeAsset(contract, client.Client) 34 | if err != nil { 35 | return false, err 36 | } 37 | 38 | exists, err := instance.AssetsStored(client.CallOpts, hash) 39 | if err != nil { 40 | return false, err 41 | } 42 | return exists, nil 43 | } 44 | -------------------------------------------------------------------------------- /shared/ethereum/client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 ChainSafe Systems 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | package utils 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "fmt" 10 | "math/big" 11 | "sync" 12 | "time" 13 | 14 | "github.com/ethereum/go-ethereum" 15 | 16 | "github.com/ChainSafe/chainbridge-utils/crypto/secp256k1" 17 | "github.com/ethereum/go-ethereum/accounts/abi/bind" 18 | ethtypes "github.com/ethereum/go-ethereum/core/types" 19 | "github.com/ethereum/go-ethereum/ethclient" 20 | "github.com/ethereum/go-ethereum/rpc" 21 | ) 22 | 23 | const DefaultGasLimit = 6721975 24 | const DefaultMaxGasPrice = 20000000000 25 | const DefaultMinGasPrice = 0 26 | const DefaultGasMultiplier = 1 27 | 28 | var ExpectedBlockTime = time.Second 29 | 30 | type Client struct { 31 | Client *ethclient.Client 32 | Opts *bind.TransactOpts 33 | CallOpts *bind.CallOpts 34 | nonceLock sync.Mutex 35 | } 36 | 37 | func NewClient(endpoint string, kp *secp256k1.Keypair) (*Client, error) { 38 | ctx := context.Background() 39 | rpcClient, err := rpc.DialWebsocket(ctx, endpoint, "/ws") 40 | if err != nil { 41 | return nil, err 42 | } 43 | client := ethclient.NewClient(rpcClient) 44 | 45 | id, err := client.ChainID(ctx) 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | opts, err := bind.NewKeyedTransactorWithChainID(kp.PrivateKey(), id) 51 | if err != nil { 52 | return nil, err 53 | } 54 | opts.Nonce = big.NewInt(0) 55 | opts.Value = big.NewInt(0) // in wei 56 | opts.GasLimit = uint64(DefaultGasLimit) // in units 57 | opts.GasPrice = big.NewInt(DefaultMaxGasPrice) 58 | opts.Context = ctx 59 | 60 | return &Client{ 61 | Client: client, 62 | Opts: opts, 63 | CallOpts: &bind.CallOpts{ 64 | From: opts.From, 65 | }, 66 | }, nil 67 | } 68 | 69 | func (c *Client) LockNonceAndUpdate() error { 70 | c.nonceLock.Lock() 71 | nonce, err := c.Client.PendingNonceAt(context.Background(), c.Opts.From) 72 | if err != nil { 73 | c.nonceLock.Unlock() 74 | return err 75 | } 76 | c.Opts.Nonce.SetUint64(nonce) 77 | return nil 78 | } 79 | 80 | func (c *Client) UnlockNonce() { 81 | c.nonceLock.Unlock() 82 | } 83 | 84 | // WaitForTx will query the chain at ExpectedBlockTime intervals, until a receipt is returned. 85 | // Returns an error if the tx failed. 86 | func WaitForTx(client *Client, tx *ethtypes.Transaction) error { 87 | retry := 10 88 | for retry > 0 { 89 | receipt, err := client.Client.TransactionReceipt(context.Background(), tx.Hash()) 90 | if err != nil { 91 | if errors.Is(err, ethereum.NotFound) { 92 | retry-- 93 | time.Sleep(ExpectedBlockTime) 94 | continue 95 | } else { 96 | return err 97 | } 98 | } 99 | 100 | if receipt.Status != 1 { 101 | return fmt.Errorf("transaction failed on chain") 102 | } 103 | return nil 104 | } 105 | return nil 106 | } 107 | -------------------------------------------------------------------------------- /shared/ethereum/deploy.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 ChainSafe Systems 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | package utils 5 | 6 | import ( 7 | "context" 8 | "math/big" 9 | 10 | "github.com/ChainSafe/ChainBridge/bindings/GenericHandler" 11 | "github.com/ethereum/go-ethereum/common" 12 | 13 | bridge "github.com/ChainSafe/ChainBridge/bindings/Bridge" 14 | erc20Handler "github.com/ChainSafe/ChainBridge/bindings/ERC20Handler" 15 | erc721Handler "github.com/ChainSafe/ChainBridge/bindings/ERC721Handler" 16 | "github.com/ChainSafe/chainbridge-utils/keystore" 17 | ) 18 | 19 | var ( 20 | RelayerAddresses = []common.Address{ 21 | common.HexToAddress(keystore.TestKeyRing.EthereumKeys[keystore.AliceKey].Address()), 22 | common.HexToAddress(keystore.TestKeyRing.EthereumKeys[keystore.BobKey].Address()), 23 | common.HexToAddress(keystore.TestKeyRing.EthereumKeys[keystore.CharlieKey].Address()), 24 | common.HexToAddress(keystore.TestKeyRing.EthereumKeys[keystore.DaveKey].Address()), 25 | common.HexToAddress(keystore.TestKeyRing.EthereumKeys[keystore.EveKey].Address()), 26 | } 27 | 28 | ZeroAddress = common.HexToAddress("0x0000000000000000000000000000000000000000") 29 | ) 30 | 31 | type DeployedContracts struct { 32 | BridgeAddress common.Address 33 | ERC20HandlerAddress common.Address 34 | ERC721HandlerAddress common.Address 35 | GenericHandlerAddress common.Address 36 | } 37 | 38 | // DeployContracts deploys Bridge, Relayer, ERC20Handler, ERC721Handler and CentrifugeAssetHandler and returns the addresses 39 | func DeployContracts(client *Client, chainID uint8, initialRelayerThreshold *big.Int) (*DeployedContracts, error) { 40 | bridgeAddr, err := deployBridge(client, chainID, RelayerAddresses, initialRelayerThreshold) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | erc20HandlerAddr, err := deployERC20Handler(client, bridgeAddr) 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | erc721HandlerAddr, err := deployERC721Handler(client, bridgeAddr) 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | genericHandlerAddr, err := deployGenericHandler(client, bridgeAddr) 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | deployedContracts := DeployedContracts{bridgeAddr, erc20HandlerAddr, erc721HandlerAddr, genericHandlerAddr} 61 | 62 | return &deployedContracts, nil 63 | 64 | } 65 | 66 | func UpdateNonce(client *Client) error { 67 | newNonce, err := client.Client.PendingNonceAt(context.Background(), client.CallOpts.From) 68 | if err != nil { 69 | return err 70 | } 71 | 72 | client.Opts.Nonce = big.NewInt(int64(newNonce)) 73 | 74 | return nil 75 | } 76 | 77 | func deployBridge(client *Client, chainID uint8, relayerAddrs []common.Address, initialRelayerThreshold *big.Int) (common.Address, error) { 78 | err := client.LockNonceAndUpdate() 79 | if err != nil { 80 | return ZeroAddress, err 81 | } 82 | 83 | bridgeAddr, tx, _, err := bridge.DeployBridge(client.Opts, client.Client, chainID, relayerAddrs, initialRelayerThreshold, big.NewInt(0), big.NewInt(100)) 84 | if err != nil { 85 | return ZeroAddress, err 86 | } 87 | 88 | err = WaitForTx(client, tx) 89 | if err != nil { 90 | return ZeroAddress, err 91 | } 92 | 93 | client.UnlockNonce() 94 | 95 | return bridgeAddr, nil 96 | 97 | } 98 | 99 | func deployERC20Handler(client *Client, bridgeAddress common.Address) (common.Address, error) { 100 | err := client.LockNonceAndUpdate() 101 | if err != nil { 102 | return ZeroAddress, err 103 | } 104 | 105 | erc20HandlerAddr, tx, _, err := erc20Handler.DeployERC20Handler(client.Opts, client.Client, bridgeAddress, [][32]byte{}, []common.Address{}, []common.Address{}) 106 | if err != nil { 107 | return ZeroAddress, err 108 | } 109 | 110 | err = WaitForTx(client, tx) 111 | if err != nil { 112 | return ZeroAddress, err 113 | } 114 | 115 | client.UnlockNonce() 116 | 117 | return erc20HandlerAddr, nil 118 | } 119 | 120 | func deployERC721Handler(client *Client, bridgeAddress common.Address) (common.Address, error) { 121 | err := client.LockNonceAndUpdate() 122 | if err != nil { 123 | return ZeroAddress, err 124 | } 125 | 126 | erc721HandlerAddr, tx, _, err := erc721Handler.DeployERC721Handler(client.Opts, client.Client, bridgeAddress, [][32]byte{}, []common.Address{}, []common.Address{}) 127 | if err != nil { 128 | return ZeroAddress, err 129 | } 130 | err = WaitForTx(client, tx) 131 | if err != nil { 132 | return ZeroAddress, err 133 | } 134 | 135 | client.UnlockNonce() 136 | 137 | return erc721HandlerAddr, nil 138 | } 139 | 140 | func deployGenericHandler(client *Client, bridgeAddress common.Address) (common.Address, error) { 141 | err := client.LockNonceAndUpdate() 142 | if err != nil { 143 | return ZeroAddress, err 144 | } 145 | 146 | addr, tx, _, err := GenericHandler.DeployGenericHandler(client.Opts, client.Client, bridgeAddress, [][32]byte{}, []common.Address{}, [][4]byte{}, [][4]byte{}) 147 | if err != nil { 148 | return ZeroAddress, err 149 | } 150 | 151 | err = WaitForTx(client, tx) 152 | if err != nil { 153 | return ZeroAddress, err 154 | } 155 | 156 | client.UnlockNonce() 157 | 158 | return addr, nil 159 | } 160 | -------------------------------------------------------------------------------- /shared/ethereum/deposit.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 ChainSafe Systems 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | package utils 5 | 6 | import ( 7 | "math/big" 8 | 9 | "github.com/ethereum/go-ethereum/common/math" 10 | ) 11 | 12 | // constructErc20Data constructs the data field to be passed into a n erc20 deposit call 13 | func ConstructErc20DepositData(destRecipient []byte, amount *big.Int) []byte { 14 | var data []byte 15 | data = append(data, math.PaddedBigBytes(amount, 32)...) 16 | data = append(data, math.PaddedBigBytes(big.NewInt(int64(len(destRecipient))), 32)...) 17 | data = append(data, destRecipient...) 18 | return data 19 | } 20 | 21 | // constructErc20Data constructs the data field to be passed into an erc721 deposit call 22 | func ConstructErc721DepositData(tokenId *big.Int, destRecipient []byte) []byte { 23 | var data []byte 24 | data = append(data, math.PaddedBigBytes(tokenId, 32)...) // Resource Id + Token Id 25 | data = append(data, math.PaddedBigBytes(big.NewInt(int64(len(destRecipient))), 32)...) // Length of recipient 26 | data = append(data, destRecipient...) // Recipient 27 | 28 | return data 29 | } 30 | 31 | func ConstructGenericDepositData(metadata []byte) []byte { 32 | var data []byte 33 | data = append(data, math.PaddedBigBytes(big.NewInt(int64(len(metadata))), 32)...) 34 | data = append(data, metadata...) 35 | 36 | return data 37 | } 38 | -------------------------------------------------------------------------------- /shared/ethereum/erc20.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 ChainSafe Systems 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | package utils 5 | 6 | import ( 7 | "math/big" 8 | 9 | "github.com/ChainSafe/ChainBridge/bindings/ERC20Handler" 10 | ERC20 "github.com/ChainSafe/ChainBridge/bindings/ERC20PresetMinterPauser" 11 | "github.com/ChainSafe/chainbridge-utils/msg" 12 | "github.com/ethereum/go-ethereum/common" 13 | ) 14 | 15 | // DeployMintAndApprove deploys a new erc20 contract, mints to the deployer, and approves the erc20 handler to transfer those token. 16 | func DeployMintApproveErc20(client *Client, erc20Handler common.Address, amount *big.Int) (common.Address, error) { 17 | err := client.LockNonceAndUpdate() 18 | if err != nil { 19 | return ZeroAddress, err 20 | } 21 | 22 | // Deploy 23 | erc20Addr, tx, erc20Instance, err := ERC20.DeployERC20PresetMinterPauser(client.Opts, client.Client, "", "") 24 | if err != nil { 25 | return ZeroAddress, err 26 | } 27 | 28 | err = WaitForTx(client, tx) 29 | if err != nil { 30 | return ZeroAddress, err 31 | } 32 | 33 | client.UnlockNonce() 34 | 35 | // Mint 36 | err = client.LockNonceAndUpdate() 37 | if err != nil { 38 | return ZeroAddress, err 39 | } 40 | 41 | tx, err = erc20Instance.Mint(client.Opts, client.Opts.From, amount) 42 | if err != nil { 43 | return ZeroAddress, err 44 | } 45 | 46 | err = WaitForTx(client, tx) 47 | if err != nil { 48 | return ZeroAddress, err 49 | } 50 | 51 | client.UnlockNonce() 52 | 53 | // Approve 54 | err = client.LockNonceAndUpdate() 55 | if err != nil { 56 | return ZeroAddress, err 57 | } 58 | 59 | tx, err = erc20Instance.Approve(client.Opts, erc20Handler, amount) 60 | if err != nil { 61 | return ZeroAddress, err 62 | } 63 | 64 | err = WaitForTx(client, tx) 65 | if err != nil { 66 | return ZeroAddress, err 67 | } 68 | 69 | client.UnlockNonce() 70 | 71 | return erc20Addr, nil 72 | } 73 | 74 | func DeployAndMintErc20(client *Client, amount *big.Int) (common.Address, error) { 75 | err := client.LockNonceAndUpdate() 76 | if err != nil { 77 | return ZeroAddress, err 78 | } 79 | 80 | // Deploy 81 | erc20Addr, tx, erc20Instance, err := ERC20.DeployERC20PresetMinterPauser(client.Opts, client.Client, "", "") 82 | if err != nil { 83 | return ZeroAddress, err 84 | } 85 | 86 | err = WaitForTx(client, tx) 87 | if err != nil { 88 | return ZeroAddress, err 89 | } 90 | client.UnlockNonce() 91 | 92 | // Mint 93 | err = client.LockNonceAndUpdate() 94 | if err != nil { 95 | return ZeroAddress, err 96 | } 97 | 98 | mintTx, err := erc20Instance.Mint(client.Opts, client.Opts.From, amount) 99 | if err != nil { 100 | return ZeroAddress, err 101 | } 102 | 103 | err = WaitForTx(client, mintTx) 104 | if err != nil { 105 | return ZeroAddress, err 106 | } 107 | 108 | client.UnlockNonce() 109 | 110 | return erc20Addr, nil 111 | } 112 | 113 | func Erc20Approve(client *Client, erc20Contract, recipient common.Address, amount *big.Int) error { 114 | err := client.LockNonceAndUpdate() 115 | if err != nil { 116 | return err 117 | } 118 | 119 | instance, err := ERC20.NewERC20PresetMinterPauser(erc20Contract, client.Client) 120 | if err != nil { 121 | return err 122 | } 123 | 124 | tx, err := instance.Approve(client.Opts, recipient, amount) 125 | if err != nil { 126 | return err 127 | } 128 | 129 | err = WaitForTx(client, tx) 130 | if err != nil { 131 | return err 132 | } 133 | 134 | client.UnlockNonce() 135 | 136 | return nil 137 | } 138 | 139 | func Erc20GetBalance(client *Client, erc20Contract, account common.Address) (*big.Int, error) { //nolint:unused,deadcode 140 | instance, err := ERC20.NewERC20PresetMinterPauser(erc20Contract, client.Client) 141 | if err != nil { 142 | return nil, err 143 | } 144 | 145 | bal, err := instance.BalanceOf(client.CallOpts, account) 146 | if err != nil { 147 | return nil, err 148 | 149 | } 150 | return bal, nil 151 | 152 | } 153 | 154 | func FundErc20Handler(client *Client, handlerAddress, erc20Address common.Address, amount *big.Int) error { 155 | err := Erc20Approve(client, erc20Address, handlerAddress, amount) 156 | if err != nil { 157 | return err 158 | } 159 | 160 | instance, err := ERC20Handler.NewERC20Handler(handlerAddress, client.Client) 161 | if err != nil { 162 | return err 163 | } 164 | 165 | client.Opts.Nonce = client.Opts.Nonce.Add(client.Opts.Nonce, big.NewInt(1)) 166 | tx, err := instance.FundERC20(client.Opts, erc20Address, client.Opts.From, amount) 167 | if err != nil { 168 | return err 169 | } 170 | 171 | err = WaitForTx(client, tx) 172 | if err != nil { 173 | return err 174 | } 175 | 176 | return nil 177 | } 178 | 179 | func Erc20AddMinter(client *Client, erc20Contract, handler common.Address) error { 180 | err := client.LockNonceAndUpdate() 181 | if err != nil { 182 | return err 183 | } 184 | 185 | instance, err := ERC20.NewERC20PresetMinterPauser(erc20Contract, client.Client) 186 | if err != nil { 187 | return err 188 | } 189 | 190 | role, err := instance.MINTERROLE(client.CallOpts) 191 | if err != nil { 192 | return err 193 | } 194 | 195 | tx, err := instance.GrantRole(client.Opts, role, handler) 196 | if err != nil { 197 | return err 198 | } 199 | 200 | err = WaitForTx(client, tx) 201 | if err != nil { 202 | return err 203 | } 204 | 205 | client.UnlockNonce() 206 | 207 | return nil 208 | } 209 | 210 | func Erc20GetAllowance(client *Client, erc20Contract, owner, spender common.Address) (*big.Int, error) { 211 | instance, err := ERC20.NewERC20PresetMinterPauser(erc20Contract, client.Client) 212 | if err != nil { 213 | return nil, err 214 | } 215 | 216 | amount, err := instance.Allowance(client.CallOpts, owner, spender) 217 | if err != nil { 218 | return nil, err 219 | } 220 | 221 | return amount, nil 222 | } 223 | 224 | func Erc20GetResourceId(client *Client, handler common.Address, rId msg.ResourceId) (common.Address, error) { 225 | instance, err := ERC20Handler.NewERC20Handler(handler, client.Client) 226 | if err != nil { 227 | return ZeroAddress, err 228 | } 229 | 230 | addr, err := instance.ResourceIDToTokenContractAddress(client.CallOpts, rId) 231 | if err != nil { 232 | return ZeroAddress, err 233 | } 234 | 235 | return addr, nil 236 | } 237 | 238 | func Erc20Mint(client *Client, erc20Address, recipient common.Address, amount *big.Int) error { 239 | err := client.LockNonceAndUpdate() 240 | if err != nil { 241 | return err 242 | } 243 | 244 | instance, err := ERC20.NewERC20PresetMinterPauser(erc20Address, client.Client) 245 | if err != nil { 246 | return err 247 | } 248 | 249 | tx, err := instance.Mint(client.Opts, recipient, amount) 250 | if err != nil { 251 | return err 252 | } 253 | 254 | err = WaitForTx(client, tx) 255 | if err != nil { 256 | return err 257 | } 258 | 259 | client.UnlockNonce() 260 | 261 | return nil 262 | } 263 | -------------------------------------------------------------------------------- /shared/ethereum/erc721.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 ChainSafe Systems 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | package utils 5 | 6 | import ( 7 | "math/big" 8 | 9 | "github.com/ChainSafe/ChainBridge/bindings/ERC721Handler" 10 | "github.com/ChainSafe/ChainBridge/bindings/ERC721MinterBurnerPauser" 11 | "github.com/ethereum/go-ethereum/common" 12 | ) 13 | 14 | // DeployMintAndApprove deploys a new erc721 contract, mints to the deployer, and approves the erc20 handler to transfer those token. 15 | func DeployErc721(client *Client) (common.Address, error) { 16 | err := client.LockNonceAndUpdate() 17 | if err != nil { 18 | return ZeroAddress, err 19 | } 20 | 21 | // Deploy 22 | addr, tx, _, err := ERC721MinterBurnerPauser.DeployERC721MinterBurnerPauser(client.Opts, client.Client, "", "", "") 23 | if err != nil { 24 | return ZeroAddress, err 25 | } 26 | 27 | err = WaitForTx(client, tx) 28 | if err != nil { 29 | return ZeroAddress, err 30 | } 31 | 32 | client.UnlockNonce() 33 | 34 | return addr, nil 35 | } 36 | 37 | func Erc721Mint(client *Client, erc721Contract common.Address, id *big.Int, metadata []byte) error { 38 | instance, err := ERC721MinterBurnerPauser.NewERC721MinterBurnerPauser(erc721Contract, client.Client) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | err = client.LockNonceAndUpdate() 44 | if err != nil { 45 | return err 46 | } 47 | 48 | // Mint 49 | tx, err := instance.Mint(client.Opts, client.Opts.From, id, string(metadata)) 50 | if err != nil { 51 | return err 52 | } 53 | 54 | err = WaitForTx(client, tx) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | client.UnlockNonce() 60 | 61 | return nil 62 | } 63 | 64 | func ApproveErc721(client *Client, contractAddress, recipient common.Address, tokenId *big.Int) error { 65 | err := client.LockNonceAndUpdate() 66 | if err != nil { 67 | return err 68 | } 69 | 70 | instance, err := ERC721MinterBurnerPauser.NewERC721MinterBurnerPauser(contractAddress, client.Client) 71 | if err != nil { 72 | return err 73 | } 74 | 75 | tx, err := instance.Approve(client.Opts, recipient, tokenId) 76 | if err != nil { 77 | return err 78 | } 79 | 80 | err = WaitForTx(client, tx) 81 | if err != nil { 82 | return err 83 | } 84 | 85 | client.UnlockNonce() 86 | 87 | return nil 88 | } 89 | 90 | func FundErc721Handler(client *Client, handlerAddress, erc721Address common.Address, tokenId *big.Int) error { 91 | err := ApproveErc721(client, erc721Address, handlerAddress, tokenId) 92 | if err != nil { 93 | return err 94 | } 95 | 96 | instance, err := ERC721Handler.NewERC721Handler(handlerAddress, client.Client) 97 | if err != nil { 98 | return err 99 | } 100 | 101 | err = client.LockNonceAndUpdate() 102 | if err != nil { 103 | return err 104 | } 105 | 106 | tx, err := instance.FundERC721(client.Opts, erc721Address, client.Opts.From, tokenId) 107 | if err != nil { 108 | return err 109 | } 110 | 111 | err = WaitForTx(client, tx) 112 | if err != nil { 113 | return err 114 | } 115 | 116 | client.UnlockNonce() 117 | 118 | return nil 119 | } 120 | 121 | func OwnerOf(client *Client, erc721Contract common.Address, tokenId *big.Int) (common.Address, error) { 122 | instance, err := ERC721MinterBurnerPauser.NewERC721MinterBurnerPauser(erc721Contract, client.Client) 123 | if err != nil { 124 | return ZeroAddress, err 125 | } 126 | return instance.OwnerOf(client.CallOpts, tokenId) 127 | } 128 | 129 | func Erc721GetTokenURI(client *Client, erc721Contract common.Address, tokenId *big.Int) (string, error) { 130 | instance, err := ERC721MinterBurnerPauser.NewERC721MinterBurnerPauser(erc721Contract, client.Client) 131 | if err != nil { 132 | return "", err 133 | } 134 | 135 | return instance.TokenURI(client.CallOpts, tokenId) 136 | } 137 | 138 | func Erc721AddMinter(client *Client, erc721Contract common.Address, minter common.Address) error { 139 | instance, err := ERC721MinterBurnerPauser.NewERC721MinterBurnerPauser(erc721Contract, client.Client) 140 | if err != nil { 141 | return err 142 | } 143 | 144 | err = client.LockNonceAndUpdate() 145 | if err != nil { 146 | return err 147 | } 148 | 149 | role, err := instance.MINTERROLE(client.CallOpts) 150 | if err != nil { 151 | return err 152 | } 153 | 154 | tx, err := instance.GrantRole(client.Opts, role, minter) 155 | if err != nil { 156 | return err 157 | } 158 | 159 | err = WaitForTx(client, tx) 160 | if err != nil { 161 | return err 162 | } 163 | 164 | client.UnlockNonce() 165 | 166 | return nil 167 | } 168 | -------------------------------------------------------------------------------- /shared/ethereum/events.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 ChainSafe Systems 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | package utils 5 | 6 | import ( 7 | "github.com/ethereum/go-ethereum/common" 8 | "github.com/ethereum/go-ethereum/crypto" 9 | ) 10 | 11 | type EventSig string 12 | 13 | func (es EventSig) GetTopic() common.Hash { 14 | return crypto.Keccak256Hash([]byte(es)) 15 | } 16 | 17 | const ( 18 | Deposit EventSig = "Deposit(uint8,bytes32,uint64)" 19 | ProposalEvent EventSig = "ProposalEvent(uint8,uint64,uint8,bytes32,bytes32)" 20 | ProposalVote EventSig = "ProposalVote(uint8,uint64,uint8,bytes32)" 21 | ) 22 | 23 | type ProposalStatus int 24 | 25 | const ( 26 | Inactive ProposalStatus = iota 27 | Active 28 | Passed 29 | Executed 30 | Cancelled 31 | ) 32 | 33 | func IsActive(status uint8) bool { 34 | return ProposalStatus(status) == Active 35 | } 36 | 37 | func IsFinalized(status uint8) bool { 38 | return ProposalStatus(status) == Passed 39 | } 40 | 41 | func IsExecuted(status uint8) bool { 42 | return ProposalStatus(status) == Executed 43 | } 44 | -------------------------------------------------------------------------------- /shared/ethereum/generic.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 ChainSafe Systems 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | package utils 5 | 6 | import ( 7 | "fmt" 8 | "math/big" 9 | "strings" 10 | 11 | "github.com/ChainSafe/ChainBridge/bindings/GenericHandler" 12 | "github.com/ChainSafe/chainbridge-utils/msg" 13 | "github.com/ethereum/go-ethereum/common" 14 | ) 15 | 16 | var StoreFunctionSig = CreateFunctionSignature("store(bytes32)") 17 | 18 | // CreateFunctionSignature hashes the function signature and returns the first 4 bytes 19 | func CreateFunctionSignature(sig string) [4]byte { 20 | var res [4]byte 21 | hash := Hash([]byte(sig)) 22 | copy(res[:], hash[:]) 23 | return res 24 | } 25 | 26 | func GetGenericResourceAddress(client *Client, handler common.Address, rId msg.ResourceId) (common.Address, error) { 27 | instance, err := GenericHandler.NewGenericHandler(handler, client.Client) 28 | if err != nil { 29 | return ZeroAddress, err 30 | } 31 | 32 | addr, err := instance.ResourceIDToContractAddress(client.CallOpts, rId) 33 | if err != nil { 34 | return ZeroAddress, err 35 | } 36 | return addr, nil 37 | } 38 | 39 | const ( 40 | decimalBase = 10 41 | hexBase = 16 42 | ) 43 | 44 | // valueToBig converts a string value to a *big.Int in the provided base 45 | func valueToBig(value string, base int) (*big.Int, error) { 46 | val, ok := new(big.Int).SetString(value, base) 47 | if !ok { 48 | return nil, fmt.Errorf("unable to parse value") 49 | } 50 | 51 | return val, nil 52 | } 53 | 54 | // ParseUint256OrHex parses a string value as either a base 10 number 55 | // or as a hex value 56 | func ParseUint256OrHex(value *string) (*big.Int, error) { 57 | // Check if the value is valid 58 | if value == nil { 59 | return nil, fmt.Errorf("invalid value") 60 | } 61 | 62 | // Check if the value is hex 63 | if strings.HasPrefix(*value, "0x") { 64 | // Hex value, remove the prefix and parse it 65 | clipped := (*value)[2:] 66 | return valueToBig(clipped, hexBase) 67 | } 68 | 69 | // Decimal number, parse it 70 | return valueToBig(*value, decimalBase) 71 | } 72 | -------------------------------------------------------------------------------- /shared/ethereum/hash.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 ChainSafe Systems 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | package utils 5 | 6 | import ( 7 | "github.com/ethereum/go-ethereum/crypto" 8 | ) 9 | 10 | func Hash(data []byte) [32]byte { 11 | return crypto.Keccak256Hash(data) 12 | } 13 | -------------------------------------------------------------------------------- /shared/ethereum/testing/bridge.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 ChainSafe Systems 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | package ethtest 5 | 6 | import ( 7 | "testing" 8 | 9 | utils "github.com/ChainSafe/ChainBridge/shared/ethereum" 10 | "github.com/ChainSafe/chainbridge-utils/msg" 11 | "github.com/ethereum/go-ethereum/common" 12 | ) 13 | 14 | func RegisterResource(t *testing.T, client *utils.Client, bridge, handler common.Address, rId msg.ResourceId, addr common.Address) { 15 | err := utils.RegisterResource(client, bridge, handler, rId, addr) 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | } 20 | 21 | func RegisterGenericResource(t *testing.T, client *utils.Client, bridge, handler common.Address, rId msg.ResourceId, addr common.Address, depositSig, executeSig [4]byte) { 22 | err := utils.RegisterGenericResource(client, bridge, handler, rId, addr, depositSig, executeSig) 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | } 27 | 28 | func SetBurnable(t *testing.T, client *utils.Client, bridge, handler, contract common.Address) { 29 | err := utils.SetBurnable(client, bridge, handler, contract) 30 | if err != nil { 31 | t.Fatal(err) 32 | } 33 | } 34 | 35 | func GetDepositNonce(t *testing.T, client *utils.Client, bridge common.Address, chain msg.ChainId) uint64 { 36 | count, err := utils.GetDepositNonce(client, bridge, chain) 37 | if err != nil { 38 | t.Fatal(err) 39 | } 40 | return count 41 | } 42 | -------------------------------------------------------------------------------- /shared/ethereum/testing/centrifuge.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 ChainSafe Systems 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | package ethtest 5 | 6 | import ( 7 | "testing" 8 | 9 | utils "github.com/ChainSafe/ChainBridge/shared/ethereum" 10 | "github.com/ChainSafe/log15" 11 | "github.com/ethereum/go-ethereum/common" 12 | ) 13 | 14 | func DeployAssetStore(t *testing.T, client *utils.Client) common.Address { 15 | addr, err := utils.DeployAssetStore(client) 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | return addr 20 | } 21 | 22 | func AssertHashExistence(t *testing.T, client *utils.Client, hash [32]byte, contract common.Address) { 23 | exists, err := utils.HashExists(client, hash, contract) 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | if !exists { 28 | t.Fatalf("Hash %x does not exist on chain", hash) 29 | } 30 | log15.Info("Assert existence in asset store", "hash", hash, "assetStore", contract) 31 | } 32 | -------------------------------------------------------------------------------- /shared/ethereum/testing/client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 ChainSafe Systems 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | package ethtest 5 | 6 | import ( 7 | "context" 8 | "math/big" 9 | "testing" 10 | 11 | utils "github.com/ChainSafe/ChainBridge/shared/ethereum" 12 | "github.com/ChainSafe/chainbridge-utils/crypto/secp256k1" 13 | ) 14 | 15 | func NewClient(t *testing.T, endpoint string, kp *secp256k1.Keypair) *utils.Client { 16 | client, err := utils.NewClient(endpoint, kp) 17 | if err != nil { 18 | t.Fatal(err) 19 | } 20 | return client 21 | } 22 | 23 | func GetLatestBlock(t *testing.T, client *utils.Client) *big.Int { 24 | block, err := client.Client.BlockByNumber(context.Background(), nil) 25 | if err != nil { 26 | t.Fatal(err) 27 | } 28 | return block.Number() 29 | } 30 | 31 | func LockNonceAndUpdate(t *testing.T, client *utils.Client) { 32 | err := client.LockNonceAndUpdate() 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /shared/ethereum/testing/erc20.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 ChainSafe Systems 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | package ethtest 5 | 6 | import ( 7 | "math/big" 8 | "testing" 9 | 10 | utils "github.com/ChainSafe/ChainBridge/shared/ethereum" 11 | "github.com/ChainSafe/chainbridge-utils/msg" 12 | "github.com/ChainSafe/log15" 13 | "github.com/ethereum/go-ethereum/common" 14 | ) 15 | 16 | func DeployMintApproveErc20(t *testing.T, client *utils.Client, erc20Handler common.Address, amount *big.Int) common.Address { 17 | addr, err := utils.DeployMintApproveErc20(client, erc20Handler, amount) 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | return addr 22 | } 23 | 24 | func Erc20DeployMint(t *testing.T, client *utils.Client, amount *big.Int) common.Address { 25 | addr, err := utils.DeployAndMintErc20(client, amount) 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | return addr 30 | } 31 | 32 | func Erc20Mint(t *testing.T, client *utils.Client, erc20Contract, recipient common.Address, amount *big.Int) { 33 | err := utils.Erc20Mint(client, erc20Contract, recipient, amount) 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | } 38 | 39 | func Erc20Approve(t *testing.T, client *utils.Client, erc20Contract, recipient common.Address, amount *big.Int) { 40 | err := utils.Erc20Approve(client, erc20Contract, recipient, amount) 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | } 45 | 46 | func Erc20AssertBalance(t *testing.T, client *utils.Client, amount *big.Int, erc20Contract, account common.Address) { //nolint:unused,deadcode 47 | actual, err := utils.Erc20GetBalance(client, erc20Contract, account) 48 | if err != nil { 49 | t.Fatal(err) 50 | } 51 | 52 | if actual.Cmp(amount) != 0 { 53 | t.Fatalf("Balance mismatch. Expected: %s Got: %s", amount.String(), actual.String()) 54 | } 55 | log15.Info("Asserted balance", "account", account, "balance", actual, "erc20Contract", erc20Contract.Hex()) 56 | } 57 | 58 | func FundErc20Handler(t *testing.T, client *utils.Client, handlerAddress, erc20Address common.Address, amount *big.Int) { 59 | err := utils.FundErc20Handler(client, handlerAddress, erc20Address, amount) 60 | if err != nil { 61 | t.Fatal(err) 62 | } 63 | } 64 | 65 | func Erc20BalanceOf(t *testing.T, client *utils.Client, erc20Contract, acct common.Address) *big.Int { 66 | balance, err := utils.Erc20GetBalance(client, erc20Contract, acct) 67 | if err != nil { 68 | t.Fatal(err) 69 | } 70 | return balance 71 | } 72 | 73 | func Erc20AddMinter(t *testing.T, client *utils.Client, erc20Contract, handler common.Address) { 74 | err := utils.Erc20AddMinter(client, erc20Contract, handler) 75 | if err != nil { 76 | t.Fatal(err) 77 | } 78 | } 79 | 80 | func Erc20AssertAllowance(t *testing.T, client *utils.Client, erc20Contract, owner, spender common.Address, expected *big.Int) { 81 | amount, err := utils.Erc20GetAllowance(client, erc20Contract, owner, spender) 82 | if err != nil { 83 | t.Fatal(err) 84 | } 85 | 86 | if amount.Cmp(expected) != 0 { 87 | t.Fatalf("Allowance mismatch. Expected: %s Got: %s", expected.String(), amount.String()) 88 | } 89 | log15.Info("Asserted allowance", "owner", owner, "spender", spender, "amount", amount, "erc20Contract", erc20Contract.Hex()) 90 | } 91 | 92 | func Erc20AssertResourceMapping(t *testing.T, client *utils.Client, handler common.Address, rId msg.ResourceId, expected common.Address) { 93 | addr, err := utils.Erc20GetResourceId(client, handler, rId) 94 | if err != nil { 95 | t.Fatal(err) 96 | } 97 | 98 | if addr.String() != expected.String() { 99 | t.Fatalf("Unexpected address for resource ID %x. Expected: %x Got: %x", rId, expected, addr) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /shared/ethereum/testing/erc721.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 ChainSafe Systems 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | package ethtest 5 | 6 | import ( 7 | "errors" 8 | "math/big" 9 | "testing" 10 | 11 | utils "github.com/ChainSafe/ChainBridge/shared/ethereum" 12 | "github.com/ChainSafe/log15" 13 | "github.com/ethereum/go-ethereum/common" 14 | ) 15 | 16 | // Erc721 is used for convenience to represent an Erc721 17 | type Erc721 struct { 18 | Id *big.Int 19 | Metadata [32]byte 20 | } 21 | 22 | func GenerateErc721Tokens(start int, numOfTokens int) []Erc721 { 23 | var res []Erc721 24 | for i := start; i < start+numOfTokens; i++ { 25 | token := Erc721{ 26 | Id: big.NewInt(int64(i)), 27 | Metadata: utils.Hash([]byte{byte(i)}), 28 | } 29 | res = append(res, token) 30 | } 31 | return res 32 | } 33 | 34 | func Erc721Deploy(t *testing.T, client *utils.Client) common.Address { 35 | addr, err := utils.DeployErc721(client) 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | return addr 40 | } 41 | 42 | func Erc721Mint(t *testing.T, client *utils.Client, erc721Contract common.Address, id *big.Int, metadata []byte) { 43 | log15.Info("Minting erc721 token", "contract", erc721Contract.Hex(), "Id", id) 44 | err := utils.Erc721Mint(client, erc721Contract, id, metadata) 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | } 49 | 50 | func Erc721MintMany(t *testing.T, client *utils.Client, erc721Contract common.Address, tokens []Erc721) { 51 | for _, tok := range tokens { 52 | Erc721Mint(t, client, erc721Contract, tok.Id, tok.Metadata[:]) 53 | } 54 | } 55 | 56 | func Erc721Approve(t *testing.T, client *utils.Client, erc721Contract, recipient common.Address, tokenId *big.Int) { 57 | log15.Info("Approving erc721 token for transfer", "contract", erc721Contract.Hex(), "Id", tokenId, "recipient", recipient) 58 | err := utils.ApproveErc721(client, erc721Contract, recipient, tokenId) 59 | if err != nil { 60 | t.Fatal(err) 61 | } 62 | } 63 | 64 | func Erc721ApproveMany(t *testing.T, client *utils.Client, erc721Contract, recipient common.Address, tokens []Erc721) { 65 | for _, tok := range tokens { 66 | Erc721Approve(t, client, erc721Contract, recipient, tok.Id) 67 | } 68 | } 69 | 70 | func Erc721AssertOwner(t *testing.T, client *utils.Client, erc721Contract common.Address, tokenId *big.Int, expected common.Address) { 71 | addr, err := utils.OwnerOf(client, erc721Contract, tokenId) 72 | if err != nil { 73 | t.Fatal(err) 74 | } 75 | 76 | if addr != expected { 77 | t.Fatalf("address %s does not own %x, %s does", expected.Hex(), tokenId.Bytes(), addr.Hex()) 78 | } 79 | log15.Info("Asserted ownership of erc721", "tokenId", tokenId, "owner", addr) 80 | } 81 | 82 | func Erc721FundHandler(t *testing.T, client *utils.Client, handler, erc721Contract common.Address, tokenId *big.Int) { 83 | err := utils.FundErc721Handler(client, handler, erc721Contract, tokenId) 84 | if err != nil { 85 | t.Fatal(err) 86 | } 87 | log15.Info("Funded handler with erc721 token", "tokenId", tokenId, "handler", handler) 88 | } 89 | 90 | func Erc721FundHandlerMany(t *testing.T, client *utils.Client, handler, erc721Contract common.Address, start, numberOfTokens int) { 91 | for i := start; i < start+numberOfTokens; i++ { 92 | Erc721FundHandler(t, client, handler, erc721Contract, big.NewInt(int64(i))) 93 | } 94 | } 95 | 96 | func Erc721AddMinter(t *testing.T, client *utils.Client, erc721Contract common.Address, minter common.Address) { 97 | err := utils.Erc721AddMinter(client, erc721Contract, minter) 98 | if err != nil { 99 | t.Fatal(err) 100 | } 101 | } 102 | 103 | func Erc721AssertMetadata(t *testing.T, client *utils.Client, erc721Contract common.Address, tokenId *big.Int, expected string) { 104 | actual, err := utils.Erc721GetTokenURI(client, erc721Contract, tokenId) 105 | if err != nil { 106 | t.Fatal(err) 107 | } 108 | 109 | if actual != expected { 110 | t.Fatalf("erc721 metadata mismatch for token %s. Expected: %s Got: %s", tokenId.String(), expected, actual) 111 | } 112 | } 113 | 114 | var NonExistentTokenError = errors.New("VM Exception while processing transaction: revert ERC721: owner query for nonexistent token") 115 | 116 | func Erc721AssertNonExistence(t *testing.T, client *utils.Client, erc721Contract common.Address, id *big.Int) { 117 | _, err := utils.OwnerOf(client, erc721Contract, id) 118 | // TODO: Assert actual revert, not possible with geth currently 119 | if err != nil { 120 | //if err.Error() != NonExistentTokenError.Error() { 121 | // t.Fatal(err) 122 | //} 123 | log15.Info("Asserted non-existence of erc721", "tokenId", id, "result", err) 124 | } 125 | 126 | } 127 | -------------------------------------------------------------------------------- /shared/ethereum/testing/events.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 ChainSafe Systems 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | package ethtest 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "math/big" 10 | 11 | utils "github.com/ChainSafe/ChainBridge/shared/ethereum" 12 | "github.com/ChainSafe/log15" 13 | eth "github.com/ethereum/go-ethereum" 14 | "github.com/ethereum/go-ethereum/common" 15 | ethtypes "github.com/ethereum/go-ethereum/core/types" 16 | ) 17 | 18 | func WatchEvent(client *utils.Client, bridge common.Address, subStr utils.EventSig) { 19 | fmt.Printf("Watching for event: %s\n", subStr) 20 | query := eth.FilterQuery{ 21 | FromBlock: big.NewInt(0), 22 | Addresses: []common.Address{bridge}, 23 | Topics: [][]common.Hash{ 24 | {subStr.GetTopic()}, 25 | }, 26 | } 27 | 28 | ch := make(chan ethtypes.Log) 29 | sub, err := client.Client.SubscribeFilterLogs(context.Background(), query, ch) 30 | if err != nil { 31 | log15.Error("Failed to subscribe to event", "event", subStr) 32 | return 33 | } 34 | defer sub.Unsubscribe() 35 | 36 | for { 37 | select { 38 | case evt := <-ch: 39 | fmt.Printf("%s (block: %d): %#v\n", subStr, evt.BlockNumber, evt.Topics) 40 | 41 | case err := <-sub.Err(): 42 | if err != nil { 43 | log15.Error("Subscription error", "event", subStr, "err", err) 44 | return 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /shared/ethereum/testing/generic.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 ChainSafe Systems 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | package ethtest 5 | 6 | import ( 7 | "bytes" 8 | "testing" 9 | 10 | utils "github.com/ChainSafe/ChainBridge/shared/ethereum" 11 | "github.com/ChainSafe/chainbridge-utils/msg" 12 | "github.com/ChainSafe/log15" 13 | "github.com/ethereum/go-ethereum/common" 14 | ) 15 | 16 | func AssertGenericResourceAddress(t *testing.T, client *utils.Client, handler common.Address, rId msg.ResourceId, expected common.Address) { 17 | actual, err := utils.GetGenericResourceAddress(client, handler, rId) 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | 22 | if !bytes.Equal(actual.Bytes(), expected.Bytes()) { 23 | t.Fatalf("Generic resoruce mismatch for ID %x. Expected address: %x Got: %x", rId, expected, actual) 24 | } 25 | log15.Info("Asserted generic resource ID", "handler", handler, "rId", rId.Hex(), "contract", actual) 26 | } 27 | -------------------------------------------------------------------------------- /shared/logs.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 ChainSafe Systems 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | package shared 5 | 6 | import ( 7 | log "github.com/ChainSafe/log15" 8 | ) 9 | 10 | func SetLogger(lvl log.Lvl) { 11 | logger := log.Root() 12 | handler := logger.GetHandler() 13 | log.Root().SetHandler(log.LvlFilterHandler(lvl, handler)) 14 | } 15 | -------------------------------------------------------------------------------- /shared/substrate/client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 ChainSafe Systems 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | package utils 5 | 6 | import ( 7 | "fmt" 8 | "math/big" 9 | 10 | "github.com/ChainSafe/chainbridge-utils/msg" 11 | "github.com/ChainSafe/log15" 12 | gsrpc "github.com/centrifuge/go-substrate-rpc-client" 13 | "github.com/centrifuge/go-substrate-rpc-client/signature" 14 | "github.com/centrifuge/go-substrate-rpc-client/types" 15 | ) 16 | 17 | // Client is a container for all the components required to submit extrinsics 18 | // TODO: Perhaps this would benefit an interface so we can interchange Connection and a client like this 19 | type Client struct { 20 | Api *gsrpc.SubstrateAPI 21 | Meta *types.Metadata 22 | Genesis types.Hash 23 | Key *signature.KeyringPair 24 | } 25 | 26 | func CreateClient(key *signature.KeyringPair, endpoint string) (*Client, error) { 27 | c := &Client{Key: key} 28 | api, err := gsrpc.NewSubstrateAPI(endpoint) 29 | if err != nil { 30 | return nil, err 31 | } 32 | c.Api = api 33 | 34 | // Fetch metadata 35 | meta, err := api.RPC.State.GetMetadataLatest() 36 | if err != nil { 37 | return nil, err 38 | } 39 | c.Meta = meta 40 | 41 | // Fetch genesis hash 42 | genesisHash, err := c.Api.RPC.Chain.GetBlockHash(0) 43 | if err != nil { 44 | return nil, err 45 | } 46 | c.Genesis = genesisHash 47 | 48 | return c, nil 49 | } 50 | 51 | // Admin calls 52 | 53 | func (c *Client) SetRelayerThreshold(threshold types.U32) error { 54 | log15.Info("Setting threshold", "threshold", threshold) 55 | return SubmitSudoTx(c, SetThresholdMethod, threshold) 56 | } 57 | 58 | func (c *Client) AddRelayer(relayer types.AccountID) error { 59 | log15.Info("Adding relayer", "accountId", relayer) 60 | return SubmitSudoTx(c, AddRelayerMethod, relayer) 61 | } 62 | 63 | func (c *Client) WhitelistChain(id msg.ChainId) error { 64 | log15.Info("Whitelisting chain", "chainId", id) 65 | return SubmitSudoTx(c, WhitelistChainMethod, types.U8(id)) 66 | } 67 | 68 | func (c *Client) RegisterResource(id msg.ResourceId, method string) error { 69 | log15.Info("Registering resource", "rId", id, "method", []byte(method)) 70 | return SubmitSudoTx(c, SetResourceMethod, types.NewBytes32(id), []byte(method)) 71 | } 72 | 73 | // Standard transfer calls 74 | 75 | func (c *Client) InitiateNativeTransfer(amount types.U128, recipient []byte, destId msg.ChainId) error { 76 | log15.Info("Initiating Substrate native transfer", "amount", amount, "recipient", fmt.Sprintf("%x", recipient), "destId", destId) 77 | return SubmitTx(c, ExampleTransferNativeMethod, amount, recipient, types.U8(destId)) 78 | } 79 | 80 | func (c *Client) InitiateNonFungibleTransfer(tokenId types.U256, recipient []byte, destId msg.ChainId) error { 81 | log15.Info("Initiating Substrate nft transfer", "tokenId", tokenId, "recipient", recipient, "destId", destId) 82 | return SubmitTx(c, ExampleTransferErc721Method, recipient, tokenId, types.U8(destId)) 83 | } 84 | 85 | func (c *Client) InitiateHashTransfer(hash types.Hash, destId msg.ChainId) error { 86 | log15.Info("Initiating hash transfer", "hash", hash.Hex()) 87 | return SubmitTx(c, ExampleTransferHashMethod, hash, types.U8(destId)) 88 | } 89 | 90 | // Call creation methods for batching 91 | 92 | func (c *Client) NewSudoCall(call types.Call) (types.Call, error) { 93 | return types.NewCall(c.Meta, string(SudoMethod), call) 94 | } 95 | 96 | func (c *Client) NewSetRelayerThresholdCall(threshold types.U32) (types.Call, error) { 97 | call, err := types.NewCall(c.Meta, string(SetThresholdMethod), threshold) 98 | if err != nil { 99 | return types.Call{}, err 100 | } 101 | return c.NewSudoCall(call) 102 | } 103 | 104 | func (c *Client) NewAddRelayerCall(relayer types.AccountID) (types.Call, error) { 105 | call, err := types.NewCall(c.Meta, string(AddRelayerMethod), relayer) 106 | if err != nil { 107 | return types.Call{}, err 108 | } 109 | return c.NewSudoCall(call) 110 | } 111 | 112 | func (c *Client) NewWhitelistChainCall(id msg.ChainId) (types.Call, error) { 113 | call, err := types.NewCall(c.Meta, string(WhitelistChainMethod), id) 114 | if err != nil { 115 | return types.Call{}, err 116 | } 117 | return c.NewSudoCall(call) 118 | } 119 | 120 | func (c *Client) NewRegisterResourceCall(id msg.ResourceId, method string) (types.Call, error) { 121 | call, err := types.NewCall(c.Meta, string(SetResourceMethod), id, method) 122 | if err != nil { 123 | return types.Call{}, err 124 | } 125 | return c.NewSudoCall(call) 126 | } 127 | 128 | func (c *Client) NewNativeTransferCall(amount types.U128, recipient []byte, destId msg.ChainId) (types.Call, error) { 129 | return types.NewCall(c.Meta, string(ExampleTransferNativeMethod), amount, recipient, types.U8(destId)) 130 | } 131 | 132 | // Utility methods 133 | 134 | func (c *Client) LatestBlock() (uint64, error) { 135 | head, err := c.Api.RPC.Chain.GetHeaderLatest() 136 | if err != nil { 137 | return 0, err 138 | } 139 | return uint64(head.Number), nil 140 | } 141 | 142 | func (c *Client) MintErc721(tokenId *big.Int, metadata []byte, recipient *signature.KeyringPair) error { 143 | fmt.Printf("Mint info: account %x amount: %x meta: %x\n", recipient.PublicKey, types.NewU256(*tokenId), types.Bytes(metadata)) 144 | return SubmitSudoTx(c, Erc721MintMethod, types.NewAccountID(recipient.PublicKey), types.NewU256(*tokenId), types.Bytes(metadata)) 145 | } 146 | 147 | func (c *Client) OwnerOf(tokenId *big.Int) (types.AccountID, error) { 148 | var owner types.AccountID 149 | tokenIdBz, err := types.EncodeToBytes(types.NewU256(*tokenId)) 150 | if err != nil { 151 | return types.AccountID{}, err 152 | } 153 | 154 | exists, err := QueryStorage(c, "TokenStorage", "TokenOwner", tokenIdBz, nil, &owner) 155 | if err != nil { 156 | return types.AccountID{}, err 157 | } 158 | if !exists { 159 | return types.AccountID{}, fmt.Errorf("token %s doesn't have an owner", tokenId.String()) 160 | } 161 | return owner, nil 162 | } 163 | 164 | func (c *Client) GetDepositNonce(chain msg.ChainId) (uint64, error) { 165 | var count types.U64 166 | chainId, err := types.EncodeToBytes(types.U8(chain)) 167 | if err != nil { 168 | return 0, err 169 | } 170 | exists, err := QueryStorage(c, BridgeStoragePrefix, "ChainNonces", chainId, nil, &count) 171 | if err != nil { 172 | return 0, err 173 | } 174 | if !exists { 175 | return 0, nil 176 | } 177 | return uint64(count), nil 178 | } 179 | -------------------------------------------------------------------------------- /shared/substrate/init.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 ChainSafe Systems 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | package utils 5 | 6 | import ( 7 | "github.com/ChainSafe/chainbridge-utils/msg" 8 | "github.com/centrifuge/go-substrate-rpc-client/types" 9 | ) 10 | 11 | func InitializeChain(client *Client, relayers []types.AccountID, chains []msg.ChainId, resources map[msg.ResourceId]Method, threshold uint32) error { 12 | calls := []types.Call{} 13 | 14 | // Create AddRelayer calls 15 | for _, relayer := range relayers { 16 | call, err := client.NewAddRelayerCall(relayer) 17 | if err != nil { 18 | return err 19 | } 20 | calls = append(calls, call) 21 | } 22 | // Create WhitelistChain calls 23 | for _, chain := range chains { 24 | call, err := client.NewWhitelistChainCall(chain) 25 | if err != nil { 26 | return err 27 | } 28 | calls = append(calls, call) 29 | } 30 | 31 | // Create SetResource calls 32 | for id, method := range resources { 33 | call, err := client.NewRegisterResourceCall(id, string(method)) 34 | if err != nil { 35 | return err 36 | } 37 | calls = append(calls, call) 38 | } 39 | 40 | // Create a SetThreshold call 41 | call, err := client.NewSetRelayerThresholdCall(types.U32(threshold)) 42 | if err != nil { 43 | return err 44 | } 45 | calls = append(calls, call) 46 | 47 | return BatchSubmit(client, calls) 48 | } 49 | -------------------------------------------------------------------------------- /shared/substrate/methods.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 ChainSafe Systems 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | package utils 5 | 6 | // An available method on the substrate chain 7 | type Method string 8 | 9 | var AddRelayerMethod Method = BridgePalletName + ".add_relayer" 10 | var SetResourceMethod Method = BridgePalletName + ".set_resource" 11 | var SetThresholdMethod Method = BridgePalletName + ".set_threshold" 12 | var WhitelistChainMethod Method = BridgePalletName + ".whitelist_chain" 13 | var ExampleTransferNativeMethod Method = "Example.transfer_native" 14 | var ExampleTransferErc721Method Method = "Example.transfer_erc721" 15 | var ExampleTransferHashMethod Method = "Example.transfer_hash" 16 | var ExampleMintErc721Method Method = "Example.mint_erc721" 17 | var ExampleTransferMethod Method = "Example.transfer" 18 | var ExampleRemarkMethod Method = "Example.remark" 19 | var Erc721MintMethod Method = "Erc721.mint" 20 | var SudoMethod Method = "Sudo.sudo" 21 | -------------------------------------------------------------------------------- /shared/substrate/query.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 ChainSafe Systems 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | package utils 5 | 6 | import ( 7 | "fmt" 8 | "math/big" 9 | 10 | "github.com/centrifuge/go-substrate-rpc-client/types" 11 | ) 12 | 13 | func QueryStorage(client *Client, prefix, method string, arg1, arg2 []byte, result interface{}) (bool, error) { 14 | key, err := types.CreateStorageKey(client.Meta, prefix, method, arg1, arg2) 15 | if err != nil { 16 | return false, err 17 | } 18 | return client.Api.RPC.State.GetStorageLatest(key, result) 19 | } 20 | 21 | // TODO: Add to GSRPC 22 | func getConst(meta *types.Metadata, prefix, name string, res interface{}) error { 23 | for _, mod := range meta.AsMetadataV12.Modules { 24 | if string(mod.Name) == prefix { 25 | for _, cons := range mod.Constants { 26 | if string(cons.Name) == name { 27 | return types.DecodeFromBytes(cons.Value, res) 28 | } 29 | } 30 | } 31 | } 32 | return fmt.Errorf("could not find constant %s.%s", prefix, name) 33 | } 34 | 35 | // QueryConst looks up a constant in the metadata 36 | func QueryConst(client *Client, prefix, name string, res interface{}) error { 37 | err := getConst(client.Meta, prefix, name, res) 38 | if err != nil { 39 | return err 40 | } 41 | return nil 42 | } 43 | 44 | // BalanceOf returns the free balance of an account 45 | func BalanceOf(client *Client, publicKey []byte) (*big.Int, error) { 46 | var acct types.AccountInfo 47 | 48 | ok, err := QueryStorage(client, "System", "Account", publicKey, nil, &acct) 49 | if err != nil { 50 | return nil, err 51 | } else if !ok { 52 | return big.NewInt(0), nil 53 | } 54 | return acct.Data.Free.Int, nil 55 | } 56 | 57 | func GetErc721Token(client *Client, id types.U256) (*Erc721Token, error) { 58 | var res Erc721Token 59 | tokenIdBz, err := types.EncodeToBytes(id) 60 | if err != nil { 61 | return nil, err 62 | } 63 | exists, err := QueryStorage(client, "TokenStorage", "Tokens", tokenIdBz, nil, &res) 64 | if err != nil { 65 | return nil, err 66 | } 67 | if !exists { 68 | return nil, fmt.Errorf("token %s does not exist", id.String()) 69 | } 70 | return &res, nil 71 | } 72 | -------------------------------------------------------------------------------- /shared/substrate/submit.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 ChainSafe Systems 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | package utils 5 | 6 | import ( 7 | "fmt" 8 | "math/big" 9 | "sync" 10 | 11 | "github.com/ChainSafe/log15" 12 | "github.com/centrifuge/go-substrate-rpc-client/types" 13 | ) 14 | 15 | func SubmitTx(client *Client, method Method, args ...interface{}) error { 16 | // Create call and extrinsic 17 | call, err := types.NewCall( 18 | client.Meta, 19 | string(method), 20 | args..., 21 | ) 22 | if err != nil { 23 | return err 24 | } 25 | ext := types.NewExtrinsic(call) 26 | 27 | // Get latest runtime version 28 | rv, err := client.Api.RPC.State.GetRuntimeVersionLatest() 29 | if err != nil { 30 | return err 31 | } 32 | 33 | var acct types.AccountInfo 34 | _, err = QueryStorage(client, "System", "Account", client.Key.PublicKey, nil, &acct) 35 | if err != nil { 36 | return err 37 | } 38 | 39 | // Sign the extrinsic 40 | o := types.SignatureOptions{ 41 | BlockHash: client.Genesis, 42 | Era: types.ExtrinsicEra{IsMortalEra: false}, 43 | GenesisHash: client.Genesis, 44 | Nonce: types.NewUCompactFromUInt(uint64(acct.Nonce)), 45 | SpecVersion: rv.SpecVersion, 46 | Tip: types.NewUCompactFromUInt(0), 47 | TransactionVersion: rv.TransactionVersion, 48 | } 49 | err = ext.Sign(*client.Key, o) 50 | if err != nil { 51 | return err 52 | } 53 | 54 | // Submit and watch the extrinsic 55 | sub, err := client.Api.RPC.Author.SubmitAndWatchExtrinsic(ext) 56 | if err != nil { 57 | return err 58 | } 59 | 60 | for { 61 | status := <-sub.Chan() 62 | switch { 63 | case status.IsInBlock: 64 | log15.Info("Extrinsic in block", "block", status.AsInBlock.Hex()) 65 | return nil 66 | case status.IsDropped: 67 | return fmt.Errorf("extrinsic dropped") 68 | case status.IsInvalid: 69 | return fmt.Errorf("extrinsic invalid") 70 | } 71 | } 72 | } 73 | 74 | func SubmitSudoTx(client *Client, method Method, args ...interface{}) error { 75 | call, err := types.NewCall(client.Meta, string(method), args...) 76 | if err != nil { 77 | return err 78 | } 79 | return SubmitTx(client, SudoMethod, call) 80 | } 81 | 82 | // Batch submit take multiple calls and attempts to sumbit all of them, then waits until they complete. 83 | // This should allow for the calls to be processed in a single block (to some limit). 84 | // WARNING: Failed calls are not reported 85 | func BatchSubmit(client *Client, calls []types.Call) error { 86 | // Get latest runtime version 87 | rv, err := client.Api.RPC.State.GetRuntimeVersionLatest() 88 | if err != nil { 89 | return err 90 | } 91 | 92 | var acct types.AccountInfo 93 | _, err = QueryStorage(client, "System", "Account", client.Key.PublicKey, nil, &acct) 94 | if err != nil { 95 | return err 96 | } 97 | 98 | // Sign the extrinsic 99 | o := types.SignatureOptions{ 100 | BlockHash: client.Genesis, 101 | Era: types.ExtrinsicEra{IsMortalEra: false}, 102 | GenesisHash: client.Genesis, 103 | Nonce: types.NewUCompactFromUInt(uint64(acct.Nonce)), 104 | SpecVersion: rv.SpecVersion, 105 | Tip: types.NewUCompactFromUInt(0), 106 | TransactionVersion: rv.TransactionVersion, 107 | } 108 | 109 | wg := &sync.WaitGroup{} 110 | wg.Add(len(calls)) 111 | 112 | for _, call := range calls { 113 | ext := types.NewExtrinsic(call) 114 | 115 | err = ext.Sign(*client.Key, o) 116 | if err != nil { 117 | return err 118 | } 119 | 120 | // Submit and watch the extrinsic 121 | sub, err := client.Api.RPC.Author.SubmitAndWatchExtrinsic(ext) 122 | if err != nil { 123 | return err 124 | } 125 | 126 | go func() { 127 | for { 128 | status := <-sub.Chan() 129 | switch { 130 | case status.IsInBlock: 131 | wg.Done() 132 | return 133 | case status.IsDropped: 134 | wg.Done() 135 | return 136 | case status.IsInvalid: 137 | wg.Done() 138 | return 139 | } 140 | } 141 | }() 142 | 143 | bigNonce := big.Int(o.Nonce) 144 | o.Nonce = types.NewUCompactFromUInt(bigNonce.Uint64() + 1) 145 | } 146 | 147 | wg.Wait() 148 | return nil 149 | } 150 | -------------------------------------------------------------------------------- /shared/substrate/testing/client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 ChainSafe Systems 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | package subtest 5 | 6 | import ( 7 | "bytes" 8 | "math/big" 9 | "testing" 10 | 11 | utils "github.com/ChainSafe/ChainBridge/shared/substrate" 12 | "github.com/ChainSafe/chainbridge-utils/msg" 13 | "github.com/ChainSafe/log15" 14 | "github.com/centrifuge/go-substrate-rpc-client/signature" 15 | "github.com/centrifuge/go-substrate-rpc-client/types" 16 | ) 17 | 18 | func CreateClient(t *testing.T, key *signature.KeyringPair, endpoint string) *utils.Client { 19 | client, err := utils.CreateClient(key, endpoint) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | return client 24 | } 25 | 26 | func AddRelayer(t *testing.T, client *utils.Client, relayer types.AccountID) { 27 | err := client.AddRelayer(relayer) 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | } 32 | 33 | func WhitelistChain(t *testing.T, client *utils.Client, id msg.ChainId) { 34 | err := client.WhitelistChain(id) 35 | if err != nil { 36 | t.Fatal(err) 37 | } 38 | } 39 | 40 | func InitiateNativeTransfer(t *testing.T, client *utils.Client, amount types.U128, recipient []byte, destId msg.ChainId) { 41 | err := client.InitiateNativeTransfer(amount, recipient, destId) 42 | if err != nil { 43 | t.Fatal(err) 44 | } 45 | } 46 | 47 | func InitiateNonFungibleTransfer(t *testing.T, client *utils.Client, tokenId types.U256, recipient []byte, destId msg.ChainId) { 48 | err := client.InitiateNonFungibleTransfer(tokenId, recipient, destId) 49 | if err != nil { 50 | t.Fatal(err) 51 | } 52 | } 53 | 54 | func InitiateHashTransfer(t *testing.T, client *utils.Client, hash types.Hash, destId msg.ChainId) { 55 | err := client.InitiateHashTransfer(hash, destId) 56 | if err != nil { 57 | t.Fatal(err) 58 | } 59 | } 60 | 61 | func RegisterResource(t *testing.T, client *utils.Client, id msg.ResourceId, method string) { 62 | err := client.RegisterResource(id, method) 63 | if err != nil { 64 | t.Fatal(err) 65 | } 66 | } 67 | 68 | func BalanceOf(t *testing.T, client *utils.Client, publicKey []byte) *big.Int { 69 | balance, err := utils.BalanceOf(client, publicKey) 70 | if err != nil { 71 | t.Fatal(err) 72 | } 73 | return balance 74 | } 75 | 76 | func AssertBalanceOf(t *testing.T, client *utils.Client, publicKey []byte, expected *big.Int) { 77 | current, err := utils.BalanceOf(client, publicKey) 78 | if err != nil { 79 | t.Fatal(err) 80 | } 81 | 82 | if expected.Cmp(current) != 0 { 83 | t.Fatalf("Balance does not match expected. Expected: %s Got: %s (change %s) \n", expected.String(), current.String(), big.NewInt(0).Sub(current, expected).String()) 84 | } 85 | } 86 | 87 | func MintErc721(t *testing.T, client *utils.Client, tokenId *big.Int, metadata []byte, recipient *signature.KeyringPair) { 88 | err := client.MintErc721(tokenId, metadata, recipient) 89 | if err != nil { 90 | t.Fatal(err) 91 | } 92 | } 93 | 94 | func AssertOwnerOf(t *testing.T, client *utils.Client, tokenId *big.Int, expected types.AccountID) { 95 | owner, err := client.OwnerOf(tokenId) 96 | if err != nil { 97 | t.Fatal(err) 98 | } 99 | 100 | if !bytes.Equal(owner[:], expected[:]) { 101 | t.Fatalf("Owner does not match for token %s. Got: %x expected: %x", tokenId.String(), owner, expected) 102 | } 103 | log15.Info("Asserted ownership of erc721", "tokenId", tokenId.String(), "owner", owner) 104 | } 105 | 106 | func GetDepositNonce(t *testing.T, client *utils.Client, chain msg.ChainId) uint64 { 107 | count, err := client.GetDepositNonce(chain) 108 | if err != nil { 109 | t.Fatal(err) 110 | } 111 | return count 112 | } 113 | 114 | func NewNativeTransferCall(t *testing.T, client *utils.Client, amount types.U128, recipient []byte, destId msg.ChainId) types.Call { 115 | call, err := client.NewNativeTransferCall(amount, recipient, destId) 116 | if err != nil { 117 | t.Fatal(err) 118 | } 119 | return call 120 | } 121 | -------------------------------------------------------------------------------- /shared/substrate/testing/events.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 ChainSafe Systems 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | package subtest 5 | 6 | import ( 7 | "testing" 8 | "time" 9 | 10 | utils "github.com/ChainSafe/ChainBridge/shared/substrate" 11 | "github.com/ChainSafe/log15" 12 | "github.com/centrifuge/go-substrate-rpc-client/types" 13 | ) 14 | 15 | var TestTimeout = time.Second * 15 16 | 17 | func WaitForRemarkEvent(t *testing.T, client *utils.Client, hash types.Hash) { 18 | key, err := types.CreateStorageKey(client.Meta, "System", "Events", nil, nil) 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | 23 | sub, err := client.Api.RPC.State.SubscribeStorageRaw([]types.StorageKey{key}) 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | 28 | timeout := time.After(TestTimeout) 29 | for { 30 | select { 31 | case <-timeout: 32 | t.Fatalf("Timed out waiting for proposal success/fail event") 33 | case set := <-sub.Chan(): 34 | for _, chng := range set.Changes { 35 | if !types.Eq(chng.StorageKey, key) || !chng.HasStorageData { 36 | // skip, we are only interested in events with content 37 | continue 38 | } 39 | 40 | // Decode the event records 41 | events := utils.Events{} 42 | err = types.EventRecordsRaw(chng.StorageData).DecodeEventRecords(client.Meta, &events) 43 | if err != nil { 44 | t.Fatal(err) 45 | } 46 | 47 | for _, evt := range events.Example_Remark { 48 | if evt.Hash == hash { 49 | log15.Info("Found matching Remark event", "hash", evt.Hash) 50 | return 51 | } else { 52 | log15.Info("Found mismatched Remark event", "hash", evt.Hash) 53 | } 54 | } 55 | } 56 | } 57 | 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /shared/substrate/testing/events_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 ChainSafe Systems 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | package subtest 5 | 6 | import ( 7 | "fmt" 8 | "testing" 9 | 10 | "github.com/ChainSafe/ChainBridge/e2e/substrate" 11 | utils "github.com/ChainSafe/ChainBridge/shared/substrate" 12 | gsrpc "github.com/centrifuge/go-substrate-rpc-client" 13 | "github.com/centrifuge/go-substrate-rpc-client/types" 14 | ) 15 | 16 | func TestChain_Events(t *testing.T) { 17 | targetURL := substrate.TestSubEndpoint // Replace with desired endpoint 18 | api, err := gsrpc.NewSubstrateAPI(targetURL) 19 | if err != nil { 20 | panic(err) 21 | } 22 | 23 | meta, err := api.RPC.State.GetMetadataLatest() 24 | if err != nil { 25 | panic(err) 26 | } 27 | 28 | key, err := types.CreateStorageKey(meta, "System", "Events", nil, nil) 29 | if err != nil { 30 | panic(err) 31 | } 32 | 33 | //fmt.Printf("%x\n", key) 34 | 35 | //latest, err := api.RPC.Chain.GetBlockLatest() 36 | //if err != nil { 37 | // panic(err) 38 | //} 39 | latestNumber := uint32(1) // Set to uint32(latest.Block.Header.Number) 40 | 41 | batchSize := uint32(1) // Set to higher value accordingly, like 1000 42 | current := uint64(0) // Start block 43 | numBatches := latestNumber - uint32(current)/batchSize 44 | 45 | // Not smart enough to adjust batch size to last batch 46 | // Manually trigger the last one with minimum batch size 47 | for i := uint32(0); i < numBatches; i++ { 48 | lower, err := api.RPC.Chain.GetBlockHash(current) 49 | if err != nil { 50 | panic(err) 51 | } 52 | 53 | upperBlock := current + uint64(batchSize) 54 | upper, err := api.RPC.Chain.GetBlockHash(upperBlock) 55 | if err != nil { 56 | panic(err) 57 | } 58 | 59 | raws, err := api.RPC.State.QueryStorage([]types.StorageKey{key}, lower, upper) 60 | if err != nil { 61 | panic(err) 62 | } 63 | 64 | for j := 0; j < len(raws); j++ { 65 | events := utils.Events{} 66 | for k := 0; k < len(raws[j].Changes); k++ { 67 | raw := raws[j].Changes[k] 68 | fmt.Printf("Processing events for block %s with data: %x\n", raws[j].Block.Hex(), raw.StorageData) 69 | err = types.EventRecordsRaw(raw.StorageData).DecodeEventRecords(meta, &events) 70 | if err != nil { 71 | panic(err) 72 | } 73 | } 74 | } 75 | 76 | fmt.Println("Events batch successfully processed: ", i, "until block", upperBlock) 77 | current += uint64(batchSize) 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /shared/substrate/testing/init.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 ChainSafe Systems 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | package subtest 5 | 6 | import ( 7 | "fmt" 8 | "testing" 9 | 10 | utils "github.com/ChainSafe/ChainBridge/shared/substrate" 11 | "github.com/ChainSafe/chainbridge-utils/msg" 12 | "github.com/centrifuge/go-substrate-rpc-client/types" 13 | ) 14 | 15 | // WARNING: THIS METHOD IS UNSAFE AND MAY PANIC 16 | func EnsureInitializedChain(t *testing.T, client *utils.Client, relayers []types.AccountID, chains []msg.ChainId, resources map[msg.ResourceId]utils.Method, threshold uint32) { 17 | var count types.U32 18 | _, err := utils.QueryStorage(client, utils.BridgeStoragePrefix, "RelayerCount", nil, nil, &count) 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | 23 | // Only perform setup if no relayers exist already (ie. state is uninitialized) 24 | if count == 0 { 25 | err = utils.InitializeChain(client, relayers, chains, resources, threshold) 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | 30 | fmt.Println("======================== Substrate Initialized ========================") 31 | fmt.Printf("Relayers: %x\n", relayers) 32 | fmt.Printf("Whitelisted Chain IDs: %x\n", chains) 33 | fmt.Printf("Relayer Threshold: %x\n", threshold) 34 | for id, method := range resources { 35 | fmt.Printf("Resource - Id: %x, %s\n", id, method) 36 | } 37 | fmt.Println("========================================================================") 38 | 39 | } else { 40 | fmt.Println("=========================================================================") 41 | fmt.Println("! WARNING: Running tests against an initialized chain, results may vary !") 42 | fmt.Println("=========================================================================") 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /shared/substrate/testing/query.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 ChainSafe Systems 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | package subtest 5 | 6 | import ( 7 | "bytes" 8 | "math/big" 9 | "testing" 10 | 11 | utils "github.com/ChainSafe/ChainBridge/shared/substrate" 12 | "github.com/ChainSafe/log15" 13 | "github.com/centrifuge/go-substrate-rpc-client/types" 14 | ) 15 | 16 | func QueryStorage(t *testing.T, client *utils.Client, prefix, method string, arg1, arg2 []byte, result interface{}) bool { 17 | exists, err := utils.QueryStorage(client, prefix, method, arg1, arg2, result) 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | return exists 22 | } 23 | 24 | func QueryConst(t *testing.T, client *utils.Client, prefix, name string, res interface{}) { 25 | err := utils.QueryConst(client, prefix, name, res) 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | } 30 | 31 | func AssertErc721Metadata(t *testing.T, client *utils.Client, id *big.Int, expected types.Bytes) { 32 | token, err := utils.GetErc721Token(client, types.NewU256(*id)) 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | if !bytes.Equal(token.Metadata, expected) { 37 | t.Fatalf("Erc721 metadata mismatch for token %s. Got: %x Expected: %x", id.String(), token.Metadata, expected) 38 | } 39 | log15.Info("Asserted erc721 metadata", "tokenId", id, "metadata", token.Metadata) 40 | } 41 | 42 | func AssertErc721NonExistence(t *testing.T, client *utils.Client, id *big.Int) { 43 | var res types.Bytes // No expecting to decode is 44 | exists := QueryStorage(t, client, "TokenStorage", "Tokens", id.Bytes(), nil, &res) 45 | if exists { 46 | t.Fatalf("erc721 token %s exists but should have been burned", id) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /shared/substrate/types.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 ChainSafe Systems 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | package utils 5 | 6 | import ( 7 | "github.com/centrifuge/go-substrate-rpc-client/types" 8 | ) 9 | 10 | const BridgePalletName = "ChainBridge" 11 | const BridgeStoragePrefix = "ChainBridge" 12 | 13 | type Erc721Token struct { 14 | Id types.U256 15 | Metadata types.Bytes 16 | } 17 | 18 | type RegistryId types.H160 19 | type TokenId types.U256 20 | 21 | type AssetId struct { 22 | RegistryId RegistryId 23 | TokenId TokenId 24 | } 25 | --------------------------------------------------------------------------------