├── .eslintignore
├── .eslintrc
├── .gitattributes
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
└── workflows
│ └── test.yml
├── .gitignore
├── .gitlab-ci.yml
├── .prettierignore
├── .prettierrc
├── .solhint.json
├── .vscode
└── settings.json
├── LICENSE
├── README.md
├── contracts
├── IRelay.sol
├── Parser.sol
├── ParserDelegate.sol
├── Relay.sol
├── Script.sol
├── ScriptDelegate.sol
└── TestRelay.sol
├── hardhat.config.ts
├── index.ts
├── package.json
├── scripts
├── builder.ts
├── contracts.ts
├── deploy.ts
├── fetch.sh
├── ganache.ts
├── latest.sh
├── latest.ts
├── metrics.ts
├── parser_test.js
├── proof.sh
├── regtest.sh
├── setup.sh
└── testdata.py
├── test
├── build.test.ts
├── constants.ts
├── fork.test.ts
├── gas.test.ts
├── parser.test.ts
├── proof.test.ts
├── relay.test.ts
├── scripts.test.ts
└── target.test.ts
└── tsconfig.json
/.eslintignore:
--------------------------------------------------------------------------------
1 | /typechain/**
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "node": true,
4 | "commonjs": true,
5 | "es6": true,
6 | "mocha": true
7 | },
8 | "root": true,
9 | "parser": "@typescript-eslint/parser",
10 | "plugins": ["@typescript-eslint", "prettier"],
11 | "extends": [
12 | "eslint:recommended",
13 | "plugin:@typescript-eslint/recommended",
14 | "prettier"
15 | ],
16 | "rules": {
17 | "no-console": 1,
18 | "prettier/prettier": 2
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.sol linguist-language=Solidity
--------------------------------------------------------------------------------
/.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 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Software (please complete the following information/versions of your software):**
27 | - OS:
28 | - Node:
29 | - Yarn:
30 | - solc:
31 | - buidler:
32 |
33 | **Additional context**
34 | Add any other context about the problem here.
35 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: test
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v2
14 | - name: setup node
15 | uses: actions/setup-node@v1
16 | with:
17 | node-version: '14.x'
18 | - run: yarn install
19 | - run: yarn build
20 | - run: yarn lint
21 | - run: yarn test
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ### Solidity ###
2 | # Logs
3 | logs
4 | *.log
5 | npm-debug.log*
6 | yarn-debug.log*
7 | yarn-error.log*
8 | lerna-debug.log*
9 |
10 | # Diagnostic reports (https://nodejs.org/api/report.html)
11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
12 |
13 | # Runtime data
14 | pids
15 | *.pid
16 | *.seed
17 | *.pid.lock
18 |
19 | # Directory for instrumented libs generated by jscoverage/JSCover
20 | lib-cov
21 |
22 | # Coverage directory used by tools like istanbul
23 | coverage
24 | *.lcov
25 |
26 | # nyc test coverage
27 | .nyc_output
28 |
29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
30 | .grunt
31 |
32 | # Bower dependency directory (https://bower.io/)
33 | bower_components
34 |
35 | # node-waf configuration
36 | .lock-wscript
37 |
38 | # Compiled binary addons (https://nodejs.org/api/addons.html)
39 | build/Release
40 |
41 | # Dependency directories
42 | node_modules/
43 | jspm_packages/
44 |
45 | # TypeScript v1 declaration files
46 | typings/
47 |
48 | # TypeScript cache
49 | *.tsbuildinfo
50 |
51 | # Optional npm cache directory
52 | .npm
53 |
54 | # Optional eslint cache
55 | .eslintcache
56 |
57 | # Optional REPL history
58 | .node_repl_history
59 |
60 | # Output of 'npm pack'
61 | *.tgz
62 |
63 | # Yarn Integrity file
64 | .yarn-integrity
65 |
66 | # dotenv environment variables file
67 | .env
68 | .env.test
69 |
70 | # parcel-bundler cache (https://parceljs.org/)
71 | .cache
72 |
73 | # next.js build output
74 | .next
75 |
76 | # nuxt.js build output
77 | .nuxt
78 |
79 | # rollup.js default build output
80 | dist/
81 |
82 | # Uncomment the public line if your project uses Gatsby
83 | # https://nextjs.org/blog/next-9-1#public-directory-support
84 | # https://create-react-app.dev/docs/using-the-public-folder/#docsNav
85 | # public
86 |
87 | # Storybook build outputs
88 | .out
89 | .storybook-out
90 |
91 | # vuepress build output
92 | .vuepress/dist
93 |
94 | # Serverless directories
95 | .serverless/
96 |
97 | # FuseBox cache
98 | .fusebox/
99 |
100 | # DynamoDB Local files
101 | .dynamodb/
102 |
103 | # Temporary folders
104 | tmp/
105 | temp/
106 |
107 | ### SolidityTruffle ###
108 | # depedencies
109 | node_modules
110 |
111 | # testing
112 |
113 | # production
114 | build
115 | build_webpack
116 |
117 | # misc
118 | .DS_Store
119 | npm-debug.log
120 | .truffle-solidity-loader
121 | .vagrant/**
122 | blockchain/geth/**
123 | blockchain/keystore/**
124 | blockchain/history
125 |
126 | # truffle
127 | .tern-port
128 | yarn.lock
129 | package-lock.json
130 |
131 | # buidler
132 | artifacts
133 | cache
134 | typechain
--------------------------------------------------------------------------------
/.gitlab-ci.yml:
--------------------------------------------------------------------------------
1 | stages:
2 | - test
3 |
4 | contracts:
5 | stage: test
6 | image: node:13
7 | script:
8 | - yarn install
9 | - yarn build
10 | - yarn lint
11 | - yarn test
12 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | /typechain/**
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": true,
3 | "trailingComma": "none",
4 | "singleQuote": true,
5 | "printWidth": 80
6 | }
7 |
--------------------------------------------------------------------------------
/.solhint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["solhint:recommended"],
3 | "rules": {
4 | "prettier/prettier": "error",
5 | "avoid-throw": "off",
6 | "avoid-suicide": "error",
7 | "avoid-sha3": "warn",
8 | "quotes": ["error", "single"],
9 | "compiler-version": ["error", "^0.6.0"]
10 | },
11 | "plugins": ["prettier"]
12 | }
13 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "[typescript]": {
3 | "editor.defaultFormatter": "esbenp.prettier-vscode",
4 | "editor.formatOnSave": true
5 | },
6 | "[solidity]": {
7 | "editor.defaultFormatter": "JuanBlanco.solidity",
8 | "editor.formatOnSave": true
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Interlay
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # BTC-Relay
2 |
3 | ## Notice
4 |
5 | This repository is for educational purposes only. The BTC Relay is not audited and contains critical bugs. Don't use this in production.
6 |
7 | ## Relevant Repositories
8 |
9 | Our libs:
10 |
11 | * https://github.com/interlay/compressed-inclusion-proofs
12 | * https://github.com/crossclaim/btcrelay-sol
13 |
14 | External libs:
15 |
16 | * Summa Bitcoin SPV library: https://github.com/summa-tx/bitcoin-spv/tree/master/solidity
17 | * Summa Bitcoin Relay: https://github.com/summa-tx/relays/tree/master/solidity
18 | * Original [Deprecated] BTC-Relay: https://github.com/ethereum/btcrelay/tree/develop/fetchd
19 |
20 | ## Background
21 |
22 | ### Chain Relays
23 | Chain relays are on-chain programs or smart contracts deployed on a blockchain A capable of reading and verifying the state of another blockchain B.
24 | The underlying technical design and functionality is comparable to that of SPV-Clients. That is, a chain relay stores and maintains block headers of chain B on chain A and allows to verify transaction inclusion proofs. Summarizing, the two main functionalities a chain relay must/should provide are: consensus verification and transaction inclusion verification.
25 |
26 | Read more about chain relays in the XCLAIM paper (Section V.B descibes the basic concept of chain relays, while Appendix B provides a formal model of the required functionality for PoW chain relays.).
27 |
28 | ### Architecture
29 | This project is an implementation of a chain relay for Bitcoin on Ethereum. The first implementation of a BTC relay was implemented in Serpent and can be found here.
30 | However, as Serpent is outdated (last commit: December 2017), this project aims to implement an updated version in Solidity.
31 |
32 | ## Installation
33 |
34 | Install dependencies:
35 |
36 | ```bash
37 | yarn install
38 | ```
39 |
40 | Build the contracts and interfaces:
41 |
42 | ```bash
43 | yarn build
44 | ```
45 |
46 | ## Testing
47 |
48 | Run the tests:
49 |
50 | ```bash
51 | yarn test
52 | ```
53 |
54 | Run with [eth-gas-reporter](https://github.com/cgewecke/eth-gas-reporter):
55 |
56 | ```bash
57 | export COINMARKETCAP_API_KEY=*****
58 | npx buidler node
59 | yarn test --network localhost
60 | ```
61 |
62 | ## Gas Costs
63 |
64 | ```bash
65 | npx buidler run scripts/metrics.ts
66 | ```
67 |
68 | | Function | Gas | Description |
69 | |--------------------------|---------|--------------|
70 | | `constructor` | 1796743 | Genesis |
71 | | `submitBlockHeader` | 105299 | 1st Header |
72 | | `submitBlockHeader` | 105311 | 2nd Header |
73 | | `submitBlockHeader` | 105287 | 3rd Header |
74 | | `submitBlockHeader` | 105275 | 4th Header |
75 | | `submitBlockHeader` | 105299 | 5th Header |
76 | | `submitBlockHeader` | 105263 | 6th Header |
77 | | `submitBlockHeaderBatch` | 464777 | Combined |
78 | | `verifyTx` | 62884 | Inclusion |
79 |
80 | ### Summa Relay
81 |
82 | [Summa](https://github.com/summa-tx/relays) have also developed a Bitcoin relay in Solidity.
83 | There are a number of differences between the two approaches however. As summarized in the table
84 | below, their block submission is significantly cheaper compared to ours. This is primarily due to
85 | their more restrictive use of storage and separation of functionality - block submission, difficulty adjustment
86 | and fork selection are all separate calls. However, checking transaction inclusion is slightly more involved
87 | as the implementation needs to recurse backwards through all ancestors.
88 |
89 | | Interlay | Summa | Purpose | Description |
90 | |----------|---------|-----------|------------------------------|
91 | | 616782 | 403903 | Submit | 8 Block Headers |
92 | | 2397012 | 1520844 | Submit | 32 Block Headers |
93 | | 30462 | 32731 | Inclusion | Coinbase - Tx Depth 1 |
94 | | 67240 | 69510 | Inclusion | Heavy (230 Txs) - Tx Depth 1 |
95 | | 67326 | 79540 | Inclusion | Tx Depth 6 |
96 | | 67326 | 102364 | Inclusion | Tx Depth 32 |
97 |
98 | There are two primary motivations for our higher cost in block submission:
99 |
100 | 1. The relay should be self-healing, requiring minimal user intervention.
101 | 2. Constant time lookup - given a height we should be able to instantly verify inclusion.
102 |
103 | ## Deployments
104 |
105 | ```bash
106 | yarn deploy
107 | ```
108 |
109 | ### Ropsten
110 |
111 | + [0x43E5180B3F3C29F0C8BE0B100f48893993ce3600](https://ropsten.etherscan.io/address/0x43E5180B3F3C29F0C8BE0B100f48893993ce3600)
112 |
--------------------------------------------------------------------------------
/contracts/IRelay.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | pragma solidity ^0.6.0;
4 |
5 | interface IRelay {
6 | /**
7 | * @param digest block header hash of block header submitted for storage
8 | * @param height height of the stored block
9 | */
10 | event StoreHeader(bytes32 indexed digest, uint32 indexed height);
11 |
12 | /**
13 | * @param from previous best block hash
14 | * @param to new best block hash
15 | * @param id identifier of the fork triggering the reorg
16 | */
17 | event ChainReorg(
18 | bytes32 indexed from,
19 | bytes32 indexed to,
20 | uint256 indexed id
21 | );
22 |
23 | /**
24 | * @notice Parses, validates and stores a block header
25 | * @param header Raw block header bytes (80 bytes)
26 | */
27 | function submitBlockHeader(bytes calldata header) external;
28 |
29 | /**
30 | * @notice Parses, validates and stores a batch of headers
31 | * @param headers Raw block headers (80* bytes)
32 | */
33 | function submitBlockHeaderBatch(bytes calldata headers) external;
34 |
35 | /**
36 | * @notice Gets the height of an included block
37 | * @param digest Hash of the referenced block
38 | * @return Height of the stored block, reverts if not found
39 | */
40 | function getBlockHeight(bytes32 digest) external view returns (uint32);
41 |
42 | /**
43 | * @notice Gets the hash of an included block
44 | * @param height Height of the referenced block
45 | * @return Hash of the stored block, reverts if not found
46 | */
47 | function getBlockHash(uint32 height) external view returns (bytes32);
48 |
49 | /**
50 | * @notice Gets the hash and height for the best tip
51 | * @return digest Hash of stored block
52 | * @return height Height of stored block
53 | */
54 | function getBestBlock()
55 | external
56 | view
57 | returns (bytes32 digest, uint32 height);
58 |
59 | /**
60 | * @notice Verifies that a transaction is included in a block
61 | * @param height Height of block that included transaction
62 | * @param index Index of transaction in the block's tx merkle tree
63 | * @param txid Transaction identifier (little endian)
64 | * @param header Raw block header (80 bytes)
65 | * @param proof Merkle proof
66 | * @param confirmations Required confirmations (insecure)
67 | * @param insecure Check custom inclusion confirmations
68 | * @return True if txid is included, false otherwise
69 | */
70 | function verifyTx(
71 | uint32 height,
72 | uint256 index,
73 | bytes32 txid,
74 | bytes calldata header,
75 | bytes calldata proof,
76 | uint256 confirmations,
77 | bool insecure
78 | ) external view returns (bool);
79 | }
80 |
--------------------------------------------------------------------------------
/contracts/Parser.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | pragma solidity ^0.6.0;
4 |
5 | import {SafeMath} from '@openzeppelin/contracts/math/SafeMath.sol';
6 | import {BytesLib} from '@interlay/bitcoin-spv-sol/contracts/BytesLib.sol';
7 | import {BTCUtils} from '@interlay/bitcoin-spv-sol/contracts/BTCUtils.sol';
8 |
9 | library Parser {
10 | using SafeMath for uint256;
11 | using BytesLib for bytes;
12 | using BTCUtils for bytes;
13 |
14 | // EXCEPTION MESSAGES
15 | string internal constant ERR_INVALID_OUTPUT = 'Invalid output';
16 |
17 | /**
18 | * @notice Extracts number of inputs and ending index
19 | * @param rawTx Raw transaction
20 | * @return Number of inputs
21 | * @return Scanner end position
22 | */
23 | function extractInputLength(bytes memory rawTx)
24 | internal
25 | pure
26 | returns (uint256, uint256)
27 | {
28 | uint256 length = rawTx.length;
29 |
30 | // skip version
31 | uint256 pos = 4;
32 |
33 | bytes memory segwit = rawTx.slice(pos, 2);
34 | if (segwit[0] == 0x00 && segwit[1] == 0x01) {
35 | pos = pos + 2;
36 | }
37 |
38 | uint256 varIntLen = rawTx
39 | .slice(pos, length - pos)
40 | .determineVarIntDataLength();
41 | if (varIntLen == 0) {
42 | varIntLen = 1;
43 | }
44 |
45 | uint256 numInputs = rawTx.slice(pos, varIntLen).bytesToUint();
46 | pos = pos + varIntLen;
47 |
48 | for (uint256 i = 0; i < numInputs; i++) {
49 | pos = pos + 32;
50 | pos = pos + 4;
51 | // read varInt for script sig
52 | uint256 scriptSigvarIntLen = rawTx
53 | .slice(pos, length - pos)
54 | .determineVarIntDataLength();
55 | if (scriptSigvarIntLen == 0) {
56 | scriptSigvarIntLen = 1;
57 | }
58 | uint256 scriptSigLen = rawTx
59 | .slice(pos, scriptSigvarIntLen)
60 | .bytesToUint();
61 | pos = pos + scriptSigvarIntLen;
62 | // get script sig
63 | pos = pos + scriptSigLen;
64 | // get sequence 4 bytes
65 | pos = pos + 4;
66 | // new pos is now start of next index
67 | }
68 |
69 | return (numInputs, pos);
70 | }
71 |
72 | /**
73 | * @notice Extracts number of outputs and ending index
74 | * @param rawTx Raw transaction
75 | * @return Number of outputs
76 | * @return Scanner end position
77 | */
78 | function extractOutputLength(bytes memory rawTx)
79 | internal
80 | pure
81 | returns (uint256, uint256)
82 | {
83 | uint256 length = rawTx.length;
84 | uint256 pos = 0;
85 |
86 | uint256 varIntLen = rawTx
87 | .slice(pos, length - pos)
88 | .determineVarIntDataLength();
89 | if (varIntLen == 0) {
90 | varIntLen = 1;
91 | }
92 |
93 | uint256 numOutputs = rawTx.slice(pos, varIntLen).bytesToUint();
94 | pos = pos + varIntLen;
95 |
96 | for (uint256 i = 0; i < numOutputs; i++) {
97 | pos = pos + 8;
98 | uint256 pkScriptVarIntLen = rawTx
99 | .slice(pos, length - pos)
100 | .determineVarIntDataLength();
101 | if (pkScriptVarIntLen == 0) {
102 | pkScriptVarIntLen = 1;
103 | }
104 | uint256 pkScriptLen = rawTx
105 | .slice(pos, pkScriptVarIntLen)
106 | .bytesToUint();
107 | pos = pos + pkScriptVarIntLen;
108 | pos = pos + pkScriptLen;
109 | }
110 |
111 | return (numOutputs, pos);
112 | }
113 |
114 | /**
115 | * @notice Extracts output from transaction outputs
116 | * @param outputs Raw transaction outputs
117 | * @param index Index of output
118 | * @return Output bytes
119 | */
120 | function extractOutputAtIndex(bytes memory outputs, uint256 index)
121 | internal
122 | pure
123 | returns (bytes memory)
124 | {
125 | uint256 length = outputs.length;
126 | uint256 pos = 0;
127 |
128 | uint256 varIntLen = outputs
129 | .slice(pos, length - pos)
130 | .determineVarIntDataLength();
131 | if (varIntLen == 0) {
132 | varIntLen = 1;
133 | }
134 |
135 | uint256 numOutputs = outputs.slice(pos, varIntLen).bytesToUint();
136 | require(numOutputs >= index, ERR_INVALID_OUTPUT);
137 | pos = pos + varIntLen;
138 |
139 | uint256 start = pos;
140 | for (uint256 i = 0; i < numOutputs; i++) {
141 | pos = pos + 8;
142 | uint256 pkScriptVarIntLen = outputs
143 | .slice(pos, length - pos)
144 | .determineVarIntDataLength();
145 | if (pkScriptVarIntLen == 0) {
146 | pkScriptVarIntLen = 1;
147 | }
148 | uint256 pkScriptLen = outputs
149 | .slice(pos, pkScriptVarIntLen)
150 | .bytesToUint();
151 | pos = pos + pkScriptVarIntLen;
152 | pos = pos + pkScriptLen;
153 | if (i == index) {
154 | return outputs.slice(start, pos.sub(start));
155 | }
156 | start = pos;
157 | }
158 |
159 | return '';
160 | }
161 |
162 | /**
163 | * @notice Extracts the amount from a tx output
164 | * @param out Raw transaction output
165 | * @return Value
166 | */
167 | function extractOutputValue(bytes memory out)
168 | internal
169 | pure
170 | returns (uint64)
171 | {
172 | return out.extractValue();
173 | }
174 |
175 | /**
176 | * @notice Extracts the script from a tx output
177 | * @param out Raw transaction output
178 | * @return Script bytes
179 | */
180 | function extractOutputScript(bytes memory out)
181 | internal
182 | pure
183 | returns (bytes memory)
184 | {
185 | uint256 length = out.length;
186 |
187 | // skip value
188 | uint256 pos = 8;
189 | uint256 pkScriptVarIntLen = out
190 | .slice(pos, length - pos)
191 | .determineVarIntDataLength();
192 | if (pkScriptVarIntLen == 0) {
193 | pkScriptVarIntLen = 1;
194 | }
195 |
196 | uint256 pkScriptLen = out.slice(pos, pkScriptVarIntLen).bytesToUint();
197 | pos = pos + pkScriptVarIntLen;
198 | return out.slice(pos, pkScriptLen);
199 | }
200 | }
201 |
--------------------------------------------------------------------------------
/contracts/ParserDelegate.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | pragma solidity ^0.6.0;
4 |
5 | import {BytesLib} from '@interlay/bitcoin-spv-sol/contracts/BytesLib.sol';
6 |
7 | import {Parser} from './Parser.sol';
8 |
9 | contract ParserDelegate {
10 | using BytesLib for bytes;
11 | using Parser for bytes;
12 |
13 | function extractInputLength(bytes memory rawTx)
14 | public
15 | pure
16 | returns (uint256 numInputs, uint256 lenInputs)
17 | {
18 | return rawTx.extractInputLength();
19 | }
20 |
21 | function extractOutputLength(bytes memory rawTx)
22 | public
23 | pure
24 | returns (uint256 numOutputs, uint256 lenOutputs)
25 | {
26 | return rawTx.extractOutputLength();
27 | }
28 |
29 | function extractNumOutputs(bytes memory rawTx)
30 | public
31 | pure
32 | returns (uint256)
33 | {
34 | (, uint256 lenInputs) = rawTx.extractInputLength();
35 | bytes memory outputs = rawTx.slice(lenInputs, rawTx.length - lenInputs);
36 | (uint256 numOutputs, ) = outputs.extractOutputLength();
37 | return numOutputs;
38 | }
39 |
40 | function extractOutputAtIndex(bytes memory rawTx, uint256 index)
41 | public
42 | pure
43 | returns (bytes memory)
44 | {
45 | (, uint256 lenInputs) = rawTx.extractInputLength();
46 | return
47 | rawTx
48 | .slice(lenInputs, rawTx.length - lenInputs)
49 | .extractOutputAtIndex(index);
50 | }
51 |
52 | function extractOutputValueAtIndex(bytes memory rawTx, uint256 index)
53 | public
54 | pure
55 | returns (uint256)
56 | {
57 | bytes memory output = extractOutputAtIndex(rawTx, index);
58 | return output.extractOutputValue();
59 | }
60 |
61 | function extractOutputScriptAtIndex(bytes memory rawTx, uint256 index)
62 | public
63 | pure
64 | returns (bytes memory)
65 | {
66 | bytes memory output = extractOutputAtIndex(rawTx, index);
67 | return output.extractOutputScript();
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/contracts/Relay.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | pragma solidity ^0.6.0;
4 |
5 | import {SafeMath} from '@openzeppelin/contracts/math/SafeMath.sol';
6 | import {BytesLib} from '@interlay/bitcoin-spv-sol/contracts/BytesLib.sol';
7 | import {BTCUtils} from '@interlay/bitcoin-spv-sol/contracts/BTCUtils.sol';
8 | import {ValidateSPV} from '@interlay/bitcoin-spv-sol/contracts/ValidateSPV.sol';
9 | import {IRelay} from './IRelay.sol';
10 |
11 | /// @title BTC Relay
12 | contract Relay is IRelay {
13 | using SafeMath for uint256;
14 | using BytesLib for bytes;
15 | using BTCUtils for bytes;
16 |
17 | struct Header {
18 | // height of this block header (cannot be zero)
19 | uint32 height;
20 | // identifier of chain fork
21 | uint64 chainId;
22 | }
23 |
24 | // mapping of block hashes to block headers (ALL ever submitted, i.e., incl. forks)
25 | mapping(bytes32 => Header) public _headers;
26 |
27 | // main chain mapping for constant time inclusion check
28 | mapping(uint32 => bytes32) public _chain;
29 |
30 | struct Fork {
31 | uint32 height; // best height of fork
32 | bytes32 ancestor; // branched from this
33 | bytes32[] descendants; // references to submitted block headers
34 | }
35 |
36 | // mapping of ids to forks
37 | mapping(uint256 => Fork) public _forks;
38 |
39 | // incrementing counter to track forks
40 | // OPTIMIZATION: default to zero value
41 | uint256 private _chainCounter;
42 |
43 | // target of the difficulty period
44 | uint256 public _epochStartTarget;
45 | uint256 public _epochEndTarget;
46 |
47 | uint64 public _epochStartTime;
48 | uint64 public _epochEndTime;
49 |
50 | // block with the most accumulated work, i.e., blockchain tip
51 | uint32 internal _bestHeight;
52 | bytes32 internal _bestBlock;
53 |
54 | // CONSTANTS
55 | uint256 public constant DIFFICULTY_ADJUSTMENT_INTERVAL = 2016;
56 | uint256 public constant MAIN_CHAIN_ID = 0;
57 | uint256 public constant CONFIRMATIONS = 6;
58 |
59 | // EXCEPTION MESSAGES
60 | // OPTIMIZATION: limit string length to 32 bytes
61 | string
62 | internal constant ERR_INVALID_HEADER_SIZE = 'Invalid block header size';
63 | string
64 | internal constant ERR_INVALID_GENESIS_HEIGHT = 'Invalid genesis height';
65 | string
66 | internal constant ERR_INVALID_HEADER_BATCH = 'Invalid block header batch';
67 | string internal constant ERR_DUPLICATE_BLOCK = 'Block already stored';
68 | string
69 | internal constant ERR_PREVIOUS_BLOCK = 'Previous block hash not found';
70 | string internal constant ERR_LOW_DIFFICULTY = 'Insufficient difficulty';
71 | string
72 | internal constant ERR_DIFF_TARGET_HEADER = 'Incorrect difficulty target';
73 | string internal constant ERR_DIFF_PERIOD = 'Invalid difficulty period';
74 | string internal constant ERR_NOT_EXTENSION = 'Not extension of chain';
75 | string internal constant ERR_BLOCK_NOT_FOUND = 'Block not found';
76 | string internal constant ERR_CONFIRMS = 'Insufficient confirmations';
77 | string internal constant ERR_VERIFY_TX = 'Incorrect merkle proof';
78 | string internal constant ERR_INVALID_TXID = 'Invalid tx identifier';
79 |
80 | /**
81 | * @notice Initializes the relay with the provided block.
82 | * @param header Genesis block header
83 | * @param height Genesis block height
84 | */
85 | constructor(bytes memory header, uint32 height) public {
86 | require(header.length == 80, ERR_INVALID_HEADER_SIZE);
87 | require(height > 0, ERR_INVALID_GENESIS_HEIGHT);
88 | bytes32 digest = header.hash256();
89 | uint256 target = header.extractTarget();
90 | uint64 timestamp = header.extractTimestamp();
91 | uint256 chainId = MAIN_CHAIN_ID;
92 |
93 | _bestBlock = digest;
94 | _bestHeight = height;
95 |
96 | _forks[chainId].height = height;
97 | _chain[height] = digest;
98 |
99 | _epochStartTarget = target;
100 | _epochStartTime = timestamp;
101 | _epochEndTarget = target;
102 | _epochEndTime = timestamp;
103 |
104 | _storeBlockHeader(digest, height, chainId);
105 | }
106 |
107 | /**
108 | * @dev Core logic for block header validation
109 | */
110 | function _submitBlockHeader(bytes memory header) internal virtual {
111 | require(header.length == 80, ERR_INVALID_HEADER_SIZE);
112 |
113 | // Fail if block already exists
114 | bytes32 hashCurrBlock = header.hash256();
115 | require(_headers[hashCurrBlock].height == 0, ERR_DUPLICATE_BLOCK);
116 |
117 | // Fail if previous block hash not in current state of main chain
118 | bytes32 hashPrevBlock = header.extractPrevBlockLE().toBytes32();
119 | require(_headers[hashPrevBlock].height > 0, ERR_PREVIOUS_BLOCK);
120 |
121 | uint256 target = header.extractTarget();
122 |
123 | // Check the PoW solution matches the target specified in the block header
124 | require(
125 | abi.encodePacked(hashCurrBlock).reverseEndianness().bytesToUint() <=
126 | target,
127 | ERR_LOW_DIFFICULTY
128 | );
129 |
130 | uint32 height = 1 + _headers[hashPrevBlock].height;
131 |
132 | // Check the specified difficulty target is correct
133 | if (_isPeriodStart(height)) {
134 | require(
135 | isCorrectDifficultyTarget(
136 | _epochStartTarget,
137 | _epochStartTime,
138 | _epochEndTarget,
139 | _epochEndTime,
140 | target
141 | ),
142 | ERR_DIFF_TARGET_HEADER
143 | );
144 |
145 | _epochStartTarget = target;
146 | _epochStartTime = header.extractTimestamp();
147 |
148 | delete _epochEndTarget;
149 | delete _epochEndTime;
150 | } else if (_isPeriodEnd(height)) {
151 | // only update if end to save gas
152 | _epochEndTarget = target;
153 | _epochEndTime = header.extractTimestamp();
154 | }
155 |
156 | uint256 chainId = _headers[hashPrevBlock].chainId;
157 | bool isNewFork = _forks[chainId].height !=
158 | _headers[hashPrevBlock].height;
159 |
160 | if (isNewFork) {
161 | chainId = _incrementChainCounter();
162 | _initializeFork(hashCurrBlock, hashPrevBlock, chainId, height);
163 |
164 | _storeBlockHeader(hashCurrBlock, height, chainId);
165 | } else {
166 | _storeBlockHeader(hashCurrBlock, height, chainId);
167 |
168 | if (chainId == MAIN_CHAIN_ID) {
169 | _bestBlock = hashCurrBlock;
170 | _bestHeight = height;
171 |
172 | // extend height of main chain
173 | _forks[chainId].height = height;
174 | _chain[height] = hashCurrBlock;
175 | } else if (height >= _bestHeight + CONFIRMATIONS) {
176 | // with sufficient confirmations, reorg
177 | _reorgChain(chainId, height, hashCurrBlock);
178 | } else {
179 | // extend fork
180 | _forks[chainId].height = height;
181 | _forks[chainId].descendants.push(hashCurrBlock);
182 | }
183 | }
184 | }
185 |
186 | /**
187 | * @dev See {IRelay-submitBlockHeader}.
188 | */
189 | function submitBlockHeader(bytes calldata header) external override {
190 | _submitBlockHeader(header);
191 | }
192 |
193 | /**
194 | * @dev See {IRelay-submitBlockHeaderBatch}.
195 | */
196 | function submitBlockHeaderBatch(bytes calldata headers) external override {
197 | require(headers.length % 80 == 0, ERR_INVALID_HEADER_BATCH);
198 |
199 | for (uint256 i = 0; i < headers.length / 80; i = i.add(1)) {
200 | bytes memory header = headers.slice(i.mul(80), 80);
201 | _submitBlockHeader(header);
202 | }
203 | }
204 |
205 | function _storeBlockHeader(
206 | bytes32 digest,
207 | uint32 height,
208 | uint256 chainId
209 | ) internal {
210 | _chain[height] = digest;
211 | _headers[digest].height = height;
212 | _headers[digest].chainId = uint64(chainId);
213 | emit StoreHeader(digest, height);
214 | }
215 |
216 | function _incrementChainCounter() internal returns (uint256) {
217 | _chainCounter = _chainCounter.add(1);
218 | return _chainCounter;
219 | }
220 |
221 | function _initializeFork(
222 | bytes32 hashCurrBlock,
223 | bytes32 hashPrevBlock,
224 | uint256 chainId,
225 | uint32 height
226 | ) internal {
227 | bytes32[] memory descendants = new bytes32[](1);
228 | descendants[0] = hashCurrBlock;
229 |
230 | _forks[chainId].height = height;
231 | _forks[chainId].ancestor = hashPrevBlock;
232 | _forks[chainId].descendants = descendants;
233 | }
234 |
235 | function _reorgChain(
236 | uint256 chainId,
237 | uint32 height,
238 | bytes32 hashCurrBlock
239 | ) internal {
240 | // reorg fork to main
241 | uint256 ancestorId = chainId;
242 | uint256 forkId = _incrementChainCounter();
243 | uint32 forkHeight = height - 1;
244 |
245 | // TODO: add new fork struct for old main
246 |
247 | while (ancestorId != MAIN_CHAIN_ID) {
248 | for (
249 | uint256 i = _forks[ancestorId].descendants.length;
250 | i > 0;
251 | i--
252 | ) {
253 | // get next descendant in fork
254 | bytes32 descendant = _forks[ancestorId].descendants[i - 1];
255 | // promote header to main chain
256 | _headers[descendant].chainId = uint64(MAIN_CHAIN_ID);
257 | // demote old header to new fork
258 | _headers[_chain[height]].chainId = uint64(forkId);
259 | // swap header at height
260 | _chain[height] = descendant;
261 | forkHeight--;
262 | }
263 |
264 | bytes32 ancestor = _forks[ancestorId].ancestor;
265 | ancestorId = _headers[ancestor].chainId;
266 | }
267 |
268 | emit ChainReorg(_bestBlock, hashCurrBlock, chainId);
269 |
270 | _bestBlock = hashCurrBlock;
271 | _bestHeight = height;
272 |
273 | delete _forks[chainId];
274 |
275 | // extend to current head
276 | _chain[_bestHeight] = _bestBlock;
277 | _headers[_bestBlock].chainId = uint64(MAIN_CHAIN_ID);
278 | }
279 |
280 | /**
281 | * @notice Checks if the difficulty target should be adjusted at this block height
282 | * @param height Block height to be checked
283 | * @return True if block height is at difficulty adjustment interval, otherwise false
284 | */
285 | function _isPeriodStart(uint32 height) internal pure returns (bool) {
286 | return height % DIFFICULTY_ADJUSTMENT_INTERVAL == 0;
287 | }
288 |
289 | function _isPeriodEnd(uint32 height) internal pure returns (bool) {
290 | return height % DIFFICULTY_ADJUSTMENT_INTERVAL == 2015;
291 | }
292 |
293 | /**
294 | * @notice Validates difficulty interval
295 | * @dev Reverts if previous period invalid
296 | * @param prevStartTarget Period starting target
297 | * @param prevStartTime Period starting timestamp
298 | * @param prevEndTarget Period ending target
299 | * @param prevEndTime Period ending timestamp
300 | * @param nextTarget Next period starting target
301 | * @return True if difficulty level is valid
302 | */
303 | function isCorrectDifficultyTarget(
304 | uint256 prevStartTarget,
305 | uint64 prevStartTime,
306 | uint256 prevEndTarget,
307 | uint64 prevEndTime,
308 | uint256 nextTarget
309 | ) public pure returns (bool) {
310 | require(
311 | BTCUtils.calculateDifficulty(prevStartTarget) ==
312 | BTCUtils.calculateDifficulty(prevEndTarget),
313 | ERR_DIFF_PERIOD
314 | );
315 |
316 | uint256 expectedTarget = BTCUtils.retargetAlgorithm(
317 | prevStartTarget,
318 | prevStartTime,
319 | prevEndTime
320 | );
321 |
322 | return (nextTarget & expectedTarget) == nextTarget;
323 | }
324 |
325 | /**
326 | * @dev See {IRelay-getBlockHeight}.
327 | */
328 | function getBlockHeight(bytes32 digest)
329 | external
330 | view
331 | override
332 | returns (uint32)
333 | {
334 | return _headers[digest].height;
335 | }
336 |
337 | /**
338 | * @dev See {IRelay-getBlockHash}.
339 | */
340 | function getBlockHash(uint32 height)
341 | external
342 | view
343 | override
344 | returns (bytes32)
345 | {
346 | bytes32 digest = _chain[height];
347 | require(digest > 0, ERR_BLOCK_NOT_FOUND);
348 | return digest;
349 | }
350 |
351 | /**
352 | * @dev See {IRelay-getBestBlock}.
353 | */
354 | function getBestBlock()
355 | external
356 | view
357 | override
358 | returns (bytes32 digest, uint32 height)
359 | {
360 | return (_bestBlock, _bestHeight);
361 | }
362 |
363 | /**
364 | * @dev See {IRelay-verifyTx}.
365 | */
366 | function verifyTx(
367 | uint32 height,
368 | uint256 index,
369 | bytes32 txid,
370 | bytes calldata header,
371 | bytes calldata proof,
372 | uint256 confirmations,
373 | bool insecure
374 | ) external view override returns (bool) {
375 | // txid must be little endian
376 | require(txid != 0, ERR_INVALID_TXID);
377 |
378 | if (insecure) {
379 | require(height + confirmations <= _bestHeight, ERR_CONFIRMS);
380 | } else {
381 | require(height + CONFIRMATIONS <= _bestHeight, ERR_CONFIRMS);
382 | }
383 |
384 | require(_chain[height] == header.hash256(), ERR_BLOCK_NOT_FOUND);
385 | bytes32 root = header.extractMerkleRootLE().toBytes32();
386 | require(ValidateSPV.prove(txid, root, proof, index), ERR_VERIFY_TX);
387 |
388 | return true;
389 | }
390 | }
391 |
--------------------------------------------------------------------------------
/contracts/Script.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | pragma solidity ^0.6.0;
4 |
5 | import {BytesLib} from '@interlay/bitcoin-spv-sol/contracts/BytesLib.sol';
6 | import {BTCUtils} from '@interlay/bitcoin-spv-sol/contracts/BTCUtils.sol';
7 |
8 | library Script {
9 | using BytesLib for bytes;
10 | using BTCUtils for bytes;
11 |
12 | bytes1 internal constant OP_HASH160 = 0xa9;
13 | bytes1 internal constant OP_EQUAL = 0x87;
14 | bytes1 internal constant OP_DUP = 0x76;
15 | bytes1 internal constant OP_EQUALVERIFY = 0x88;
16 | bytes1 internal constant OP_CHECKSIG = 0xac;
17 | bytes1 internal constant OP_CHECKLOCKTIMEVERIFY = 0xb1;
18 | bytes1 internal constant OP_DROP = 0x75;
19 | bytes1 internal constant OP_0 = 0x00;
20 | bytes1 internal constant OP_RETURN = 0x6a;
21 |
22 | // EXCEPTION MESSAGES
23 | string internal constant ERR_INVALID_SIZE = 'Invalid size';
24 | string internal constant ERR_INVALID_OPCODE = 'Invalid opcode';
25 |
26 | // 0x76 (OP_DUP) - 0xa9 (OP_HASH160) - 0x14 (20 bytes len) - <20 bytes pubkey hash> - 0x88 (OP_EQUALVERIFY) - 0xac (OP_CHECKSIG)
27 | function isP2PKH(bytes memory script) internal pure returns (bool) {
28 | return
29 | (script.length == 25) &&
30 | (script[0] == OP_DUP) &&
31 | (script[1] == OP_HASH160) &&
32 | (script[2] == 0x14) &&
33 | (script[23] == OP_EQUALVERIFY) &&
34 | (script[24] == OP_CHECKSIG);
35 | }
36 |
37 | // solhint-disable-next-line func-name-mixedcase
38 | function P2PKH(bytes memory script) internal pure returns (bytes20) {
39 | return toBytes20(script.slice(3, 20));
40 | }
41 |
42 | function isP2WPKH(bytes memory script) internal pure returns (bool) {
43 | return (script.length == 22) && (script[1] == 0x14);
44 | }
45 |
46 | // solhint-disable-next-line func-name-mixedcase
47 | function P2WPKH(bytes memory script)
48 | internal
49 | pure
50 | returns (bytes1, bytes20)
51 | {
52 | return (script[0], toBytes20(script.slice(2, 20)));
53 | }
54 |
55 | // 0xa9 (OP_HASH160) - 0x14 (20 bytes hash) - <20 bytes script hash> - 0x87 (OP_EQUAL)
56 | function isP2SH(bytes memory script) internal pure returns (bool) {
57 | return
58 | (script.length == 23) &&
59 | (script[0] == OP_HASH160) &&
60 | (script[1] == 0x14) &&
61 | (script[22] == OP_EQUAL);
62 | }
63 |
64 | // solhint-disable-next-line func-name-mixedcase
65 | function P2SH(bytes memory script) internal pure returns (bytes20) {
66 | return toBytes20(script.slice(2, 20));
67 | }
68 |
69 | function isOpReturn(bytes memory script) internal pure returns (bool) {
70 | return script[0] == OP_RETURN;
71 | }
72 |
73 | function OpReturn(bytes memory script)
74 | internal
75 | pure
76 | returns (bytes memory)
77 | {
78 | bytes memory output = script.slice(1, script.length - 1);
79 | return output.slice(1, uint8(output[0]));
80 | }
81 |
82 | // 04 9f7b2a5c b1 75 76 a9 14 371c20fb2e9899338ce5e99908e64fd30b789313 88 ac
83 | function isCLTV(bytes memory script)
84 | internal
85 | pure
86 | returns (uint256, bytes memory)
87 | {
88 | uint256 varIntLen = script.determineVarIntDataLength();
89 | if (varIntLen == 0) {
90 | varIntLen = 1;
91 | }
92 | uint256 timeLen = script.slice(0, varIntLen).bytesToUint();
93 | uint256 timestamp = script
94 | .slice(varIntLen, timeLen)
95 | .reverseEndianness()
96 | .bytesToUint();
97 | uint256 pos = varIntLen + timeLen;
98 | require(script.length == pos + 27, ERR_INVALID_SIZE);
99 |
100 | require(script[pos] == OP_CHECKLOCKTIMEVERIFY, ERR_INVALID_OPCODE);
101 | require(script[pos + 1] == OP_DROP, ERR_INVALID_OPCODE);
102 | require(script[pos + 2] == OP_DUP, ERR_INVALID_OPCODE);
103 | require(script[pos + 3] == OP_HASH160, ERR_INVALID_OPCODE);
104 | require(script[pos + 4] == 0x14, ERR_INVALID_OPCODE); // OP_PUSHDATA
105 |
106 | require(script[pos + 5 + 20] == OP_EQUALVERIFY, ERR_INVALID_OPCODE);
107 | require(script[pos + 6 + 20] == OP_CHECKSIG, ERR_INVALID_OPCODE);
108 |
109 | return (timestamp, script.slice(pos + 5, 20));
110 | }
111 |
112 | function toBytes20(bytes memory data)
113 | internal
114 | pure
115 | returns (bytes20 result)
116 | {
117 | if (data.length == 0) {
118 | return 0x0;
119 | }
120 |
121 | assembly {
122 | // solhint-disable-previous-line no-inline-assembly
123 | result := mload(add(data, 0x20))
124 | }
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/contracts/ScriptDelegate.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | pragma solidity ^0.6.0;
4 |
5 | import {Script} from './Script.sol';
6 |
7 | contract ScriptDelegate {
8 | using Script for bytes;
9 |
10 | function isP2PKH(bytes memory script) public pure returns (bool) {
11 | return script.isP2PKH();
12 | }
13 |
14 | // solhint-disable-next-line func-name-mixedcase
15 | function P2PKH(bytes memory script) public pure returns (bytes20) {
16 | return script.P2PKH();
17 | }
18 |
19 | function isP2WPKH(bytes memory script) public pure returns (bool) {
20 | return script.isP2WPKH();
21 | }
22 |
23 | // solhint-disable-next-line func-name-mixedcase
24 | function P2WPKH(bytes memory script)
25 | public
26 | pure
27 | returns (bytes1 version, bytes20 program)
28 | {
29 | return script.P2WPKH();
30 | }
31 |
32 | function isP2SH(bytes memory script) public pure returns (bool) {
33 | return script.isP2SH();
34 | }
35 |
36 | // solhint-disable-next-line func-name-mixedcase
37 | function P2SH(bytes memory script) public pure returns (bytes20) {
38 | return script.P2SH();
39 | }
40 |
41 | function isOpReturn(bytes memory script) public pure returns (bool) {
42 | return script.isOpReturn();
43 | }
44 |
45 | function OpReturn(bytes memory script) public pure returns (bytes memory) {
46 | return script.OpReturn();
47 | }
48 |
49 | function isCLTV(bytes memory script)
50 | public
51 | pure
52 | returns (uint256 time, bytes memory addr)
53 | {
54 | return script.isCLTV();
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/contracts/TestRelay.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | pragma solidity ^0.6.0;
4 |
5 | import {SafeMath} from '@openzeppelin/contracts/math/SafeMath.sol';
6 | import {BytesLib} from '@interlay/bitcoin-spv-sol/contracts/BytesLib.sol';
7 | import {BTCUtils} from '@interlay/bitcoin-spv-sol/contracts/BTCUtils.sol';
8 | import {ValidateSPV} from '@interlay/bitcoin-spv-sol/contracts/ValidateSPV.sol';
9 | import {Relay} from './Relay.sol';
10 |
11 | /// @title Testnet BTC Relay
12 | contract TestRelay is Relay {
13 | using SafeMath for uint256;
14 | using BytesLib for bytes;
15 | using BTCUtils for bytes;
16 |
17 | /**
18 | * @notice Initializes the relay with the provided block.
19 | * @param header Genesis block header
20 | * @param height Genesis block height
21 | */
22 | constructor(bytes memory header, uint32 height)
23 | public
24 | Relay(header, height)
25 | {
26 | // solhint-disable-previous-line no-empty-blocks
27 | }
28 |
29 | /**
30 | * @dev Override to remove the difficulty check
31 | */
32 | function _submitBlockHeader(bytes memory header) internal override {
33 | require(header.length == 80, ERR_INVALID_HEADER_SIZE);
34 |
35 | bytes32 hashPrevBlock = header.extractPrevBlockLE().toBytes32();
36 | bytes32 hashCurrBlock = header.hash256();
37 |
38 | // Fail if block already exists
39 | require(_headers[hashCurrBlock].height == 0, ERR_DUPLICATE_BLOCK);
40 |
41 | // Fail if previous block hash not in current state of main chain
42 | require(_headers[hashPrevBlock].height > 0, ERR_PREVIOUS_BLOCK);
43 |
44 | uint256 target = header.extractTarget();
45 |
46 | // Check the PoW solution matches the target specified in the block header
47 | require(
48 | abi.encodePacked(hashCurrBlock).reverseEndianness().bytesToUint() <=
49 | target,
50 | ERR_LOW_DIFFICULTY
51 | );
52 |
53 | uint32 height = 1 + _headers[hashPrevBlock].height;
54 |
55 | // NO DIFFICULTY CHECK
56 |
57 | uint256 chainId = _headers[hashPrevBlock].chainId;
58 | bool isNewFork = _forks[chainId].height !=
59 | _headers[hashPrevBlock].height;
60 |
61 | if (isNewFork) {
62 | chainId = _incrementChainCounter();
63 | _initializeFork(hashCurrBlock, hashPrevBlock, chainId, height);
64 |
65 | _storeBlockHeader(hashCurrBlock, height, chainId);
66 | } else {
67 | _storeBlockHeader(hashCurrBlock, height, chainId);
68 |
69 | if (chainId == MAIN_CHAIN_ID) {
70 | _bestBlock = hashCurrBlock;
71 | _bestHeight = height;
72 |
73 | // extend height of main chain
74 | _forks[chainId].height = height;
75 | _chain[height] = hashCurrBlock;
76 | } else if (height >= _bestHeight + CONFIRMATIONS) {
77 | _reorgChain(chainId, height, hashCurrBlock);
78 | } else {
79 | // extend fork
80 | _forks[chainId].height = height;
81 | _forks[chainId].descendants.push(hashCurrBlock);
82 | }
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/hardhat.config.ts:
--------------------------------------------------------------------------------
1 | require('@nomiclabs/hardhat-ganache');
2 | require('@nomiclabs/hardhat-waffle');
3 | require('hardhat-typechain');
4 | import '@nomiclabs/hardhat-ethers';
5 | // require('buidler-gas-reporter');
6 |
7 | const INFURA_API_KEY = process.env.INFURA_API_KEY || '';
8 | const ROPSTEN_PRIVATE_KEY =
9 | process.env.ROPSTEN_PRIVATE_KEY ||
10 | '5de75329e619948d55744d85d763790ae3f7643f0a498070558acdb37d6b2057'; // Dummy wallet
11 |
12 | const COINMARKETCAP_API_KEY = process.env.COINMARKETCAP_API_KEY;
13 |
14 | const config = {
15 | defaultNetwork: 'hardhat',
16 | solidity: {
17 | version: '0.6.6',
18 | optimizer: {enabled: true, runs: 500}
19 | },
20 | paths: {
21 | sources: './contracts',
22 | tests: './test'
23 | },
24 | typechain: {
25 | outDir: './typechain',
26 | target: 'ethers-v5'
27 | },
28 | networks: {
29 | hardhat: {},
30 | ganache: {
31 | url: 'http://127.0.0.1:8545',
32 | mnemonic:
33 | 'lion album emotion suffer october belt uphold mind chronic stool february flag',
34 | networkId: 3,
35 | timeout: 0,
36 | logger: console
37 | },
38 | ropsten: {
39 | url: `https://ropsten.infura.io/v3/${INFURA_API_KEY}`,
40 | accounts: [ROPSTEN_PRIVATE_KEY]
41 | }
42 | },
43 | gasReporter: {
44 | enabled: COINMARKETCAP_API_KEY ? true : false,
45 | coinmarketcap: COINMARKETCAP_API_KEY,
46 | currency: 'GBP',
47 | src: './contracts'
48 | }
49 | };
50 |
51 | export default config;
52 |
--------------------------------------------------------------------------------
/index.ts:
--------------------------------------------------------------------------------
1 | export {ethers} from 'ethers';
2 | export {IRelayFactory} from './typechain/IRelayFactory';
3 |
4 | export const contracts = {
5 | ropsten: '0x43E5180B3F3C29F0C8BE0B100f48893993ce3600'
6 | };
7 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@interlay/btc-relay-sol",
3 | "version": "0.3.14",
4 | "description": "BTC Relay in Solidity",
5 | "main": "dist/index.js",
6 | "types": "dist/index.d.ts",
7 | "dependencies": {
8 | "@interlay/bitcoin-spv-sol": "3.2.2",
9 | "@openzeppelin/contracts": "3.2.0",
10 | "bitcoinjs-lib": "^5.1.7",
11 | "ethers": "^5.0.19"
12 | },
13 | "files": [
14 | "contracts",
15 | "dist"
16 | ],
17 | "devDependencies": {
18 | "@nomiclabs/hardhat-ethers": "^2.0.0",
19 | "@nomiclabs/hardhat-etherscan": "^2.0.0",
20 | "@nomiclabs/hardhat-ganache": "^2.0.0",
21 | "@nomiclabs/hardhat-waffle": "^2.0.0",
22 | "@typechain/ethers-v5": "^2.0.0",
23 | "@types/chai": "^4.2.8",
24 | "@types/mocha": "^8.0.3",
25 | "@types/node": "^14.14.2",
26 | "@typescript-eslint/eslint-plugin": "^4.5.0",
27 | "@typescript-eslint/parser": "^4.5.0",
28 | "buidler-gas-reporter": "^0.1.4",
29 | "chai": "^4.2.0",
30 | "eslint": "^7.11.0",
31 | "eslint-config-prettier": "^6.10.0",
32 | "eslint-plugin-prettier": "^3.1.2",
33 | "ethereum-waffle": "^3.1.1",
34 | "hardhat": "^2.0.0",
35 | "hardhat-deploy-ethers": "^0.3.0-beta.5",
36 | "hardhat-gas-reporter": "^1.0.0-beta.0",
37 | "hardhat-typechain": "^0.2.5",
38 | "husky": "^4.2.5",
39 | "prettier": "^2.0.5",
40 | "prettier-plugin-solidity": "^1.0.0-alpha.56",
41 | "solhint": "^3.1.0",
42 | "solhint-plugin-prettier": "^0.0.5",
43 | "solidity-coverage": "^0.7.1",
44 | "ts-generator": "0.1.1",
45 | "ts-node": "^9.0.0",
46 | "typechain": "^3.0.0",
47 | "typescript": "^4.0.3"
48 | },
49 | "scripts": {
50 | "build": "yarn run compile && hardhat typechain",
51 | "compile": "hardhat compile",
52 | "test": "hardhat test",
53 | "deploy": "hardhat run scripts/deploy.ts",
54 | "ganache": "hardhat run scripts/ganache.ts",
55 | "tsc": "tsc && cp typechain/*.d.ts dist/typechain/",
56 | "clean": "hardhat clean && rm -rf dist",
57 | "lint::typescript": "eslint './**/*.ts' --ext .ts",
58 | "prettier::typescript": "prettier --write './**/*.ts'",
59 | "lint::solidity": "solhint 'contracts/**/*.sol'",
60 | "prettier::solidity": "prettier --write 'contracts/**/*.sol'",
61 | "lint": "yarn run lint::typescript && yarn run lint::solidity",
62 | "prettier": "yarn run prettier::typescript && yarn run prettier::solidity"
63 | },
64 | "husky": {
65 | "hooks": {
66 | "pre-commit": "yarn run prettier"
67 | }
68 | },
69 | "repository": {
70 | "type": "git",
71 | "url": "git+https://gitlab.com/interlay/btc-relay-sol.git"
72 | },
73 | "keywords": [
74 | "bitcoin",
75 | "relay",
76 | "ethereum",
77 | "solidity"
78 | ],
79 | "maintainers": [
80 | "Gregory Hill",
81 | "Alexei Zamyatin",
82 | "Dominik Harz",
83 | "Daniel Perez"
84 | ],
85 | "license": "ISC",
86 | "bugs": {
87 | "url": "https://gitlab.com/interlay/btc-relay-sol/issues"
88 | },
89 | "homepage": "https://gitlab.com/interlay/btc-relay-sol#readme"
90 | }
91 |
--------------------------------------------------------------------------------
/scripts/builder.ts:
--------------------------------------------------------------------------------
1 | import * as bitcoin from 'bitcoinjs-lib';
2 |
3 | function p2wpkh(data: Buffer): Buffer {
4 | return bitcoin.script.compile([
5 | bitcoin.script.OPS.OP_0,
6 | bitcoin.script.OPS.OP_20,
7 | data
8 | ]);
9 | }
10 |
11 | export function genesis(): bitcoin.Block {
12 | const block = new bitcoin.Block();
13 | block.version = 1;
14 | block.timestamp = 1296688602;
15 | block.bits = 545259519;
16 | block.prevHash = Buffer.from(
17 | '0000000000000000000000000000000000000000000000000000000000000000',
18 | 'hex'
19 | );
20 | block.merkleRoot = Buffer.from(
21 | '3ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a',
22 | 'hex'
23 | );
24 | block.nonce = 2;
25 | return block;
26 | }
27 |
28 | function mine(block: bitcoin.Block): bitcoin.Block {
29 | block.merkleRoot = bitcoin.Block.calculateMerkleRoot(
30 | block.transactions || []
31 | );
32 |
33 | block.nonce = 0;
34 | while (!block.checkProofOfWork()) {
35 | block.nonce += 1;
36 | }
37 |
38 | return block;
39 | }
40 |
41 | export function generate(address: string, prevHash?: Buffer): bitcoin.Block {
42 | const block = new bitcoin.Block();
43 | block.version = 536870912;
44 | block.timestamp = 1592233681;
45 | block.bits = 545259519;
46 | block.prevHash = prevHash;
47 |
48 | const tx = new bitcoin.Transaction();
49 | tx.ins = [
50 | {
51 | hash: Buffer.from(
52 | '0000000000000000000000000000000000000000000000000000000000000000',
53 | 'hex'
54 | ),
55 | index: 4294967295,
56 | script: Buffer.from('520101', 'hex'),
57 | sequence: 4294967295,
58 | witness: [
59 | Buffer.from(
60 | '0000000000000000000000000000000000000000000000000000000000000000',
61 | 'hex'
62 | )
63 | ]
64 | }
65 | ];
66 | const addr = bitcoin.address.fromBech32(address);
67 | tx.outs = [
68 | {
69 | value: 5000000000,
70 | script: p2wpkh(addr.data)
71 | },
72 | {
73 | value: 0,
74 | script: Buffer.from(
75 | '6a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf9',
76 | 'hex'
77 | )
78 | }
79 | ];
80 | block.transactions = [];
81 | block.transactions.push(tx);
82 |
83 | return mine(block);
84 | }
85 |
--------------------------------------------------------------------------------
/scripts/contracts.ts:
--------------------------------------------------------------------------------
1 | import {Signer, Contract} from 'ethers';
2 | import {TestRelayFactory} from '../typechain/TestRelayFactory';
3 | import {RelayFactory} from '../typechain/RelayFactory';
4 | // import {FunctionDescription} from '../typechain'
5 | // import {TransactionReceipt} from 'ethers/providers';
6 | import {Relay} from '../typechain/Relay';
7 | import {TestRelay} from '../typechain/TestRelay';
8 |
9 | export type Genesis = {
10 | header: string;
11 | height: number;
12 | };
13 |
14 | export async function DeployRelay(
15 | signer: Signer,
16 | genesis: Genesis
17 | ): Promise {
18 | const factory = new RelayFactory(signer);
19 | const contract = await factory.deploy(genesis.header, genesis.height);
20 | return contract.deployed();
21 | }
22 |
23 | export async function DeployTestRelay(
24 | signer: Signer,
25 | genesis: Genesis
26 | ): Promise {
27 | const factory = new TestRelayFactory(signer);
28 | const contract = await factory.deploy(genesis.header, genesis.height);
29 | return contract.deployed();
30 | }
31 |
32 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
33 | // export async function Call(
34 | // signer: Signer,
35 | // contract: C,
36 | // func: FunctionDescription<{encode: (arg0: T) => string}>,
37 | // args: T
38 | // ): Promise {
39 | // const call = contract.interface.functions[func.name].encode(args);
40 | // const response = await signer.sendTransaction({
41 | // to: contract.address,
42 | // data: call
43 | // });
44 | // return response.wait(0);
45 | // }
46 |
--------------------------------------------------------------------------------
/scripts/deploy.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | import {ethers} from 'hardhat';
3 | import {DeployTestRelay, Genesis} from './contracts';
4 |
5 | // const mainnet: Genesis = {
6 | // header: '0x000040202842774747733a4863b6bbb7b4cfb66baa9287d5ce0d13000000000000000000df550e01d02ee37fce8dd2fbf919a47b8b65684bcb48d4da699078916da2f7decbc7905ebc2013178f58d533',
7 | // height: 625332,
8 | // };
9 |
10 | const testnet: Genesis = {
11 | header:
12 | '0x0000002026f83590aa5ae1f3ec4f7b87ee2ef78ea27ca62130b96dc5e1000000000000008d6cbb517c561f53efa451193c692f5d863475be0811f8ec7fa038e5206814af5bb1855fa494021afab33009',
13 | height: 1862781
14 | };
15 |
16 | // const regtest: Genesis = {
17 | // header: '0x0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4adae5494dffff7f2002000000',
18 | // height: 0,
19 | // }
20 |
21 | async function main(genesis: Genesis): Promise {
22 | const signers = await ethers.getSigners();
23 | const contract = await DeployTestRelay(signers[0], genesis);
24 | console.log(`Genesis height: ${genesis.height}`);
25 | console.log(`Contract address: ${contract.address}`);
26 | // console.log(await contract.getHashAtHeight(start.height));
27 | }
28 |
29 | main(testnet)
30 | .then(() => process.exit(0))
31 | .catch((error) => {
32 | console.error(error);
33 | process.exit(1);
34 | });
35 |
--------------------------------------------------------------------------------
/scripts/fetch.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # https://github.com/Blockstream/esplora
4 |
5 | ## Transaction
6 |
7 | TXID=9f0370848f7bbf67908808997661a320af4f0075dce313e2934a576ed8204059
8 |
9 | echo -e "\nTx Proof:"
10 | curl -S https://blockstream.info/api/tx/${TXID}/merkle-proof --output -
11 |
12 | echo -e "\n\nTx Hex:"
13 | curl -S https://blockstream.info/api/tx/${TXID}/hex --output -
14 |
15 | ## Block
16 |
17 | HASH=000000000000000000059fc83a88cbbe1532e40edaf95fb0eb5fb76257977ff7
18 |
19 | echo -e "\n\nBlock Info:"
20 | curl -sS https://blockstream.info/api/block/${HASH} --output -
21 |
22 | echo -e "\n\nBlock Header:"
23 | curl -sS https://blockstream.info/api/block/${HASH}/raw --output - | tac | tac | xxd -p | tr -d \\n | head -c 160
--------------------------------------------------------------------------------
/scripts/ganache.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 |
3 | import config from '../hardhat.config';
4 | import * as child from 'child_process';
5 |
6 | const ganacheCmd = 'ganache-cli -d';
7 | const port = '-p 8545';
8 | const mnemonic = '-m '.concat(config.networks.ganache.mnemonic);
9 | const id = '-i '.concat(config.networks.ganache.networkId.toString());
10 |
11 | console.log(ganacheCmd.concat(' ', port, ' ', mnemonic, ' ', id));
12 |
13 | const ganache: child.ChildProcess = child.spawn('ganache-cli', [
14 | port,
15 | mnemonic,
16 | id
17 | ]);
18 |
19 | ganache.stdout!.on('data', (data) => {
20 | console.log(data.toString());
21 | });
22 | ganache.stderr!.on('data', (data) => {
23 | console.log(data.toString());
24 | });
25 | ganache.on('close', (code) => {
26 | console.log(`child process exited with code ${code}`);
27 | });
28 |
--------------------------------------------------------------------------------
/scripts/latest.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | HEIGHT=${HEIGHT:-$(bitcoin-cli -testnet getblockcount)}
4 | DIGEST=$(bitcoin-cli -testnet getblockhash ${HEIGHT})
5 | HEADER=$(bitcoin-cli -testnet getblock ${DIGEST} 0 | cut -c1-160)
6 |
7 | echo $HEIGHT
8 | echo $HEADER
--------------------------------------------------------------------------------
/scripts/latest.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 |
3 | import {ethers} from '@nomiclabs/buidler';
4 | import {TestRelayFactory} from '../typechain/TestRelayFactory';
5 |
6 | async function main(address: string): Promise {
7 | const signers = await ethers.signers();
8 | const factory = new TestRelayFactory(signers[0]);
9 | const contract = factory.attach(address);
10 | const height = (await contract.bestHeight()).toNumber();
11 | console.log(`Current height: ${height}`);
12 | }
13 |
14 | main('0x151eA753f0aF1634B90e1658054C247eFF1C2464')
15 | .then(() => process.exit(0))
16 | .catch((error) => {
17 | console.error(error);
18 | process.exit(1);
19 | });
20 |
--------------------------------------------------------------------------------
/scripts/metrics.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 |
3 | import {ethers} from '@nomiclabs/buidler';
4 | import {Genesis, DeployTestRelay, Call} from './contracts';
5 | import {TestRelay} from '../typechain/TestRelay';
6 |
7 | const testnet: Genesis = {
8 | header:
9 | '0x00e0ff2ffb2b253e060112bed62614fcf9ae61f82c08d9e6c36a1b53070000000000000003b91283ae15b75dc2023735356f1412411d07ece715f3ed0e612ab4349b7db83030eb5ef2a5011add0c8e82',
10 | height: 1772070
11 | };
12 |
13 | const header1 =
14 | '0x00e0ff2ff52256c9e5df9e6e39024ee23ce2604a6b32bb7ad4733bcff6000000000000005fcc9125f16b4ae49bf506be9443aeeb2c552fd31b38278d443cb2900a9d62d02631eb5ef2a5011adc56d881';
15 | const header2 =
16 | '0x00e0ff37df39f4cf01c11b5293b63253e72d72c6bf03c9c67c7f206d47010000000000000d8fd592ed0cb0eff8ba97654518535ded4ac65e60d8caff0f7f4c92318675343533eb5ef2a5011a63ca05a4';
17 | const header3 =
18 | '0x00e0ff379e7c88a954ff65b100f769b3cc939f6290f39a535ea20e54b400000000000000a1158216c4706c03fa2d32d5ed3bfa37ec88e3531f5038daf3a65cae1650a7ae3f33eb5ef2a5011a423e3ce6';
19 | const header4 =
20 | '0x00000020bc326e72bfe82dbb3bef4ce02c504c7b7a3e9e01c23942099700000000000000191210b151a77f1a304712f7ad0d032b3f273b78fa4845f64b4f5c132c9329f62534eb5ef2a5011a8248a3e2';
21 | const header5 =
22 | '0x00e0ff37a8d07739c1ed5596bfdac945d573dc0b9877ceba67d85d509a00000000000000d1944ac1b4c9cf39fcb65846b1fd964e73d3f6efdcfc10f9ff86c7a2d36f2c780436eb5ef2a5011aaa0bcd2e';
23 | const header6 =
24 | '0x000000206589831ee7a19099e0cac44e81c00e4cec2ffdbd11b8daa6a101000000000000cb015e7ea21600b6ea3d08bbdc248de9f09d033b80486d52628764ecb58676550037eb5ef2a5011a12fdc910';
25 |
26 | const tx = {
27 | id: '0x2aa1c99122795b13627a19e8d1c797f732d73192449dbb78070cd6dd8f2c5873',
28 | index: 26,
29 | proof:
30 | '0x80aa78579fcc3acaaa9f7ded3d79219994ab5d678cb2d3174a74e512e1bce312769d180c1dd9de52f2a346784eebb8d79dce4a7bc6e8f99a0fb86e031f12fda63c4c00554bbffbff036d7801d5b12e60797c7a7fffbd6e96631173862542beee5ded8c7972616fb41634adbfcffcb84f79ab4bd86b4b62e7c6e628bc4cbee782ef42b28c8bfae0d52680845f8109ffd0f869424e9e1d93f06a19ec53370ac349e91d43d22f5e989501bb73bb764369ff5f8c0299c80d9a771f227972de4e8233a9461039a44cae80048f60aaad19427a2dae896f619c302e412ebbd3e37f8503',
31 | header:
32 | '0x00e0ff2ffb2b253e060112bed62614fcf9ae61f82c08d9e6c36a1b53070000000000000003b91283ae15b75dc2023735356f1412411d07ece715f3ed0e612ab4349b7db83030eb5ef2a5011add0c8e82',
33 | headerHash:
34 | '0x00000000000000f6cf3b73d47abb326b4a60e23ce24e02396e9edfe5c95622f5',
35 | height: 1772070
36 | };
37 |
38 | async function submitHeader(relay: TestRelay, header: string): Promise {
39 | const transaction = await relay.submitBlockHeader(header);
40 | const receipt = await transaction.wait(0);
41 | console.log(`Gas [Header]: ${receipt.gasUsed?.toString()}`);
42 | }
43 |
44 | // async function submitHeaderBatch(
45 | // relay: TestRelay,
46 | // ...headers: string[]
47 | // ): Promise {
48 | // let batch =
49 | // '0x' +
50 | // headers
51 | // .map((header) => {
52 | // return header.substr(2);
53 | // })
54 | // .join('');
55 | // let transaction = await relay.submitBlockHeaderBatch(batch);
56 | // let receipt = await transaction.wait(0);
57 | // console.log(`Gas [Header - Batch]: ${receipt.gasUsed?.toString()}`);
58 | // }
59 |
60 | async function main(genesis: Genesis): Promise {
61 | const signers = await ethers.signers();
62 | const contract = await DeployTestRelay(signers[0], genesis);
63 | let receipt = await contract.deployTransaction.wait(0);
64 | console.log(`Gas [Deploy]: ${receipt.gasUsed?.toString()}`);
65 |
66 | // await submitHeaderBatch(contract, header1, header2, header3, header4, header5, header6);
67 | await submitHeader(contract, header1);
68 | await submitHeader(contract, header2);
69 | await submitHeader(contract, header3);
70 | await submitHeader(contract, header4);
71 | await submitHeader(contract, header5);
72 | await submitHeader(contract, header6);
73 |
74 | receipt = await Call(
75 | signers[0],
76 | contract,
77 | contract.interface.functions.verifyTx,
78 | [tx.height, tx.index, tx.id, tx.header, tx.proof, 0, false]
79 | );
80 | console.log(`Gas [Verify]: ${receipt.gasUsed?.toString()}`);
81 | }
82 |
83 | main(testnet)
84 | .then(() => process.exit(0))
85 | .catch((error) => {
86 | console.error(error);
87 | process.exit(1);
88 | });
89 |
--------------------------------------------------------------------------------
/scripts/parser_test.js:
--------------------------------------------------------------------------------
1 | const XCLAIM = artifacts.require("XCLAIM");
2 | const Relay = artifacts.require("test_relays/ValidRelay.sol");
3 | const InValidRelay = artifacts.require("test_relays/InValidRelay.sol");
4 |
5 | const bs58 = require('bs58');
6 | const truffleAssert = require('truffle-assertions');
7 | const math = require('mathjs');
8 |
9 |
10 | contract("confirmIssueTests", accounts => {
11 |
12 | var xclaim;
13 | var relay;
14 |
15 | // Standard test values
16 | let amountETH = 100;
17 | let amountBTC = 200000;
18 | let vaultId = 1;
19 | let userBTC = bs58.decode('16UjcYNBG9GTK4uq2f7yYEbuifqCzoLMGScG3f467Sd');
20 |
21 | let vaultAddress = bs58.decode('16UjcYNBG9GTK4uq2f7yYEbuifqCzoLMGScG3f467Ss');
22 | let ethCollateral = 1000000000000000000;
23 | let fee = 0;
24 |
25 |
26 | // This will reset the contract state between tests
27 | beforeEach(async function () {
28 | relay = await Relay.new();
29 | xclaim = await XCLAIM.new(relay.address);
30 |
31 | // Register a vault with default collateral and issue a single user the default amountBTC
32 |
33 | // Add vault with sufficient collateral
34 | let pubkey = hexToBytes("e20cf24f62878ad5da120bd1efe6bcd6b1e13ea3");
35 | let userpubkey = hexToBytes("e20cf24f62878ad5da120bd1efe6bcd6b1e13ea3")
36 |
37 | await xclaim.registerVault(vaultAddress, fee, pubkey,{ from: accounts[1], value: ethCollateral });
38 |
39 | // Issue ERC20 tokens to user
40 | await xclaim.requestIssue(amountBTC, vaultId, userBTC, userpubkey, { from: accounts[0] });
41 | });
42 |
43 | it("On successful issue the isc will emit a success for its issue event", async () => {
44 | // bytes calldata rawTransaction
45 | let rawTransaction = hexToBytes("01000000017df4eee40fef1cf0ac17bb9e723489a078511d8b402b1063d00a5f42dd5fa97b010000006a4730440220779202bedffce6d14c1d3918215b46de57aa3efb873455b0697ab9002a6226d3022039d684012fa8c92d8fe100cb23ec7c07a0129a58b55e82e507c78484b1ef378f0121027137d153419cbfe555b69a17c862978afa56cc2562568cfbae842c47636991a4ffffffff02400d0300000000001976a914e20cf24f62878ad5da120bd1efe6bcd6b1e13ea388ac0000000000000000036a013100000000");
46 | // uint256 _transactionIndex,
47 | let transactionIndex = 1;
48 | // uint256[] calldata _merkelSibling,
49 | let merkelSiblings = [1, 2];
50 | // uint256 _redeemId
51 | let issueId = 1;
52 | // uint256 _blockHash
53 | let blockHash = parseInt('0x0000000000009b958a82c10');
54 |
55 |
56 | let event = await xclaim.confirmIssue(rawTransaction,
57 | transactionIndex,
58 | merkelSiblings,
59 | blockHash,
60 | issueId);
61 |
62 | truffleAssert.eventEmitted(event, 'ConfirmIssue', (ev) => {
63 | assert.equal(
64 | ev.success,
65 | true,
66 | "The outcome of the issue should be a success"
67 | )
68 | assert.equal(
69 | ev.confirmId,
70 | issueId,
71 | "The issueId should match"
72 | )
73 | assert.equal(
74 | ev.errorMsg,
75 | "",
76 | "There should be no error msg"
77 | )
78 | return true;
79 | });
80 | });
81 |
82 | it("Confirm issue fails confirming issue for a invalid requestIssueId", async () => {
83 | // bytes calldata rawTransaction
84 | let rawTransaction = hexToBytes("01000000017df4eee40fef1cf0ac17bb9e723489a078511d8b402b1063d00a5f42dd5fa97b010000006a4730440220779202bedffce6d14c1d3918215b46de57aa3efb873455b0697ab9002a6226d3022039d684012fa8c92d8fe100cb23ec7c07a0129a58b55e82e507c78484b1ef378f0121027137d153419cbfe555b69a17c862978afa56cc2562568cfbae842c47636991a4ffffffff02400d0300000000001976a914e20cf24f62878ad5da120bd1efe6bcd6b1e13ea388ac0000000000000000126a10486f772069732065766572796f6e652100000000");
85 | // uint256 _transactionIndex,
86 | let transactionIndex = 1;
87 | // uint256[] calldata _merkelSibling,
88 | let merkelSiblings = [1, 2];
89 | // uint256 _redeemId
90 | let issueId = 1100;
91 | // uint256 _blockHash
92 | let blockHash = parseInt('0x0000000000009b958a82c10');
93 |
94 |
95 | let event = await xclaim.confirmIssue(rawTransaction,
96 | transactionIndex,
97 | merkelSiblings,
98 | blockHash,
99 | issueId,
100 | { from: accounts[4] });
101 |
102 | truffleAssert.eventEmitted(event, 'ConfirmIssue', (ev) => {
103 | assert.equal(
104 | ev.success,
105 | false,
106 | "Confirm should fail as issueID is invalid"
107 | )
108 | assert.equal(
109 | ev.confirmId,
110 | issueId,
111 | "The issueId should match"
112 | )
113 | assert.equal(
114 | ev.errorMsg,
115 | "error: no issue request for this sender",
116 | "There should be an error msg"
117 | )
118 | return true;
119 | });
120 | });
121 |
122 | it("On failed issue the isc will emit a failure for its issue event", async () => {
123 | relay = await InValidRelay.new();
124 | await XCLAIM.new(relay.address)
125 | .then(function (instance) {
126 | xclaim = instance;
127 | });
128 |
129 | // Register a vault with default collateral and issue a single user the default amountBTC
130 |
131 | let pubkey = hexToBytes("053496c1ea3d54d649ed54de490fda3425222440");
132 | let userpubkey = hexToBytes("e20cf24f62878ad5da120bd1efe6bcd6b1e13ea3")
133 |
134 |
135 | // Add vault with sufficient collateral
136 | await xclaim.registerVault(vaultAddress, fee, pubkey, { from: accounts[1], value: ethCollateral });
137 |
138 | // Issue ERC20 tokens to user
139 | await xclaim.requestIssue(amountBTC, vaultId, userBTC, userpubkey, { from: accounts[0] });
140 |
141 | // bytes calldata rawTransaction
142 | let rawTransaction = hexToBytes("01000000017df4eee40fef1cf0ac17bb9e723489a078511d8b402b1063d00a5f42dd5fa97b010000006a4730440220779202bedffce6d14c1d3918215b46de57aa3efb873455b0697ab9002a6226d3022039d684012fa8c92d8fe100cb23ec7c07a0129a58b55e82e507c78484b1ef378f0121027137d153419cbfe555b69a17c862978afa56cc2562568cfbae842c47636991a4ffffffff02400d0300000000001976a914e20cf24f62878ad5da120bd1efe6bcd6b1e13ea388ac0000000000000000126a10486f772069732065766572796f6e652100000000");
143 | // uint256 _transactionIndex,
144 | let transactionIndex = 1;
145 | // uint256[] calldata _merkelSibling,
146 | let merkelSiblings = [1, 2];
147 | // uint256 _redeemId
148 | let issueId = 1;
149 | // uint256 _blockHash
150 | let blockHash = parseInt('0x0000000000009b958a82c10');
151 |
152 |
153 | let event = await xclaim.confirmIssue(rawTransaction,
154 | transactionIndex,
155 | merkelSiblings,
156 | blockHash,
157 | issueId);
158 | truffleAssert.eventEmitted(event, 'ConfirmIssue', (ev) => {
159 | assert.equal(
160 | ev.success,
161 | false,
162 | "The outcome of the issue should be a failure(==false)"
163 | )
164 | assert.equal(
165 | ev.confirmId,
166 | issueId,
167 | "The issue id should match"
168 | )
169 | assert.equal(
170 | ev.errorMsg,
171 | "error: failed validity",
172 | "The error message should say: 'Error: failed relay validity'"
173 | )
174 | return true;
175 | });
176 | });
177 |
178 | it("confirming an issue will fail if incorrect OP_RETURN data present", async () => {
179 | // bytes calldata rawTransaction
180 | let rawTransaction = hexToBytes("01000000017df4eee40fef1cf0ac17bb9e723489a078511d8b402b1063d00a5f42dd5fa97b010000006a4730440220779202bedffce6d14c1d3918215b46de57aa3efb873455b0697ab9002a6226d3022039d684012fa8c92d8fe100cb23ec7c07a0129a58b55e82e507c78484b1ef378f0121027137d153419cbfe555b69a17c862978afa56cc2562568cfbae842c47636991a4ffffffff02400d0300000000001976a914e20cf24f62878ad5da120bd1efe6bcd6b1e13ea388ac0000000000000000036a013200000000");
181 | // uint256 _transactionIndex,
182 | let transactionIndex = 1;
183 | // uint256[] calldata _merkelSibling,
184 | let merkelSiblings = [1, 2];
185 | // uint256 _redeemId
186 | let issueId = 1;
187 | // uint256 _blockHash
188 | let blockHash = parseInt('0x0000000000009b958a82c10');
189 |
190 |
191 | let event = await xclaim.confirmIssue(rawTransaction,
192 | transactionIndex,
193 | merkelSiblings,
194 | blockHash,
195 | issueId);
196 |
197 | truffleAssert.eventEmitted(event, 'ConfirmIssue', (ev) => {
198 | assert.equal(
199 | ev.success,
200 | false,
201 | "The outcome of the issue should be a success"
202 | )
203 | return true;
204 | });
205 | });
206 |
207 | it("Confirm issue fails confirming issue for invalid funds sent", async () => {
208 | // bytes calldata rawTransaction
209 | let rawTransaction = hexToBytes("01000000017df4eee40fef1cf0ac17bb9e723489a078511d8b402b1063d00a5f42dd5fa97b010000006a4730440220779202bedffce6d14c1d3918215b46de57aa3efb873455b0697ab9002a6226d3022039d684012fa8c92d8fe100cb23ec7c07a0129a58b55e82e507c78484b1ef378f0121027137d153419cbfe555b69a17c862978afa56cc2562568cfbae842c47636991a4ffffffff02400d0200000000001976a914e20cf24f62878ad5da120bd1efe6bcd6b1e13ea388ac0000000000000000036a013100000000");
210 |
211 | // uint256 _transactionIndex,
212 | let transactionIndex = 1;
213 | // uint256[] calldata _merkelSibling,
214 | let merkelSiblings = [1, 2];
215 | // uint256 _redeemId
216 | let issueId = 1;
217 | // uint256 _blockHash
218 | let blockHash = parseInt('0x0000000000009b958a82c10');
219 |
220 |
221 | let event = await xclaim.confirmIssue(rawTransaction,
222 | transactionIndex,
223 | merkelSiblings,
224 | blockHash,
225 | issueId,
226 | { from: accounts[4] });
227 |
228 | truffleAssert.eventEmitted(event, 'ConfirmIssue', (ev) => {
229 | assert.equal(
230 | ev.success,
231 | false,
232 | "Confirm should fail as invalid funds in rawtx"
233 | )
234 | return true;
235 | });
236 | })
237 |
238 | it("Confirm issue fails confirming issue for invalid receiver of funds", async () => {
239 | // bytes calldata rawTransaction
240 | let rawTransaction = hexToBytes("01000000017df4eee40fef1cf0ac17bb9e723489a078511d8b402b1063d00a5f42dd5fa97b010000006a4730440220779202bedffce6d14c1d3918215b46de57aa3efb873455b0697ab9002a6226d3022039d684012fa8c92d8fe100cb23ec7c07a0129a58b55e82e507c78484b1ef378f0121027137d153419cbfe555b69a17c862978afa56cc2562568cfbae842c47636991a4ffffffff02400d0300000000001976a914e20cf23f62878ad5da120bd1efe6bcd6b1e13ea388ac0000000000000000036a013100000000");
241 | // uint256 _transactionIndex,
242 | let transactionIndex = 1;
243 | // uint256[] calldata _merkelSibling,
244 | let merkelSiblings = [1, 2];
245 | // uint256 _redeemId
246 | let issueId = 1;
247 | // uint256 _blockHash
248 | let blockHash = parseInt('0x0000000000009b958a82c10');
249 |
250 |
251 | let event = await xclaim.confirmIssue(rawTransaction,
252 | transactionIndex,
253 | merkelSiblings,
254 | blockHash,
255 | issueId,
256 | { from: accounts[4] });
257 |
258 | truffleAssert.eventEmitted(event, 'ConfirmIssue', (ev) => {
259 | assert.equal(
260 | ev.success,
261 | false,
262 | "Confirm should fail as invalid funds in rawtx"
263 | )
264 | return true;
265 | });
266 | })
267 |
268 | it("Confirm issue fails confirming issue for invalid OP_RETURN data", async () => {
269 | // bytes calldata rawTransaction
270 | let rawTransaction = hexToBytes("01000000017df4eee40fef1cf0ac17bb9e723489a078511d8b402b1063d00a5f42dd5fa97b010000006a4730440220779202bedffce6d14c1d3918215b46de57aa3efb873455b0697ab9002a6226d3022039d684012fa8c92d8fe100cb23ec7c07a0129a58b55e82e507c78484b1ef378f0121027137d153419cbfe555b69a17c862978afa56cc2562568cfbae842c47636991a4ffffffff02400d0300000000001976a914e20cf24f62878ad5da120bd1efe6bcd6b1e13ea388ac0000000000000000036a013200000000");
271 | // uint256 _transactionIndex,
272 | let transactionIndex = 1;
273 | // uint256[] calldata _merkelSibling,
274 | let merkelSiblings = [1, 2];
275 | // uint256 _redeemId
276 | let issueId = 1;
277 | // uint256 _blockHash
278 | let blockHash = parseInt('0x0000000000009b958a82c10');
279 |
280 |
281 | let event = await xclaim.confirmIssue(rawTransaction,
282 | transactionIndex,
283 | merkelSiblings,
284 | blockHash,
285 | issueId,
286 | { from: accounts[4] });
287 |
288 | truffleAssert.eventEmitted(event, 'ConfirmIssue', (ev) => {
289 | assert.equal(
290 | ev.success,
291 | false,
292 | "Confirm should fail as invalid OP_RETURN data"
293 | )
294 | return true;
295 | });
296 | })
297 |
298 | it("Confirming issue will update the amount of btc stored for a user in vault mapping", async () => {
299 | // bytes calldata rawTransaction
300 | let rawTransaction = hexToBytes("01000000017df4eee40fef1cf0ac17bb9e723489a078511d8b402b1063d00a5f42dd5fa97b010000006a4730440220779202bedffce6d14c1d3918215b46de57aa3efb873455b0697ab9002a6226d3022039d684012fa8c92d8fe100cb23ec7c07a0129a58b55e82e507c78484b1ef378f0121027137d153419cbfe555b69a17c862978afa56cc2562568cfbae842c47636991a4ffffffff02400d0300000000001976a914e20cf24f62878ad5da120bd1efe6bcd6b1e13ea388ac0000000000000000036a013100000000");
301 | // uint256 _transactionIndex,
302 | let transactionIndex = 1;
303 | // uint256[] calldata _merkelSibling,
304 | let merkelSiblings = [1, 2];
305 | // uint256 _redeemId
306 | let issueId = 1;
307 | // uint256 _blockHash
308 | let blockHash = parseInt('0x0000000000009b958a82c10');
309 |
310 |
311 | await xclaim.confirmIssue(rawTransaction,
312 | transactionIndex,
313 | merkelSiblings,
314 | blockHash,
315 | issueId);
316 |
317 | let tokens = await xclaim.getTokensStored(vaultId, accounts[0]);
318 | assert.equal(
319 | tokens,
320 | amountBTC,
321 | "The mapping of tokens stored for the user in vault should match that of request"
322 | )
323 | })
324 |
325 | it("Confirming issue will add vaultId to list of user vaultIds", async () => {
326 | // bytes calldata rawTransaction
327 | let rawTransaction = hexToBytes("01000000017df4eee40fef1cf0ac17bb9e723489a078511d8b402b1063d00a5f42dd5fa97b010000006a4730440220779202bedffce6d14c1d3918215b46de57aa3efb873455b0697ab9002a6226d3022039d684012fa8c92d8fe100cb23ec7c07a0129a58b55e82e507c78484b1ef378f0121027137d153419cbfe555b69a17c862978afa56cc2562568cfbae842c47636991a4ffffffff02400d0300000000001976a914e20cf24f62878ad5da120bd1efe6bcd6b1e13ea388ac0000000000000000036a013100000000");
328 | // uint256 _transactionIndex,
329 | let transactionIndex = 1;
330 | // uint256[] calldata _merkelSibling,
331 | let merkelSiblings = [1, 2];
332 | // uint256 _redeemId
333 | let issueId = 1;
334 | // uint256 _blockHash
335 | let blockHash = parseInt('0x0000000000009b958a82c10');
336 |
337 | let user = await xclaim.getUser(accounts[0]);
338 | assert.equal(
339 | user.vaultIds.length,
340 | 0,
341 | "Initially the user will have no vaults stored in vaultIds list"
342 | )
343 |
344 |
345 | await xclaim.confirmIssue(rawTransaction,
346 | transactionIndex,
347 | merkelSiblings,
348 | blockHash,
349 | issueId);
350 |
351 | user = await xclaim.getUser(accounts[0]);
352 | assert.equal(
353 | user.vaultIds[0],
354 | 1,
355 | "The user should have vault with id 1 stored"
356 | )
357 | })
358 |
359 | it("Confirming issue will add funds to the user account", async () => {
360 | // bytes calldata rawTransaction
361 | let rawTransaction = hexToBytes("01000000017df4eee40fef1cf0ac17bb9e723489a078511d8b402b1063d00a5f42dd5fa97b010000006a4730440220779202bedffce6d14c1d3918215b46de57aa3efb873455b0697ab9002a6226d3022039d684012fa8c92d8fe100cb23ec7c07a0129a58b55e82e507c78484b1ef378f0121027137d153419cbfe555b69a17c862978afa56cc2562568cfbae842c47636991a4ffffffff02400d0300000000001976a914e20cf24f62878ad5da120bd1efe6bcd6b1e13ea388ac0000000000000000036a013100000000");
362 | // uint256 _transactionIndex,
363 | let transactionIndex = 1;
364 | // uint256[] calldata _merkelSibling,
365 | let merkelSiblings = [1, 2];
366 | // uint256 _redeemId
367 | let issueId = 1;
368 | // uint256 _blockHash
369 | let blockHash = parseInt('0x0000000000009b958a82c10');
370 |
371 | let value = await xclaim.balanceOf(accounts[0]);
372 | assert.equal(
373 | value,
374 | 0,
375 | "The user should begin with no funds"
376 | )
377 |
378 | await xclaim.confirmIssue(rawTransaction,
379 | transactionIndex,
380 | merkelSiblings,
381 | blockHash,
382 | issueId);
383 |
384 | value = await xclaim.balanceOf(accounts[0]);
385 | assert.equal(
386 | value,
387 | amountBTC,
388 | "The number of tokens for the user should increase"
389 | )
390 | })
391 |
392 | it("Cancelling issue will check that only correct vault can cancel the issue", async () => {
393 | let issueId = 1;
394 | let event;
395 | await xclaim.setTimeOutTime(0);
396 | sleep(2000);
397 |
398 | try {
399 | event = await xclaim.cancelIssue(issueId, {from: accounts[0]})
400 | } catch (error) {
401 | assert.equal(
402 | error.toString(),
403 | "Error: Returned error: VM Exception while processing transaction: revert Only vault pertaining to issue may cancel the issue -- Reason given: Only vault pertaining to issue may cancel the issue.",
404 | "Error should match"
405 | )
406 | }
407 |
408 | try {
409 | event = await xclaim.cancelIssue(issueId, {from: accounts[1]})
410 | } catch (error) {
411 | assert.equal(
412 | error.toString(),
413 | "",
414 | "Should not error."
415 | )
416 | }
417 | })
418 |
419 | it("Cancelling issue will only work for non completed issue", async () => {
420 | // bytes calldata rawTransaction
421 | let rawTransaction = hexToBytes("01000000017df4eee40fef1cf0ac17bb9e723489a078511d8b402b1063d00a5f42dd5fa97b010000006a4730440220779202bedffce6d14c1d3918215b46de57aa3efb873455b0697ab9002a6226d3022039d684012fa8c92d8fe100cb23ec7c07a0129a58b55e82e507c78484b1ef378f0121027137d153419cbfe555b69a17c862978afa56cc2562568cfbae842c47636991a4ffffffff02400d0300000000001976a914e20cf24f62878ad5da120bd1efe6bcd6b1e13ea388ac0000000000000000036a013100000000");
422 | // uint256 _transactionIndex,
423 | let transactionIndex = 1;
424 | // uint256[] calldata _merkelSibling,
425 | let merkelSiblings = [1, 2];
426 | // uint256 _redeemId
427 | let issueId = 1;
428 | // uint256 _blockHash
429 | let blockHash = parseInt('0x0000000000009b958a82c10');
430 |
431 | await xclaim.confirmIssue(rawTransaction,
432 | transactionIndex,
433 | merkelSiblings,
434 | blockHash,
435 | issueId);
436 |
437 | try {
438 | event = await xclaim.cancelIssue(issueId, {from: accounts[1]})
439 | } catch (error) {
440 | assert.equal(
441 | error.toString(),
442 | "Error: Returned error: VM Exception while processing transaction: revert The issue should not already be completed -- Reason given: The issue should not already be completed.",
443 | "Should error"
444 | )
445 | }
446 | })
447 |
448 | it("Cancelling issue will unlock reserved collateral in vault", async () => {
449 | await xclaim.setTimeOutTime(0);
450 |
451 | sleep(2000);
452 |
453 | // bytes calldata rawTransaction
454 | let rawTransaction = hexToBytes("01000000017df4eee40fef1cf0ac17bb9e723489a078511d8b402b1063d00a5f42dd5fa97b010000006a4730440220779202bedffce6d14c1d3918215b46de57aa3efb873455b0697ab9002a6226d3022039d684012fa8c92d8fe100cb23ec7c07a0129a58b55e82e507c78484b1ef378f0121027137d153419cbfe555b69a17c862978afa56cc2562568cfbae842c47636991a4ffffffff02400d0300000000001976a914e20cf24f62878ad5da120bd1efe6bcd6b1e13ea388ac0000000000000000036a013100000000");
455 | // uint256 _transactionIndex,
456 | let transactionIndex = 1;
457 | // uint256[] calldata _merkelSibling,
458 | let merkelSiblings = [1, 2];
459 | // uint256 _redeemId
460 | let issueId = 1;
461 | // uint256 _blockHash
462 | let blockHash = parseInt('0x0000000000009b958a82c10');
463 |
464 | let event;
465 |
466 | try {
467 | event = await xclaim.cancelIssue(issueId, {from: accounts[1]})
468 | } catch (error) {
469 | assert.equal(
470 | error.toString(),
471 | "",
472 | "Should not error"
473 | )
474 | }
475 |
476 | event = await xclaim.getVaultFromId(vaultId);
477 | assert.equal(
478 | event.lockedBTC,
479 | 0,
480 | "there should be no more locked btc in vault"
481 | )
482 |
483 | })
484 |
485 | it("Confirming issue checks that maxTime has not exceeded for issue", async () => {
486 | xclaim = await XCLAIM.new(relay.address);
487 |
488 | // Register a vault with default collateral and issue a single user the default amountBTC
489 |
490 | // Add vault with sufficient collateral
491 | let pubkey = hexToBytes("e20cf24f62878ad5da120bd1efe6bcd6b1e13ea3");
492 | let userpubkey = hexToBytes("e20cf24f62878ad5da120bd1efe6bcd6b1e13ea3")
493 |
494 | await xclaim.registerVault(vaultAddress, fee, pubkey,{ from: accounts[1], value: ethCollateral });
495 |
496 | // times out after 0 seconds
497 | await xclaim.setTimeOutTime(0);
498 |
499 | // Issue ERC20 tokens to user
500 | await xclaim.requestIssue(amountBTC, vaultId, userBTC, userpubkey, { from: accounts[0] });
501 | // bytes calldata rawTransaction
502 | let rawTransaction = hexToBytes("01000000017df4eee40fef1cf0ac17bb9e723489a078511d8b402b1063d00a5f42dd5fa97b010000006a4730440220779202bedffce6d14c1d3918215b46de57aa3efb873455b0697ab9002a6226d3022039d684012fa8c92d8fe100cb23ec7c07a0129a58b55e82e507c78484b1ef378f0121027137d153419cbfe555b69a17c862978afa56cc2562568cfbae842c47636991a4ffffffff02400d0300000000001976a914e20cf24f62878ad5da120bd1efe6bcd6b1e13ea388ac0000000000000000036a013100000000");
503 | // uint256 _transactionIndex,
504 | let transactionIndex = 1;
505 | // uint256[] calldata _merkelSibling,
506 | let merkelSiblings = [1, 2];
507 | // uint256 _redeemId
508 | let issueId = 1;
509 | // uint256 _blockHash
510 | let blockHash = parseInt('0x0000000000009b958a82c10');
511 |
512 | sleep(1000);
513 |
514 | let error = "";
515 |
516 | try {
517 | await xclaim.confirmIssue(rawTransaction,
518 | transactionIndex,
519 | merkelSiblings,
520 | blockHash,
521 | issueId);
522 | } catch (e) {
523 | error = e;
524 | }
525 | assert.equal(
526 | error.toString(),
527 | "Error: Returned error: VM Exception while processing transaction: revert Max time has been passed to confirm issue -- Reason given: Max time has been passed to confirm issue.",
528 | "Should error as max time exceeded"
529 | )
530 | })
531 |
532 | it("Cancelling issue will only work if given time has been exceeded", async () => {
533 | // bytes calldata rawTransaction
534 | let rawTransaction = hexToBytes("01000000017df4eee40fef1cf0ac17bb9e723489a078511d8b402b1063d00a5f42dd5fa97b010000006a4730440220779202bedffce6d14c1d3918215b46de57aa3efb873455b0697ab9002a6226d3022039d684012fa8c92d8fe100cb23ec7c07a0129a58b55e82e507c78484b1ef378f0121027137d153419cbfe555b69a17c862978afa56cc2562568cfbae842c47636991a4ffffffff02400d0300000000001976a914e20cf24f62878ad5da120bd1efe6bcd6b1e13ea388ac0000000000000000036a013100000000");
535 | // uint256 _transactionIndex,
536 | let transactionIndex = 1;
537 | // uint256[] calldata _merkelSibling,
538 | let merkelSiblings = [1, 2];
539 | // uint256 _redeemId
540 | let issueId = 1;
541 | // uint256 _blockHash
542 | let blockHash = parseInt('0x0000000000009b958a82c10');
543 |
544 | let error = "";
545 |
546 | try {
547 | event = await xclaim.cancelIssue(issueId, {from: accounts[1]})
548 | } catch (e) {
549 | error = e;
550 | }
551 | assert.equal(
552 | error.toString(),
553 | "Error: Returned error: VM Exception while processing transaction: revert The issue has not yet expired -- Reason given: The issue has not yet expired.",
554 | "Should error"
555 | )
556 | })
557 |
558 | it("Sufficient funds transfered to vault on successful confirm issue", async () => {
559 | relay = await Relay.new();
560 | xclaim = await XCLAIM.new(relay.address);
561 |
562 | // Register a vault with default collateral and issue a single user the default amountBTC
563 |
564 | // Add vault with sufficient collateral
565 | let pubkey = hexToBytes("e20cf24f62878ad5da120bd1efe6bcd6b1e13ea3");
566 | let userpubkey = hexToBytes("e20cf24f62878ad5da120bd1efe6bcd6b1e13ea3")
567 |
568 | await xclaim.registerVault(vaultAddress, 10, pubkey,{ from: accounts[1], value: ethCollateral });
569 |
570 |
571 |
572 | // Issue ERC20 tokens to user
573 | await xclaim.requestIssue(amountBTC, vaultId, userBTC, userpubkey, { from: accounts[0], value: 10 });
574 | let prevUserBalance = await web3.eth.getBalance(accounts[1]);
575 |
576 | // bytes calldata rawTransaction
577 | let rawTransaction = hexToBytes("01000000017df4eee40fef1cf0ac17bb9e723489a078511d8b402b1063d00a5f42dd5fa97b010000006a4730440220779202bedffce6d14c1d3918215b46de57aa3efb873455b0697ab9002a6226d3022039d684012fa8c92d8fe100cb23ec7c07a0129a58b55e82e507c78484b1ef378f0121027137d153419cbfe555b69a17c862978afa56cc2562568cfbae842c47636991a4ffffffff02400d0300000000001976a914e20cf24f62878ad5da120bd1efe6bcd6b1e13ea388ac0000000000000000036a013100000000");
578 | // uint256 _transactionIndex,
579 | let transactionIndex = 1;
580 | // uint256[] calldata _merkelSibling,
581 | let merkelSiblings = [1, 2];
582 | // uint256 _redeemId
583 | let issueId = 1;
584 | // uint256 _blockHash
585 | let blockHash = parseInt('0x0000000000009b958a82c10');
586 |
587 |
588 | let event = await xclaim.confirmIssue(rawTransaction,
589 | transactionIndex,
590 | merkelSiblings,
591 | blockHash,
592 | issueId);
593 |
594 |
595 |
596 | vault = await xclaim.getVaultFromId(vaultId);
597 |
598 | // previousBalance + (BTCredeemed * exchange to GWEI * exchange to WEI)
599 | let correctBalance = math.add(math.bignumber(prevUserBalance), math.bignumber(10));
600 | let userBalance = math.bignumber(await web3.eth.getBalance(accounts[1]));
601 | assert.equal(
602 | userBalance.value,
603 | correctBalance.value,
604 | "Balance of user decreases: " + (+prevUserBalance) + " == " + userBalance
605 | )
606 | })
607 |
608 |
609 |
610 | });
611 | function hexToBytes(hex) {
612 | for (var bytes = [], c = 0; c < hex.length; c += 2)
613 | bytes.push(parseInt(hex.substr(c, 2), 16));
614 | return bytes;
615 | }
616 |
617 | // Convert a byte array to a hex string
618 | function bytesToHex(bytes) {
619 | for (var hex = [], i = 0; i < bytes.length; i++) {
620 | var current = bytes[i] < 0 ? bytes[i] + 256 : bytes[i];
621 | hex.push((current >>> 4).toString(16));
622 | hex.push((current & 0xF).toString(16));
623 | }
624 | return hex.join("");
625 | }
626 |
627 | function sleep(milliseconds) {
628 | const date = Date.now();
629 | let currentDate = null;
630 | do {
631 | currentDate = Date.now();
632 | } while (currentDate - date < milliseconds);
633 | }
--------------------------------------------------------------------------------
/scripts/proof.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | HEIGHT=1760000
4 | INDEX=0
5 |
6 | HASH=$(bitcoin-cli -testnet getblockhash ${HEIGHT})
7 | TXID=$(bitcoin-cli -testnet getblock ${HASH} | jq ".tx[${INDEX}]")
8 |
9 | echo ${HASH}
10 |
11 | bitcoin-cli -testnet gettxoutproof "[${TXID}]" ${HASH}
--------------------------------------------------------------------------------
/scripts/regtest.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # bitcoind -regtest -daemon -rest -txindex
4 |
5 | ADDRESS1=$(bitcoin-cli -regtest getnewaddress "sender")
6 | ADDRESS2=$(bitcoin-cli -regtest getnewaddress "receiver")
7 |
8 | bitcoin-cli -regtest generatetoaddress 101 $ADDRESS1
9 |
10 | bitcoin-cli -regtest sendtoaddress $ADDRESS2 10
11 |
12 | bitcoin-cli -regtest generatetoaddress 10 $ADDRESS1
13 |
14 |
--------------------------------------------------------------------------------
/scripts/setup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | bitcoind -regtest
4 |
5 | BTC_ADDRESS=$(bitcoin-cli -regtest getnewaddress)
6 | bitcoin-cli -regtest generatetoaddress 101 ${BTC_ADDRESS}
7 | bitcoin-cli -regtest getbalance
--------------------------------------------------------------------------------
/scripts/testdata.py:
--------------------------------------------------------------------------------
1 | import subprocess
2 | from subprocess import CalledProcessError
3 | from os import path
4 | import json
5 |
6 | import bitcoin.rpc
7 | from bitcoin.core import *
8 | from bitcoin.core.script import *
9 | from bitcoin.wallet import *
10 |
11 | DIRNAME = path.dirname(__file__)
12 | FILENAME = path.join(DIRNAME, 'blocks.json')
13 | BLOCKS = 20
14 |
15 | bitcoin.SelectParams('regtest')
16 |
17 | # Using RawProxy to avoid unwanted conversions
18 | proxy = bitcoin.rpc.RawProxy()
19 |
20 | # Generates a number of blocks
21 | # @param number: number of blocks to generate
22 | # returns number of block hashes
23 | def generateEmptyBlocks(number):
24 | # setup address to generate blocks
25 |
26 | # generate first 101 blocks (maturation period + 1)
27 | return proxy.generatetoaddress(number, address_1)[0]
28 |
29 |
30 | def generateBlockWithTx(numberTx):
31 |
32 | for i in range(numberTx):
33 | proxy.sendtoaddress(address_2, 0.0001)
34 |
35 | return proxy.generatetoaddress(1, address_1)[0]
36 |
37 |
38 |
39 | # send coins
40 | # proxy.sendtoaddress(address2, 10)
41 |
42 | # blocks = []
43 | # for blockhash in list(blockhashes):
44 | # # print(blockhash)
45 | # blocks.append(proxy.getblockheader(blockhash))
46 |
47 |
48 | #
49 | # Exports the block headers to a JSON file
50 | # @param blockhashes:
51 | def exportBlocks(blockhashes):
52 | # height = proxy.getblockcount()
53 | blocks = []
54 | for blockhash in blockhashes:
55 | block = proxy.getblock(blockhash)
56 |
57 | # convert to hex as wanted by solc (only fields used in relay)
58 | block["hash"] = "0x" + block["hash"]
59 | block["merkleroot"] = "0x" + block["merkleroot"]
60 | block["chainwork"] = "0x" + block["chainwork"]
61 | txs = block["tx"]
62 | headerBytes = proxy.getblockheader(blockhash, False)
63 | block["header"] = "0x" + headerBytes
64 | # queries the bitcoin-rpc gettxoutproof method
65 | # https://chainquery.com/bitcoin-cli/gettxoutproof#help
66 | # returns a raw proof consisting of the Merkle block
67 | # https://bitcoin.org/en/developer-reference#merkleblock
68 | proofs = []
69 | for i in range(len(txs)):
70 | # print("TX_INDEX {}".format(i))
71 | try:
72 | tx_id = txs[i]
73 | # print("TX {}".format(tx_id))
74 | output = subprocess.run(["bitcoin-cli", "-regtest", "gettxoutproof", str(json.dumps([tx_id])), blockhash], stdout=subprocess.PIPE, check=True)
75 |
76 | proof = output.stdout.rstrip()
77 | # Proof is
78 | # 160 block header
79 | # 8 number of transactionSs
80 | # 2 no hashes
81 | number_hashes = int(proof[168:170], 16)
82 |
83 | merklePath = []
84 | for h in range(number_hashes):
85 | start = 170 + 64*h
86 | end = 170 + 64*(h+1)
87 | hash = proof[start:end]
88 | merklePath.append("0x" + hash.decode("utf-8"))
89 |
90 | # print(merklePath)
91 |
92 | block["tx"][i] = {"tx_id": "0x" + str(tx_id), "merklePath": merklePath, "tx_index": i}
93 |
94 | except CalledProcessError as e:
95 | print(e.stderr)
96 |
97 |
98 | blocks.append(block)
99 |
100 |
101 |
102 | with open(FILENAME, 'w', encoding='utf-8') as f:
103 | json.dump(blocks, f, ensure_ascii=False, indent=4)
104 |
105 | print("### Exported {} blocks to {} ###".format(len(blocks), FILENAME))
106 |
107 |
108 | # print("### Exported {} proofs to {} ###".format(len(txhashes),file))
109 |
110 | # BE<>LE conversion
111 | def flipBytes(b):
112 | byteSize = 2
113 | chunks = [ b[i:i+byteSize] for i in range(0, len(b), byteSize) ]
114 | reversed_chunks = chunks[::-1]
115 | return ('').join(reversed_chunks)
116 |
117 |
118 | if __name__ == "__main__":
119 |
120 | address_1 = proxy.getnewaddress()
121 | address_2 = proxy.getnewaddress()
122 |
123 | generateEmptyBlocks(2018)
124 |
125 | # first block is empty
126 | blockhashes = []
127 |
128 | blockhashes.append(generateEmptyBlocks(1))
129 | blockhashes.append(generateEmptyBlocks(1))
130 |
131 |
132 | # generate block with 1 TX
133 | blockhashes.append(generateBlockWithTx(1))
134 |
135 | # generate a block with a lot of TX
136 | blockhashes.append(generateBlockWithTx(50))
137 |
138 | # fill with blocks
139 | for i in range(BLOCKS):
140 | blockhashes.append(generateBlockWithTx(1))
141 |
142 |
143 | exportBlocks(blockhashes)
144 |
145 |
146 |
--------------------------------------------------------------------------------
/test/build.test.ts:
--------------------------------------------------------------------------------
1 | import {ethers} from 'hardhat';
2 | import chai from 'chai';
3 | import {solidity} from 'ethereum-waffle';
4 | import {genesis, generate} from '../scripts/builder';
5 | import {DeployTestRelay} from '../scripts/contracts';
6 |
7 | chai.use(solidity);
8 | const {expect} = chai;
9 |
10 | describe('Build', () => {
11 | it('should build and store headers', async () => {
12 | let block = genesis();
13 | const signers = await ethers.getSigners();
14 | const contract = await DeployTestRelay(signers[0], {
15 | header: '0x' + block.toHex(true),
16 | height: 1
17 | });
18 |
19 | block = generate(
20 | 'bcrt1qu96jmjrfgpdynvqvljgszzm9vtzp7czquzcu6q',
21 | block.getHash()
22 | );
23 | await contract.submitBlockHeader('0x' + block.toHex(true));
24 | let best = await contract.getBestBlock();
25 | expect(best.digest).to.eq(
26 | '0x9588627a4b509674b5ed7180cb2f9c8679fe5f1c8a6378069af0f2b8c2ff831f'
27 | );
28 |
29 | block = generate(
30 | 'bcrt1qu96jmjrfgpdynvqvljgszzm9vtzp7czquzcu6q',
31 | block.getHash()
32 | );
33 | await contract.submitBlockHeader('0x' + block.toHex(true));
34 | best = await contract.getBestBlock();
35 | expect(best.digest).to.eq(
36 | '0x7b02735fdcd34c70e65d1442949bd0a0fae69aedabfc05503f3ae5998a8f4348'
37 | );
38 | });
39 | });
40 |
--------------------------------------------------------------------------------
/test/constants.ts:
--------------------------------------------------------------------------------
1 | export const ErrorCode = {
2 | ERR_INVALID_HEADER_SIZE: 'Invalid block header size',
3 | ERR_DUPLICATE_BLOCK: 'Block already stored',
4 | ERR_PREVIOUS_BLOCK: 'Previous block hash not found',
5 | ERR_LOW_DIFFICULTY: 'Insufficient difficulty',
6 | ERR_DIFF_TARGET_HEADER: 'Incorrect difficulty target',
7 | ERR_DIFF_PERIOD: 'Invalid difficulty period',
8 | ERR_NOT_EXTENSION: 'Not extension of chain',
9 | ERR_BLOCK_NOT_FOUND: 'Block not found',
10 | ERR_CONFIRMS: 'Insufficient confirmations',
11 | ERR_VERIFY_TX: 'Incorrect merkle proof',
12 | ERR_INVALID_TXID: 'Invalid tx identifier'
13 | };
14 |
--------------------------------------------------------------------------------
/test/fork.test.ts:
--------------------------------------------------------------------------------
1 | import {ethers} from 'hardhat';
2 | import {Signer, Wallet} from 'ethers';
3 | import chai from 'chai';
4 | import {deployContract, solidity} from 'ethereum-waffle';
5 | import RelayArtifact from '../artifacts/contracts/Relay.sol/Relay.json';
6 | import {Relay} from '../typechain/Relay';
7 |
8 | chai.use(solidity);
9 | const {expect} = chai;
10 |
11 | async function getBestBlockDigest(relay: Relay): Promise {
12 | const {digest} = await relay.getBestBlock();
13 | return digest;
14 | }
15 |
16 | describe('Forking', () => {
17 | let signers: Signer[];
18 | let relay: Relay;
19 |
20 | beforeEach(async () => {
21 | signers = await ethers.getSigners();
22 | const genesis =
23 | '0x00000020db62962b5989325f30f357762ae456b2ec340432278e14000000000000000000d1dd4e30908c361dfeabfb1e560281c1a270bde3c8719dbda7c848005317594440bf615c886f2e17bd6b082d';
24 | relay = (await deployContract(signers[0] as Wallet, RelayArtifact, [
25 | genesis,
26 | 562621
27 | ])) as Relay;
28 | });
29 |
30 | it('should reorg to longer chain', async () => {
31 | // submit main chain
32 | await relay.submitBlockHeader(
33 | '0x000000204615614beedb06491a82e78b38eb6650e29116cc9cce21000000000000000000b034884fc285ff1acc861af67be0d87f5a610daa459d75a58503a01febcc287a34c0615c886f2e17046e7325'
34 | ); // 562622
35 | await relay.submitBlockHeader(
36 | '0x00000020b8b580a399f4b15078b28c0d0bba705a6894833b8a490f000000000000000000b16c32aa36d3b70749e7febbb9e733321530cc9a390ccb62dfb78e3955859d4c44c0615c886f2e1744ea7cc4'
37 | ); // 562623
38 | await relay.submitBlockHeader(
39 | '0x00000020f549a985f656ab3416afa5734917d8bb7339829e536620000000000000000000af9c9fe22494c39cf382b5c8dcef91f079ad84cb9838387aaa17948fbf25753430c2615c886f2e170a654758'
40 | ); // 562624
41 | await relay.submitBlockHeader(
42 | '0x00e0ff3f8bfff9ae28af2aa90adfdb92b218829727886488dcc914000000000000000000e78265c12495ef469b3e85aa570667fdcb5bf534304fcbe4621c706d0e7ca8149dc3615c886f2e171c3fa2b8'
43 | ); // 562625
44 | await relay.submitBlockHeader(
45 | '0x00000020fee20039fe494f7408c090e03970ec9b132366066f380d0000000000000000006f510b84a156d42cb64f30b97a7fc6dd030c9b77e19bbcd850c9f3c69bc533dacec3615c886f2e170a1a921d'
46 | ); // 562626
47 | await relay.submitBlockHeader(
48 | '0x00000020d2406bb15e4f917104d2b2d9320454b947fb08a723301d0000000000000000006aa31402bb974ebcd45eb2b0be7df8cc48ea9721493978e8715ce93b55d5ea20d1c5615c886f2e1767973d73'
49 | ); // 562627
50 | await relay.submitBlockHeader(
51 | '0x00e0002096f25d34d30a1bf4e280d6bb38b228e8993c8b28222d2b0000000000000000002488333a3bab6cc72d04ee1523ae83c9559938bef8521cb624c9641bb58cabe953ce615c886f2e172e0885ce'
52 | ); // 562628
53 | await relay.submitBlockHeader(
54 | '0x00000020b86918706260609b3b6aaf684aed961eac6b015e637024000000000000000000e72e0a6fd324d36edee39f9336c56f129f1ef7c2ec26d0dfb01e4520fb7a8d7fcdce615c886f2e17415cdb46'
55 | ); // 562629
56 |
57 | const oldBestBlock =
58 | '0x5204b3afd5c0dc010de8eeb28925b97d4b38a16b1d020f000000000000000000';
59 |
60 | // submit orphan
61 | await relay.submitBlockHeader(
62 | '0x0000802020ffe4f7a005faa83610adf7e7a52ff5700c222b9b5f0500000000000000000058c8af6bf8e8e00c3d6ff512b2133533ebdcde164de306e6b65e53157fc22b53e7d3615c886f2e17ed205930'
63 | ); // 562630
64 | expect(await getBestBlockDigest(relay)).to.eq(oldBestBlock);
65 |
66 | // submit fork
67 | await relay.submitBlockHeader(
68 | '0x0000002020ffe4f7a005faa83610adf7e7a52ff5700c222b9b5f050000000000000000009d1479517fda612a10a279b2339952bbdba8fe47c8fec644921d146ec79482ecebd3615c886f2e179234c38e'
69 | ); // 562630
70 | expect(await getBestBlockDigest(relay)).to.eq(oldBestBlock);
71 |
72 | // extend fork
73 | await relay.submitBlockHeader(
74 | '0x0000002051214b0c42383a1ea7bf28f20062f81d7b72497cb1030a00000000000000000000af444756eb5313dae6cb8dc7b4e00ae7d79cfa67a85b4b486a9583896ab3314bd8615c886f2e17f152bc1f'
75 | ); // 562631
76 | expect(await getBestBlockDigest(relay)).to.eq(oldBestBlock);
77 | await relay.submitBlockHeader(
78 | '0x00e00020bf01515ce1f4f971b9805205373093c200b2bf92d56408000000000000000000b2c2fcb555d6e2d677bb9919cc2d9660c81879225d15f53f679e3fdbfad129d032db615c886f2e17155f22df'
79 | ); // 562632
80 | expect(await getBestBlockDigest(relay)).to.eq(oldBestBlock);
81 | await relay.submitBlockHeader(
82 | '0x00000020a4c292016c1585e6f81986f7e216c79d28b15b1d513a100000000000000000004e33f75c5f63371d4a05e7ab93afb7c1caa22d2f9d4fce61e194ab8ffe741f35c0de615c886f2e17032ddf4b'
83 | ); // 562633
84 | expect(await getBestBlockDigest(relay)).to.eq(oldBestBlock);
85 | await relay.submitBlockHeader(
86 | '0x00000020577d11b45f90733748343b5add65dbe88216fa3027cc20000000000000000000ef200d52b09ae1902d62476f09405e21fa81cd7972ccfeaf37b47b00ef4e2180cdde615c886f2e171f2b5f80'
87 | ); // 562634
88 | expect(await getBestBlockDigest(relay)).to.eq(oldBestBlock);
89 | await relay.submitBlockHeader(
90 | '0x00000020f5f2d6840112ff9281bd88f445f135dfb41a64cebeb725000000000000000000a91738fc7b8628e70906164624f80ce54bafe26fe0ef8678f569b0b64e83589cb3e0615c886f2e17c624e58c'
91 | ); // 562635
92 | expect(await getBestBlockDigest(relay)).to.eq(oldBestBlock);
93 |
94 | // overtake stable confirmations
95 | const newBestBlock =
96 | '0x0ed18ffcb751e45471dddab23d34538869d3b2cdd48428000000000000000000';
97 | await relay.submitBlockHeader(
98 | '0x00e00020f98794c5b71e25f07eb2a31ab31b2e2487e0859abec000000000000000000000c29b14f0fe90ac2173197665d460df45c37ccf0c873276f59d095cbed4bcc7c2fae4615c886f2e178a505d9d'
99 | ); // 562636
100 | expect(await getBestBlockDigest(relay)).to.eq(newBestBlock);
101 |
102 | const filter = relay.filters.ChainReorg(oldBestBlock, newBestBlock, 1);
103 | await new Promise((resolve) => {
104 | relay.once(filter, () => {
105 | // event emitted
106 | resolve();
107 | });
108 | });
109 | });
110 | });
111 |
--------------------------------------------------------------------------------
/test/gas.test.ts:
--------------------------------------------------------------------------------
1 | import {ethers} from 'hardhat';
2 | import {Signer, Wallet} from 'ethers';
3 | import chai from 'chai';
4 | import {deployContract, solidity} from 'ethereum-waffle';
5 | import RelayArtifact from '../artifacts/contracts/Relay.sol/Relay.json';
6 | import {Relay} from '../typechain/Relay';
7 |
8 | chai.use(solidity);
9 | const {expect} = chai;
10 |
11 | describe('Gas', () => {
12 | let signers: Signer[];
13 | let relay: Relay;
14 |
15 | const genesisHeader =
16 | '0x00000020db62962b5989325f30f357762ae456b2ec340432278e14000000000000000000d1dd4e30908c361dfeabfb1e560281c1a270bde3c8719dbda7c848005317594440bf615c886f2e17bd6b082d';
17 | // const genesisHash =
18 | // '0x4615614beedb06491a82e78b38eb6650e29116cc9cce21000000000000000000';
19 | const genesisHeight = 562621;
20 |
21 | // 562622
22 | const header1 =
23 | '0x000000204615614beedb06491a82e78b38eb6650e29116cc9cce21000000000000000000b034884fc285ff1acc861af67be0d87f5a610daa459d75a58503a01febcc287a34c0615c886f2e17046e7325';
24 |
25 | it('should cost less than amount', async () => {
26 | signers = await ethers.getSigners();
27 | relay = (await deployContract(signers[0] as Wallet, RelayArtifact, [
28 | genesisHeader,
29 | genesisHeight
30 | ])) as Relay;
31 | const deployCost = (
32 | await relay.deployTransaction.wait(1)
33 | ).gasUsed?.toNumber();
34 | // console.log(`Deploy: ${deployCost}`);
35 | expect(deployCost).to.be.lt(3_000_000);
36 |
37 | const result = await relay.submitBlockHeader(header1);
38 | const updateCost = (await result.wait(1)).gasUsed?.toNumber();
39 | // console.log(`Update: ${updateCost}`);
40 | expect(updateCost).to.be.lt(120_000);
41 | });
42 | });
43 |
--------------------------------------------------------------------------------
/test/parser.test.ts:
--------------------------------------------------------------------------------
1 | import {ethers} from 'hardhat';
2 | import {Signer, Wallet} from 'ethers';
3 | import chai from 'chai';
4 | import {deployContract, solidity} from 'ethereum-waffle';
5 | import Artifact from '../artifacts/contracts/ParserDelegate.sol/ParserDelegate.json';
6 | import {ParserDelegate} from '../typechain/ParserDelegate';
7 |
8 | chai.use(solidity);
9 | const {expect} = chai;
10 |
11 | function btcToSat(btc: number): number {
12 | return btc * Math.pow(10, 8);
13 | }
14 |
15 | describe('Parser', () => {
16 | let signers: Signer[];
17 | let parser: ParserDelegate;
18 |
19 | beforeEach(async () => {
20 | signers = await ethers.getSigners();
21 | parser = (await deployContract(
22 | signers[0] as Wallet,
23 | Artifact,
24 | []
25 | )) as ParserDelegate;
26 | });
27 |
28 | const rawTx1 =
29 | '0x0200000000010111b6e0460bb810b05744f8d38262f95fbab02b168b070598a6f31fad' +
30 | '438fced4000000001716001427c106013c0042da165c082b3870c31fb3ab4683feffffff' +
31 | '0200ca9a3b0000000017a914d8b6fcc85a383261df05423ddf068a8987bf0287873067a3' +
32 | 'fa0100000017a914d5df0b9ca6c0e1ba60a9ff29359d2600d9c6659d870247304402203b' +
33 | '85cb05b43cc68df72e2e54c6cb508aa324a5de0c53f1bbfe997cbd7509774d022041e1b1' +
34 | '823bdaddcd6581d7cde6e6a4c4dbef483e42e59e04dbacbaf537c3e3e8012103fbbdb3b3' +
35 | 'fc3abbbd983b20a557445fb041d6f21cc5977d2121971cb1ce5298978c000000';
36 |
37 | it('should parse first output value', async () => {
38 | const result = await parser.extractOutputValueAtIndex(rawTx1, 0);
39 | expect(result.toNumber()).to.eq(1000000000);
40 | });
41 |
42 | it('should parse first output script', async () => {
43 | const result = await parser.extractOutputScriptAtIndex(rawTx1, 0);
44 | expect(result).to.eq('0xa914d8b6fcc85a383261df05423ddf068a8987bf028787');
45 | });
46 |
47 | const rawTx2 =
48 | '0x02000000000101b3cc334a386bd6318934ebff35914b9276835ccb1b9d85400de90613bc34e9430b00000000fdffffff02002d3101000000001600145587090c3288b46df8cc928c6910a8c1bbea508f94839d02000000001600145d0dd54e7d3457478e54ff5921efc1f099c17e0d0247304402201ed83e8bef81f5770f796b91e3db1741b3248040183905acb6abf2003ff062d9022050f22fcc0af72a91a358346b579078e535b4512587d876ad91f84b6849ac23ca0121025b5424e82f8c4313f6ec880724aab8cd78ed059b3115fd5075b63b3c3524134b02ab1a00';
49 |
50 | it('should parse and verify', async () => {
51 | const result = await parser.extractOutputValueAtIndex(rawTx2, 0);
52 | expect(result.toNumber()).to.eq(btcToSat(0.2));
53 | });
54 |
55 | // d9c9213136854a53211f1c80d202b743dfe971867558fd2c5628fe781a7f7ba9
56 | const p2pkhTx =
57 | '0x0200000001f76fec5260faa8f39fbd8f17f5acb2bd50260fa715347201657fceaefc14a102' +
58 | '000000006a47304402203f09be3d47d77f6a0948023aa80dc849128ce5a9cb017ed3c2413abb' +
59 | '74accf9c022019da8fed912a6b5b01aa6088fee3bdeb0d237d37072e29fb7b238932bf140cd0' +
60 | '012103785122f4493e03a7082398099e8f159a293ba496344c1c9b673074b1318ee336feffff' +
61 | 'ff02acfad806000000001976a914679775af720fa9bf3602150ee699ad7e2a24d96888ac4e90' +
62 | 'b76e200000001976a914e5ea7e9aae7df252796864912f0df41b4b956f4488ace3c01300';
63 |
64 | it('should successfully extract p2pkh output', async () => {
65 | const result = await parser.extractOutputValueAtIndex(p2pkhTx, 1);
66 | expect(result.toNumber()).to.eq(139296477262);
67 | });
68 |
69 | const p2wpkhTx =
70 | '0x01000000000101747c038ceb0a5ab9edd61d39f3d3c611cd52ccd0f519d9ad93ccf1f81a0f' +
71 | 'c5d30100000000ffffffff026400000000000000160014867a55207369ad0fe47cf3cfd2ecba' +
72 | 'ef2446c8b40000000000000000226a2000000000000000000000000000000000000000000000' +
73 | '0000000000000000000002473044022049a2913d41700d3076cec041d1d0906fe59c6d384c4a' +
74 | 'c4e67dc2b9bdc121f300022045a0c6edb241c39b2578bc9c2cb60e60821f18e273da799010bc' +
75 | 'c2a47c3d7e4e01210290465bd783baaa9d52df3b57e31cef7df72c0cbd10afe6a10e3cfbd947' +
76 | '7c8acf00000000';
77 |
78 | it('should successfully extract p2wpkh output', async () => {
79 | const result = await parser.extractOutputScriptAtIndex(p2wpkhTx, 1);
80 | console.log(result);
81 | });
82 | });
83 |
--------------------------------------------------------------------------------
/test/proof.test.ts:
--------------------------------------------------------------------------------
1 | import {ethers} from 'hardhat';
2 | import {Signer} from 'ethers';
3 | import chai from 'chai';
4 | import {solidity} from 'ethereum-waffle';
5 | import {Relay} from '../typechain/Relay';
6 | import {RelayFactory} from '../typechain/RelayFactory';
7 | import {ErrorCode} from './constants';
8 | import {BytesLike} from 'ethers';
9 |
10 | chai.use(solidity);
11 | const {expect} = chai;
12 |
13 | async function getBestBlockHeight(relay: Relay): Promise {
14 | const {height} = await relay.getBestBlock();
15 | return height;
16 | }
17 |
18 | function deploy(
19 | signer: Signer,
20 | header: BytesLike,
21 | height: number
22 | ): Promise {
23 | const factory = new RelayFactory(signer);
24 | return factory.deploy(header, height);
25 | }
26 |
27 | describe('Proofs', () => {
28 | let signers: Signer[];
29 | let relay: Relay;
30 |
31 | // 592920
32 | const genesisHeader =
33 | '0x0000c020c238b601308b7297346ab2ed59942d7d7ecea8d23a1001000000000000000000b61ac92842abc82aa93644b190fc18ad46c6738337e78bc0c69ab21c5d5ee2ddd6376d5d3e211a17d8706a84';
34 |
35 | beforeEach(async () => {
36 | signers = await ethers.getSigners();
37 | relay = await deploy(signers[0], genesisHeader, 5);
38 | });
39 |
40 | const tx = {
41 | version: '0x01000000',
42 | vin:
43 | '0x0101748906a5c7064550a594c4683ffc6d1ee25292b638c4328bb66403cfceb58a000000006a4730440220364301a77ee7ae34fa71768941a2aad5bd1fa8d3e30d4ce6424d8752e83f2c1b02203c9f8aafced701f59ffb7c151ff2523f3ed1586d29b674efb489e803e9bf93050121029b3008c0fa147fd9db5146e42b27eb0a77389497713d3aad083313d1b1b05ec0ffffffff',
44 | vout:
45 | '0x0316312f00000000001976a91400cc8d95d6835252e0d95eb03b11691a21a7bac588ac220200000000000017a914e5034b9de4881d62480a2df81032ef0299dcdc32870000000000000000166a146f6d6e69000000000000001f0000000315e17900',
46 | locktime: '0x00000000',
47 | txId: '0x5176f6b03b8bc29f4deafbb7384b673debde6ae712deab93f3b0c91fdcd6d674',
48 | index: 26,
49 | intermediateNodes:
50 | '0x8d7a6d53ce27f79802631f1aae5f172c43d128b210ab4962d488c81c96136cfb75c95def872e878839bd93b42c04eb44da44c401a2d580ca343c3262e9c0a2819ed4bbfb9ea620280b31433f43b2512a893873b8c8c679f61e1a926c0ec80bcfc6225a15d72fbd1116f78b14663d8518236b02e765bf0a746a6a08840c122a02afa4df3ab6b9197a20f00495a404ee8e07da2b7554e94609e9ee1d5da0fb7857ea0332072568d0d53a9aedf851892580504a7fcabfbdde076242eb7f4e5f218a14d2a3f357d950b4f6a1dcf93f7c19c44d0fc122d00afa297b9503c1a6ad24cf36cb5f2835bcf490371db2e96047813a24176c3d3416f84b7ddfb7d8c915eb0c5ce7de089b5d9e700ecd12e09163f173b70bb4c9af33051b466b1f55abd66f3121216ad0ad9dfa898535e1d5e51dd07bd0a73d584daace7902f20ece4ba4f4f241c80cb31eda88a244a3c68d0f157c1049b4153d7addd6548aca0885acafbf98a1f8345c89914c24729ad095c7a0b9acd20232ccd90dbd359468fcc4eee7b67d'
51 | };
52 |
53 | it('should fail with insufficient confirmations', async () => {
54 | // relay = await deploy(signers[0], genesisHeader, 5);
55 | expect(await getBestBlockHeight(relay)).to.eq(5);
56 |
57 | // checks default stable confirmations
58 | let result = relay.verifyTx(
59 | 0,
60 | tx.index,
61 | tx.txId,
62 | genesisHeader,
63 | tx.intermediateNodes,
64 | 0,
65 | false
66 | );
67 | expect(result).to.be.revertedWith(ErrorCode.ERR_CONFIRMS);
68 |
69 | // checks custom period
70 | result = relay.verifyTx(
71 | 0,
72 | tx.index,
73 | tx.txId,
74 | genesisHeader,
75 | tx.intermediateNodes,
76 | 100,
77 | true
78 | );
79 | expect(result).to.be.revertedWith(ErrorCode.ERR_CONFIRMS);
80 | });
81 |
82 | it('should fail with block not found', async () => {
83 | relay = await deploy(signers[0], genesisHeader, 100);
84 | expect(await getBestBlockHeight(relay)).to.eq(100);
85 |
86 | // checks default stable confirmations
87 | const result = relay.verifyTx(
88 | 0,
89 | tx.index,
90 | tx.txId,
91 | genesisHeader,
92 | tx.intermediateNodes,
93 | 0,
94 | false
95 | );
96 | expect(result).to.be.revertedWith(ErrorCode.ERR_BLOCK_NOT_FOUND);
97 | });
98 |
99 | it('should validate inclusion', async () => {
100 | relay = await deploy(signers[0], genesisHeader, 1);
101 | expect(await getBestBlockHeight(relay)).to.eq(1);
102 |
103 | await relay.verifyTx(
104 | 1,
105 | tx.index,
106 | tx.txId,
107 | genesisHeader,
108 | tx.intermediateNodes,
109 | 0,
110 | true
111 | );
112 | });
113 |
114 | it('should validate empty block coinbase', async () => {
115 | const header =
116 | '0x000000208eac45cbfb6620c53cbe8d20dd637f5e305118fa798f337d92210300000000009f3bf29af9132c4df7edb1d6f1f6306da12a28295d0918d2e424d27418b74d821aa9dc5ef0ff0f1ba5938e48';
117 | const height = 1760000;
118 |
119 | relay = await deploy(signers[0], header, height);
120 | expect(await getBestBlockHeight(relay)).to.eq(height);
121 |
122 | const txId = Buffer.from(
123 | '824db71874d224e4d218095d29282aa16d30f6f1d6b1edf74d2c13f99af23b9f',
124 | 'hex'
125 | ).reverse();
126 |
127 | await relay.verifyTx(height, 0, txId, header, [], 0, true);
128 | });
129 |
130 | const testnet1 = {
131 | txId: 'e50f29833077b92270a011594c87d8e2b02f80c4cf010adf4006587877381808',
132 | index: 64,
133 | intermediateNodes: [
134 | '4bcd5ffb40a136e53357f0bc575ccd22f3383d61c7d30b520a98d5bc2bde2f0a',
135 | '8b5864ec60f6c05ef25fbe0abf530964e7f06de5869ff0a6442f7f303a09552f',
136 | '57a0e382816bf2393cadde9afe8abc5c3cd7224df6cb1b7b7829b3470d1c10c4',
137 | '9bdd0449644b5785872b001813d9f5e468e903f756a7ff0cafdd39cdbb207afe',
138 | 'f9e56e0cbfd8f769dbd4b9dc82441ba82ac70c1254a864d31db391403cfb5b76',
139 | 'ee1db72e0346d9efbc52ef0cc20664d3174d9bbaa52b579f56797a868492fcb8',
140 | '87a266e624908d7fe6c8ec3ba35a34f4e5b3e577e29d5404f6c141646876ac3e'
141 | ],
142 | header:
143 | '0x00000020c29de51a684e5219b6cfe25f6999a31e05b5626a33478a8e6ed4869d000000005125f5cb99ebbfc5d2397d8282bd849aee26f984111230c8c42ee25dbd361e944d20d95effff001d97bebd41',
144 | headerHash:
145 | '0x000000000ee8388b0c4b935dec68824af4ad284dda2065d3eab526f207b17d8c',
146 | height: 1747715
147 | };
148 |
149 | it('should validate inclusion (testnet1)', async () => {
150 | relay = await deploy(signers[0], testnet1.header, testnet1.height);
151 | expect(await getBestBlockHeight(relay)).to.eq(testnet1.height);
152 |
153 | const proof = testnet1.intermediateNodes
154 | .map((value) => Buffer.from(value, 'hex').reverse().toString('hex'))
155 | .join('');
156 | const txid = Buffer.from(testnet1.txId, 'hex').reverse().toString('hex');
157 |
158 | await relay.verifyTx(
159 | testnet1.height,
160 | testnet1.index,
161 | '0x' + txid,
162 | testnet1.header,
163 | '0x' + proof,
164 | 0,
165 | true
166 | );
167 | });
168 |
169 | const testnet2 = {
170 | txId: '73582c8fddd60c0778bb9d449231d732f797c7d1e8197a62135b792291c9a12a',
171 | index: 26,
172 | intermediateNodes: [
173 | '12e3bce112e5744a17d3b28c675dab949921793ded7d9faaca3acc9f5778aa80',
174 | 'a6fd121f036eb80f9af9e8c67b4ace9dd7b8eb4e7846a3f252ded91d0c189d76',
175 | 'eebe422586731163966ebdff7f7a7c79602eb1d501786d03fffbbf4b55004c3c',
176 | '82e7be4cbc28e6c6e7624b6bd84bab794fb8fccfbfad3416b46f6172798ced5d',
177 | '49c30a3753ec196af0931d9e4e4269f8d0ff09815f848026d5e0fa8b8cb242ef',
178 | '33824ede7279221f779a0dc899028c5fff694376bb73bb0195985e2fd2431de9',
179 | '03857fe3d3bb2e412e309c616f89ae2d7a4219adaa608f0480ae4ca4391046a9'
180 | ],
181 | header:
182 | '0x00e0ff2ffb2b253e060112bed62614fcf9ae61f82c08d9e6c36a1b53070000000000000003b91283ae15b75dc2023735356f1412411d07ece715f3ed0e612ab4349b7db83030eb5ef2a5011add0c8e82',
183 | headerHash:
184 | '0x00000000000000f6cf3b73d47abb326b4a60e23ce24e02396e9edfe5c95622f5',
185 | height: 1772070
186 | };
187 |
188 | it('should validate inclusion (testnet2)', async () => {
189 | relay = await deploy(signers[0], testnet2.header, testnet2.height);
190 | expect(await getBestBlockHeight(relay)).to.eq(testnet2.height);
191 |
192 | const proof = testnet2.intermediateNodes
193 | .map((value) => Buffer.from(value, 'hex').reverse().toString('hex'))
194 | .join('');
195 | const txid = Buffer.from(testnet2.txId, 'hex').reverse().toString('hex');
196 |
197 | await relay.verifyTx(
198 | testnet2.height,
199 | testnet2.index,
200 | '0x' + txid,
201 | testnet2.header,
202 | '0x' + proof,
203 | 0,
204 | true
205 | );
206 | });
207 |
208 | const testnet3 = {
209 | txId: '71a127e01023ada604b85c05119e3ede49fe3e465159f5b905d394390a4be02b',
210 | index: 30,
211 | intermediateNodes: [
212 | '65604897c34192eb81daa8a711c7a11796ccb930b0805efa56abfb663c1699d6',
213 | '99dd48c5c7ebf148dafb9825db6d7e6a4c051faea96db1eb0e6fd42a9be7c71f',
214 | '188132f8725c964fe710857042f47c7bc57bd93672629d70859385fad2db33f9',
215 | 'e09ee89285475ab8afdd83efa5c9ede805abb4412227d3103d89169649a16894',
216 | 'f48041eb197669babf2991d39afea85b2b5cc78bc9bc2a69bdcf750626184511',
217 | '0c91009701d6665535149683b8bc7116ce7d5d794b31f4839331062808c0b4ed'
218 | ],
219 | header:
220 | '0x00e0ff27c57b44820f2629d53e7ed7f1cb147407b6b059b1d9c479414901000000000000b3d9797f1e11c29e5f955051b3ab9a20043777f6620c5ebe3a01955c25196f9349f6f15ef2a5011aa9b53fc8',
221 | headerHash:
222 | '0x00000000000000155b7e1e56cc21226981bb7bdf4c506f35c00aa01649321566',
223 | height: 1773152
224 | };
225 |
226 | it('should validate inclusion (testnet3)', async () => {
227 | relay = await deploy(signers[0], testnet3.header, testnet3.height);
228 | expect(await getBestBlockHeight(relay)).to.eq(testnet3.height);
229 |
230 | const proof = testnet3.intermediateNodes
231 | .map((value) => Buffer.from(value, 'hex').reverse().toString('hex'))
232 | .join('');
233 | const txid = Buffer.from(testnet3.txId, 'hex').reverse().toString('hex');
234 |
235 | await relay.verifyTx(
236 | testnet3.height,
237 | testnet3.index,
238 | '0x' + txid,
239 | testnet3.header,
240 | '0x' + proof,
241 | 0,
242 | true
243 | );
244 | });
245 |
246 | const mainnet1 = {
247 | txId: '9f0370848f7bbf67908808997661a320af4f0075dce313e2934a576ed8204059',
248 | index: 104,
249 | intermediateNodes: [
250 | '590c8ceb56383e61c7002d18e35c7ea0f6e6d8cc05221c791eaa90bc50e172af',
251 | '21bc92abd2b0d97bbe3b65a76d420209277d0e2bd3a9974ca7680bcbee115752',
252 | '6babdaf89052998ab048bc12db408bc3e2e30f70add044ed9a032a60db27ad98',
253 | 'c9be68e2dd903e2de000b9b2ca2c02544fbaf0c71c14da82f13aaf7fa1d2d8e8',
254 | 'a0ed0aa4629a69dc2c764e1395bdd7c6688336fccf8643a07b36be60dfd7a340',
255 | '46e621f7fa78f6d54123b3ef91b520fda523d35533b760990f178f1fd0fb00d6',
256 | '5660db59d56bbb416773d474bdd472ddea40c96b09a9e32b24017e59ab50e4f0',
257 | 'f8d7a8087256bae032c4576046b13af00033c2f335a55619e4b4e27a17cd8472'
258 | ],
259 | header:
260 | '00000020a1d2b3b757ad55fa1c670f9f59372a2a26f1a85197711e00000000000000000070be58da7ef0a754ef5947910fa1bb0c19160d641cf212ae46ba53e0327233bc2367905b2dd729174c3ae2db',
261 | headerHash:
262 | '00000000000000000021868c2cefc52a480d173c849412fe81c4e5ab806f94ab',
263 | height: 540107
264 | };
265 |
266 | it('should validate inclusion - large block (mainnet1)', async () => {
267 | relay = await deploy(signers[0], '0x' + mainnet1.header, mainnet1.height);
268 | expect(await getBestBlockHeight(relay)).to.eq(mainnet1.height);
269 |
270 | const proof = mainnet1.intermediateNodes
271 | .map((value) => Buffer.from(value, 'hex').reverse().toString('hex'))
272 | .join('');
273 | const txid = Buffer.from(mainnet1.txId, 'hex').reverse().toString('hex');
274 |
275 | await relay.verifyTx(
276 | mainnet1.height,
277 | mainnet1.index,
278 | '0x' + txid,
279 | '0x' + mainnet1.header,
280 | '0x' + proof,
281 | 0,
282 | true
283 | );
284 | });
285 | });
286 |
--------------------------------------------------------------------------------
/test/relay.test.ts:
--------------------------------------------------------------------------------
1 | import {ethers} from 'hardhat';
2 | import {Signer} from 'ethers';
3 | import chai from 'chai';
4 | import {solidity} from 'ethereum-waffle';
5 | import {Relay} from '../typechain/Relay';
6 | import {ErrorCode} from './constants';
7 | import {RelayFactory} from '../typechain/RelayFactory';
8 | import {BytesLike} from 'ethers';
9 | import * as bitcoin from 'bitcoinjs-lib';
10 |
11 | chai.use(solidity);
12 | const {expect} = chai;
13 |
14 | function deploy(
15 | signer: Signer,
16 | header: BytesLike,
17 | height: number
18 | ): Promise {
19 | const factory = new RelayFactory(signer);
20 | return factory.deploy(header, height);
21 | }
22 |
23 | describe('Relay', () => {
24 | let signers: Signer[];
25 | let relay: Relay;
26 |
27 | const genesisHeader =
28 | '0x00000020db62962b5989325f30f357762ae456b2ec340432278e14000000000000000000d1dd4e30908c361dfeabfb1e560281c1a270bde3c8719dbda7c848005317594440bf615c886f2e17bd6b082d';
29 | const genesisHash =
30 | '0x4615614beedb06491a82e78b38eb6650e29116cc9cce21000000000000000000';
31 | const genesisHeight = 562621;
32 |
33 | beforeEach(async () => {
34 | signers = await ethers.getSigners();
35 | relay = await deploy(signers[0], genesisHeader, genesisHeight);
36 | });
37 |
38 | it('should store genesis header', async () => {
39 | const filter = relay.filters.StoreHeader(genesisHash, genesisHeight);
40 | await new Promise((resolve) => {
41 | relay.once(filter, () => {
42 | // event emitted
43 | resolve();
44 | });
45 | });
46 |
47 | // check header was stored correctly
48 | const height = await relay.getBlockHeight(genesisHash);
49 | expect(height).to.eq(genesisHeight);
50 | // expect(header.merkle).to.eq("0xd1dd4e30908c361dfeabfb1e560281c1a270bde3c8719dbda7c8480053175944");
51 | });
52 |
53 | it('should fail with duplicate (genesis)', async () => {
54 | const result = relay.submitBlockHeader(genesisHeader);
55 | await expect(result).to.be.revertedWith(ErrorCode.ERR_DUPLICATE_BLOCK);
56 | });
57 |
58 | // 562622
59 | const header1 =
60 | '0x000000204615614beedb06491a82e78b38eb6650e29116cc9cce21000000000000000000b034884fc285ff1acc861af67be0d87f5a610daa459d75a58503a01febcc287a34c0615c886f2e17046e7325';
61 |
62 | it('should store and fail on resubmission', async () => {
63 | await relay.submitBlockHeader(header1);
64 | await expect(relay.submitBlockHeader(header1)).to.be.revertedWith(
65 | ErrorCode.ERR_DUPLICATE_BLOCK
66 | );
67 | });
68 |
69 | it('should fail with block header > 80', async () => {
70 | const result = relay.submitBlockHeader(header1 + '1234');
71 | await expect(result).to.be.revertedWith(ErrorCode.ERR_INVALID_HEADER_SIZE);
72 | });
73 |
74 | it('should fail with block header < 80', async () => {
75 | const result = relay.submitBlockHeader(header1.substring(0, 28));
76 | await expect(result).to.be.revertedWith(ErrorCode.ERR_INVALID_HEADER_SIZE);
77 | });
78 |
79 | // 562623
80 | const header2 =
81 | '0x00000020b8b580a399f4b15078b28c0d0bba705a6894833b8a490f000000000000000000b16c32aa36d3b70749e7febbb9e733321530cc9a390ccb62dfb78e3955859d4c44c0615c886f2e1744ea7cc4';
82 |
83 | it('should fail because prev block not stored', async () => {
84 | const result = relay.submitBlockHeader(header2);
85 | await expect(result).to.be.revertedWith(ErrorCode.ERR_PREVIOUS_BLOCK);
86 | });
87 |
88 | it('should fail with low difficulty', async () => {
89 | const fakeGenesis = {
90 | hash:
91 | '0x00000000000000000012af6694accf510ca4a979824f30f362d387821564ca93',
92 | height: 597613,
93 | merkleroot:
94 | '0x1c7b7ac77c221e1c0410eca20c002fa7b6467ba966d700868928dae4693b3b78',
95 | header:
96 | '0x00000020614db6ddb63ec3a51555336aed1fa4b86e8cc52e01900e000000000000000000783b3b69e4da28898600d766a97b46b6a72f000ca2ec10041c1e227cc77a7b1c6a43955d240f1617cb069aed'
97 | };
98 | const fakeBlock = {
99 | hash:
100 | '0x000000000000000000050db24a549b7b9dbbc9de1f44cd94e82cc6863b4f4fc0',
101 | height: 597614,
102 | merkleroot:
103 | '0xc090099a4b0b7245724be6c7d58a64e0bd7718866a5afa81aa3e63ffa8acd69d',
104 | header:
105 | '0x0000002093ca64158287d362f3304f8279a9a40c51cfac9466af120000000000000000009dd6aca8ff633eaa81fa5a6a861877bde0648ad5c7e64b7245720b4b9a0990c07745955d240f16171c168c88'
106 | };
107 | relay = await deploy(signers[0], fakeGenesis.header, fakeGenesis.height);
108 |
109 | const result = relay.submitBlockHeader(fakeBlock.header);
110 | await expect(result).to.be.revertedWith(ErrorCode.ERR_LOW_DIFFICULTY);
111 | });
112 | });
113 |
114 | describe('Relay (Batch)', () => {
115 | let signers: Signer[];
116 | let relay: Relay;
117 |
118 | // testnet: 1780500..1780600
119 | const headers = [
120 | '00000020626148e2b1dcddb0c1627b80828ad15b82938fa75c685b98c7499880000000009299e7d2ad1c49f128157f547a67bb77a62c98be495be5e6cd987437c4b721df83cf0a5f107e0e193122a605',
121 | '000000202853c5b4973950d61ce2f48285a6c29334d9e1925a18fc310200000000000000d7d2a2e673b2f50a711224802c590e48c0b7e44489339a14fbb121b8b70e998635d40a5fffff001d0e4a958c',
122 | '00000020d359a65227e5a2026ed62bb8b64b9c3d0dda958bbe4f32860a3b65f800000000709524e20ca410fa467cd9f2a02bee6ae408d49d8b1e2e2364034687b1f47ca8d7d40a5f107e0e1996a3fdee',
123 | '000000202f32b9c714efda810046a166b0311f75668a5010547d9f1f0100000000000000a7825bb600834a1e3f2aaf1594ab30d6455b435dad7774c9400acd6b89bfdc9989d90a5fffff001d2d3b289f',
124 | '00000020a302f19330cd52a2320f24b5e53e47a66191b6927f38e9f44b2f549600000000995c648f45f1a3f529612813324bbde342898e3819e773b2b4351e37d54a04183ade0a5fffff001d0b481eda',
125 | '00000020beb737685f6da4b10e88683557dc54af2855a9607643cf41cd552dc50000000033d847a094897a48c9200cf13c10d7e8ac28ce93887429987943f3ecd66343b7ebe20a5fffff001d0e9ec4e0',
126 | '00000020b1dfe807731a83f8b0f39b262648be8e20a194ee4e1297b2f00a7d7800000000d0dcdbb1a447538146ee68dd9111c295ab887e308e2735217a7f511c60e245909ee70a5fffff001d87f0f86b',
127 | '00000020b4181ecfa8c88fee87d6f2823cb3a04bb225900c8a31f3cbfc70840e000000009e6c71eb18ab0b39eabe5290c1d278025e83ffb9676ed531a6414af10807ad8750ec0a5fffff001d0a274262',
128 | '00000020644775410fc2066dfa8894a4fe9274a51be71e475852ae09ba3dd3f900000000e1041313aa7c2f2dad08aa78d9d0fd2f1aa4d55d2f09f3e5a7dcd678887efeeb01f10a5fffff001daf0ff7c4',
129 | '000000200742b49269f9c65cb9ab6d27f5c031d5d789f31f17b01ee588b6fb89000000009ef8c04d13b830d3ee8ec094c202eba1934c0b8c9cf22100e676893c50c6b5d5b3f50a5fffff001d81e243c5',
130 | '0000002074a9d851ad8118c8c92b9dd096eb6557fd5b5b80ba0d74a9f711fa65000000000d9f9c45bf1875e4fbea0382c5bbb8990332e1abf6397db651c0c03f221584e367fa0a5fffff001d2392b556',
131 | '0000002035ebf879e09ec830a2d963b02dcf4d0ff72279fda56ef4c34cc0afea000000001352033cecbfc1c97ae8e4aaed43eb39b93b65ebc11bdebee34026d0f063d40318ff0a5fffff001d22c4bc83',
132 | '00000020ca3e46d396130a17dc9c304c32e67f22288384d747423efeb5e918a700000000661c31ae745587bb08a400611d5738581d5be560ae1316428e1f38d138a7262cca030b5fffff001da2b64f51',
133 | '000000200852be052e3c0ef8aa2a9fcfd9021732d96baf35ee94647bd0c34e1800000000c6ff703fe4d1b6aa1b625a269077364d171378a719d48477a9a50c1314c4ca2a7d080b5fffff001d1e9e152a',
134 | '00000020837cf782bb2465122999c4d19fb5755aa1adb266ee552054da75242a00000000f3fc1b282e083fc33b94e52a90bacb64fe8ffb5ffd7d8c095560eaa91097c5902f0d0b5fffff001d0c81bd0a',
135 | '0000002051a8ed7d4177104d9eafa18f379dbd4e0886652332377aecabd1032700000000566b967563bddf5b696f3ebf969baa9282309ddebe8a457abf125393ea0fc29a510e0b5f107e0e193964f2c3',
136 | '00000020bdf2de83e95a397b287017a9a0decf8c6069d6d6e922e0610400000000000000302bc0bb9de04245ddab6a2064d1e978a767a3dc5aee0e0fd1b4b5af4a1bf2ea5d100b5f107e0e192556c6e0',
137 | '00e0ff2790ac5cf4b63eb808e3f021ffee83c6637d507c1c93a4d2c109000000000000004c4821628b002b17749a01bf3f79cc7658adf52949e44964f48117e26b67c25f73140b5f107e0e1914066404',
138 | '00000020c0852219eab15c561f238a6a8b2242b18c4817a0e00602bc0400000000000000602f385b29fb7f9b40ae5e15ad59e80202629dcf6207fe106303543c4c51df2b24190b5fffff001da610ef06',
139 | '00000020608404966fcd3b2f97b155afb04d26389b2b529623c483e4add06c6700000000e7fae80dc7e40532795f4634a1d083d0f5b7468cf9080627ff807101566dfef2d61d0b5fffff001d2db9e521',
140 | '000000202ebaae5122118aa3fdcec0e0ba61b99dc65884a4bf6dcaa7afd8fa6900000000bde3eb5f01d9718ac86a9a01780582d12b39a241ffee7ff01b2436ceed29db1d88220b5fffff001dab46279b',
141 | '000000207cc4b9e89fa603317abb78ae94d1cc7487b58940fa46e368694f823200000000324a285cd7f0e9961427caf1be22414e4293b511f61ce3f48c289e2c5812c1793b270b5fffff001d301395e8',
142 | '00000020ef46ec28a09b248fbe65e440174296c130de267ccfdba77e0177165f000000008df6695bb4f238b1b7ef07c56e029aac2edef970687d78096a4a4bd5f596cbdf25290b5f107e0e19bdb7861c',
143 | '0000002007c0291b890f76b9342782ab851c06e42d4383c1acfe1a9c0700000000000000a9aa3687f31ae057d4220ef65998ca6e4fe93f242a4d28f7fd6122939f2d5a84d72d0b5fffff001da539cb60',
144 | '00000020316d46ef8509c6afc518cfcca13238065985fc2777af20876fd1d7360000000061c8d68b2ffe8c01921fc8a32334bbc08d9157873ab1c4da2ae3c16c10cfaf5f8a320b5fffff001d0100e049',
145 | '000000203850ac2a138c71242c033b48203a191278825f840ce231c3df8eb4610000000048b721b6efe9ff9da40c6c0a677003883564f15be50426870f0a1b3ce0bc9a0e3e370b5fffff001d9045be51',
146 | '00000020fcfed939f6bea39df7c9eb20cfe307d2d94b71df1c12d0de1154edf600000000501c74196849d90f75fcee1ec892cc5ea68741252be2bf3772b3d00c1bd3279bfd390b5f107e0e1968dad059',
147 | '0000002038bf1ecfe5f83b4c6666d803228811903697248e44f8b4400a0000000000000063ad01ed7e62f86b383d20bd2973eaeec1834139ee05ffc7cd3612631091a28dae3e0b5fffff001dbc697593',
148 | '00000020468d51b568188fd39a9244c65ff06201b924b18ebd50b96d587c7a8500000000e7159432b1a23222966f2072e51dd8817dc636c60fdb25faa025bdde3e596f3461430b5fffff001d9885f636',
149 | '00e0ff37a57253f40008e881519f5f617434203b1008b562f7d23e0799c316cd00000000c30eed58ca067252a08ed3e46c0cc3ea9ebd243ed1391154d15a76215b60a4adf7430b5f107e0e19d92a2dc8',
150 | '00000020a110ab26e1c4bc8908559cf5ea0ce45f73ea96c1e8ba1b26090000000000000009eb9ddd4189d17c69d3b80cbc8c7739c508609be4f0d561c00bfef3041a2c70a9480b5fffff001d025c8dac',
151 | '00000020399ae4aa1fb27f35f2231e12c6d8249dba076d6a29f90108f8d2b2de00000000cdb7ad2f3cd48047d3f257a2716be46c448aa7c2c8670c8c56e7ec5488a006ff5a4d0b5fffff001dbb70351f',
152 | '0000002054e411ba52631b1c092cf8c0c0284dbfb2faf26d961b7aa179ba4672000000000f5096808a003d6fe6773170a670946eda9930bfa28be0102595e8c37d8cdd4418500b5f107e0e19ec4b68ab',
153 | '00e0ff2f84f74ed59157973e91896ab3b56785b0ffb408e16b3c900a0500000000000000a031b66e38089ac690aa29caf3492d9c914a2b6e635bad3afc83d0d2137048d827510b5f107e0e199425776e',
154 | '00000020cb9cdef440ae0e1708156c510cd05de687479eabb863ae8a030000000000000011043ac4d163b8b10df4167132322a3e25151e51f3a0d2e14f818038c8ba746cd9550b5fffff001da4d83e65',
155 | '000000207d633ca954e1c2f8eafd3ae6dcd7fde0c6784b28e6838e0b88ce872d000000004ee638145f36da7de43829400dc880969121f62f7d4915a11ef6f584f7450e0c8c5a0b5fffff001d838d1798',
156 | '000000207f80c35c01c8cb114682df93771575435dfcc43c2325e0f71d4be08400000000fcb7c7df4d4aeabde13e152cbf3227b4788f7e96f8239581ade4a052fd5028b73e5f0b5fffff001d892faae1',
157 | '000000202b3ead2a8dd4a297035a7de2ea31ab861f2cff7a388c076bbee980810000000049d6e32d26daa1d71bdaf54a79215e08b4f05c601b26eff6aec8bc7278eb1f23f55f0b5f107e0e19a2323a0d',
158 | '00000020c2e48f660f6547ce07ad83e10b4fde8564c64a65f3f410bc0d00000000000000e17d188d112ac1651b27710e4832b3ded1ed2a48722be0b46d66de096dc2bae486600b5f107e0e19bdb69143',
159 | '000000203ec4b6596b8cd7204e820c9c10afae0925767a78c20621c90b000000000000006fa53717c2bef3d8cc5acfbebce34a5fa09d43373a8f106d289717840331bbcb5f630b5f107e0e1940200a27',
160 | '00000020830dcd38de9a482a74b8ff203c20322141d7af4dd4daf1960800000000000000b5b441cc945363f8ee15e0e1192a34de6c5dbcc73a8ca879496f8bcec4a32ef7a9650b5f107e0e199a1580ab',
161 | '00000020764ff65a216b965df52b95bd12217fc46026a6dc3d2a997b0a00000000000000eca755b9d209c3acf35187ff55e6da617aab3cb92ffe3fccc6deffbe1a897d875a6a0b5fffff001d9fb20da1',
162 | '000000207a5b2541d37604f75bf754c3efaf03be2552f7d60e3a0041a0d48cc4000000008fcbf7f06c20c2064328cb547f20c61b9bc25fdbfdc266ef6de460631ff13494cb6c0b5f107e0e195a8ba5ee',
163 | '00000020a7c3212f717c97159f5ae503b91a2ca76b0784df9a85c6b209000000000000003d2a1095f5afb0ea17c2760f09bb636d83712c81e9a9e5ccef1def040e8c6af87c710b5fffff001d0d8fb5ae',
164 | '0000002096b857513eeb58bb7d091cba750a40e9ee53d22dc5f34f506d14bbdf000000000fdfaa43bd841dd9ab59ca08181b2085790b21b9483b456130d28a1cf977a2ce2d760b5fffff001d9a6342d4',
165 | '00000020c3071c77db7ecfaa6217d522470aa2a743c27cbbb59846d236647a28000000005d29222006807dcbc6e1fb6c195ce8fc12b26e157a60ef591afc08604afc6a4cde7a0b5fffff001d93cfee6b',
166 | '000000202c00c39c057b21fa01ee371536dc2f8f793a5b7788934ae47687e40e000000006b8b55120f9007ba3ea2e132a5f06439763c7859edfc9319443ba16c1e2546316f7c0b5f107e0e191809dd4e',
167 | '000000209c6308f7544ba7036bc0fe8344e4bd7678b76893d9695ec00c0000000000000074bd3fe9b3f619042202dc7ab2d2b9c05672dc49fd01b4d1c21a356caae34071e07e0b5f107e0e19ddf8485c',
168 | '00000020fcea69398a4442081e7aa8e4acd091c72a82b4ca0c769f860300000000000000babc803ead2e3d9a5795281e47f3a27cfde6aeb357a681c3fc171598d8b863a892830b5fffff001d94371b66',
169 | '0000002027e8441a973d351f6fba2b4c848ab0367368de04c3c4a854916e5c89000000001d233c19800a237cd60783445e31442a6bc08f3424b0e1034ac6adfff46fd55044880b5fffff001db4f126a1',
170 | '000000203817ad719141165efe95391d1ffac4be2f8a72c92ce6b3c75a6dbd69000000008ea35a49b7cd5f102726b328c9d7bbc9c79fb193a7ebfccf07b95b1880cb6b92f78c0b5fffff001d3b36b386',
171 | '0000002084c8978238e0cf20f495df222187678c06f93faf0eba81928d463184000000005426b92c9d201202661f6f78d7ae0aebf05b10c30bfe0a64c321cab0c22d727385900b5f107e0e19d881d6cd',
172 | '0000002075c6ddf9178da0b953e50ab0bd87c60c55883294a28d521e0800000000000000f38a60b0618793f7786f4c25b5953e09e16cd468a212fdcd1b5639cf5d41ec3a37950b5fffff001dab2cd55b',
173 | '00000020aa83e392b182c4a6a4083e4214515cd5ab74c2619b0345cb033c5beb0000000086c15e4dd36daae91dc21b3d918a852116e5819d22527722339cc4748c32b942e8990b5fffff001d80e7dbb3',
174 | '00000020bbf04fb6cafb55624463c26b75edc2af8de282ed72f9bb09e1d0eb7400000000aff0853e76d5db22ce119a74727692b98891ffb163c4e843f0b33d38a0be99e79e9d0b5f107e0e19f1b7a561',
175 | '0000002064189419b1e252e27224efc504fab4354f0d5d0e357abf200c00000000000000cf1f6cb37e3cdc6a8672692357a601574f4064a59f8c458490a1d4ff6598fd333b9f0b5f107e0e190f020532',
176 | '00000020f3d0621eb61621d324fdb3110bd0cb98fe841e305b7ba56907000000000000001fbd86c53eb6f72f00698c0b1e5c79b42e768afe4a3a8f9a6247393417030070eca30b5fffff001d3b733000',
177 | '00e0ff370f1cc3fb1a138de5879e24ff4860a7b3db3dbd781e2ab2906d269ea900000000f69c8e5cdaf53829a43518f5dcbec614868317d41d4608b70e414996677f3f8a80a80b5f107e0e19d908f894',
178 | '00000020c6579415929eba0c66e72aaca673f3ab4cff4b9cf6992f290e00000000000000f4c80a75216e7f42f19223c0c4b5fd65654c8a74e7a5ee275e4040843c1a0be531ad0b5fffff001d84fb165b',
179 | '000000202d91d0263befba2ffba64c180330e74b12d4f548a3bccea3abd91f1200000000c4315d3468d97ab7a8c3a272d1a44ae8017bac6b49bad039cee3a9c06bee0306e4b10b5fffff001d1192286c',
180 | '000000208cddc3ca48a8053d4fbfe102eb2166471648bc2fdd4edd444caf6bab000000000ca65f70e938ab1cce5e02115dade2b0ce3a7074609bdc21851c56a5ded0b6ba96b60b5fffff001da0bda924',
181 | '00000020ac3fd9c3d1b00117c692c89ebc5cf1989cc6bc878b461c21b883728a00000000204ddfaca36d58c4f890871f69e57c18c5bb44cc4a23e9e13f03dac1240769f448bb0b5fffff001d2949c7c5',
182 | '00000020dbf15ef31767d35f2cfe8225a4d92e72b2ff3070ce4faaceb80e303300000000e1f872a213d928c89b6188a6b1c7d2746df0a00a05398301cc7e4687aa5f8a2df9bf0b5fffff001d35731a09',
183 | '0000002065866f585f17bea38bec57564b500df3e069ce03803963331f4f23cd00000000d3e41ca875af3abcec5cc9f2d81b49acf6cef2d2bd4028c4deb97ab9cfec9c80aac40b5fffff001dd4ab1c89',
184 | '0000002011a79b41822c4ca1d10965a91e4d4e1677db59e51f8a0771ca52385e0000000019b6b24985417db221e2a3cce2fc42d57fb151519cfac6b871c34f43235ad48cc8c50b5f107e0e19f293bf20',
185 | '00000020855166e5aa384dd70087fdb883e9398790eca2309e8d92e60800000000000000dfa1fbf4c6e13f8b025b131d20318d2e1992d920c3da3925804afecb96e0336c48c70b5f107e0e1951c563e8',
186 | '00000020d59b1e204dd2a39a1d75bc0bab18eae5d81836114db2f83e0900000000000000f570fe8da37d27b14175dfbb342b46f22c4282d464718e42b079a72777cad5ebf9cb0b5fffff001d3dc960d1',
187 | '00000020b4ccf9af326e89bfcc0b118b95a497ca798c80f4a593fc17db81ce1a00000000f6c8267a866a6d9fc1b102759432a9871f8c5ff29fbd7f8f818799cec1d9ce3aacd00b5fffff001d801b1e2a',
188 | '000000200ff9551b875e75483d11f7f46451bb7d1a3309067560526a61e5e4e600000000bde0e838d924e376021ef4c316e0911c11ddd8dc6e5d3cc3a6742791205bfca75ed50b5fffff001d00b576ac',
189 | '000000204fda7e169ded7d663a08a40a6b7846ddfd43fa71f2f321fcc2de79ac0000000018b6f7dc31858183d8dea23f13ef4ebbcb936ecc4a709a3f333b12ad143b463a15d80b5f107e0e19b8561111',
190 | '000000202e3da45405804daf42424b71a4030392cbbdff2c05aa71dd010000000000000012b3b637bbf8b6e42179684fd03c02093a1650620ff99adca67a608480c1166dc7dc0b5fffff001dbaa3cc0a',
191 | '00000020aa65635f3f9b553e637e35e8676262b6950b850589a3240a53d5fe270000000053d7cf8855be86eff69e1e8c8c58c29891389e1b078c216107408c4e7e617e6a79e10b5fffff001db864476a',
192 | '0000002005ba1186e8e2f06ff9606016d5b2895703e9f99b48de6aa53b7927730000000034cc8077e7978bc4d1597057c7d952f6793548d0f1a24af441491a275077e1892ae60b5fffff001d8a32d529',
193 | '00000020b77f4c20f3d2dbd5dd1136f18e30484d3da287f051c01aacfea333c7000000004ba3e1bbdd0799df86d1528404b5e1710f4fc93b9e30123c89bcabaefcafde52dbea0b5fffff001d23f35f52',
194 | '00000020a8d2dc7287d51daa365aeaa88017a50d7f82eba45a4a9aeeaa42cc090000000076c2f253267474c3812e55ac3e53cee838cdc55e05a10811c2924b4caa9c53c48cef0b5fffff001d1881bd8f',
195 | '000000207b07668150f2e17d18d70fa8789ed406ad31ccc962049534477ca55100000000e5061885fd662d27a7bb662f8811e080bc9ad4a57c52618a3955fd0386283bda3ff40b5fffff001da90e8de8',
196 | '000000203ee1ffe8680cc3c505d62fd5fbf5bd2430e9b5d36d87eeb04ecd9db800000000481850c5b466260c16a4093724edc17fa36a1bced63aa11f492bb55c3815141a8ff80b5f107e0e19f437ba62',
197 | '00000020b99317e3de29a7dee70a809fc2e70c3ac3a24e853c54652f0200000000000000a98004dfd88cdf3257d69711eb79a20a9734d6e4a6cb50facb34cc7994e0aad940fd0b5fffff001d36eca21f',
198 | '00000020219b165707dc67a2cb25a4424fa0e82af3a18575a08600c6a163e97b000000005235cc0479b99f41658cb84155c57004c43ae55f2c763883e122e94e63e7f44bf3010c5fffff001d3746226d',
199 | '000000204af32a565b18f7d98a5cb0982601d4b1adc850ebf317311f9ed5632500000000030859da6c856af0f0e8acddf7dcc9e0b5b5d47968d2c9bbe7c4d58d7449c483a5060c5fffff001d0f7ea10a',
200 | '00000020d11b224f3ba325211819f53c2d12ba06021996eee5679fdfe432293f000000004c5deb0564d3a758c575a2a9be5ddaafc0116bf6a189018173ae70ae334593c8580b0c5fffff001d8bfee02d',
201 | '000000206020d9252495b7c89abb66a476a44d2afaa871ced922148615bc586200000000c0cd9e90ee964c016688d5dc3e54996b2dbf0d6eb3dacfda9f83d9239c932ced250c0c5f107e0e1974b99b8b',
202 | '00000020201a8ae47743af261af7ed63cc40bd53c9f98c7f4f3a05bc030000000000000077527275b5c41a692f476fcd18c43c2c2b69c5f6907de4ccb73e9d36fa4c7828d6100c5fffff001dbd0c664e',
203 | '00e0ff2f4d71140236a8b3ed2489840af2aa3fe803296354c3d4e79827a5f2e3000000000a5a5e56c2730667e21fd9713b86d6ae25a5987ae04aed0d755bde254a5df76e6b150c5f107e0e19525d6ba4',
204 | '0000002067e08add1b9c0cc0f75495f3105dedfee6113f40146713030400000000000000d28dc33eed95ba57e2e3b04e16140fd1e6792ceadb5a88bab6abf403c44fa4751c1a0c5fffff001d9af62952',
205 | '00000020614e31d8797aad751d0a96ac558d2af5b75b8fa656e86ae215635fea0000000091332f48c3d8666afb1149fb5d4c6788016c6c3ef21890e590a32c8d13c27f51ce1e0c5fffff001da80cfe6a',
206 | '00000020c23c4a337626c788e6502dd9729d9efeebc0d8a702a3c5cfbfacb1740000000099e69526a1441fe9c876017b89bf7d7805cbb7af294686149c3f5290f4b48bed0b220c5f107e0e19bd8aca72',
207 | '000000207b41026173d3ba04613e03eb968cbc71efe7f4f38cc1a58b030000000000000010e8d5c4567c8a634aa89f01473583bdf583f227ab651e6f8da1d364ac6c5b78bc260c5fffff001da556d970',
208 | '000000206d033d2a32833ef0725d6b3bb161a802413cf689675c3dc338ae14bb0000000091d63795048f585d27218719df2d9610930838d060dc65f3d8b7cd10fb2a39ac6e2b0c5fffff001d81dd3182',
209 | '00000020c63ae53f37793c65398b359f87658184920f34abd3f44b2d359cd7f40000000073d5a12799ac075cd7976f25459aca3fb439fcd9a170ff8e0bf3b7ebd174703022300c5fffff001d00babe84',
210 | '00000020cad36d868f157493ffc65586843b60d7b2abc552b51cc1b5f0b39e98000000004acb2fcf9ec1d0c415cecff80792144db3ebfd66b4258900a425250fde3d77b8d4340c5fffff001d1e2ab936',
211 | '00000020bc6172c9947cb83ab9d0e4aa8df79f666ef41ebd4d7664859dddd5fe00000000149ea9f9c2815bb3ab951fb83943a71629aa04279c5475edbba5d79b3b3b8ddeae350c5f107e0e194b5ccca7',
212 | '00000020239c8c37d22e780af861d5c343addc0e76f045651f18057109000000000000000fe9006bffd1767d5ae4c99532dbca74f93123062df2798fe6ad02c721bebbe25f3a0c5fffff001d13d3d08f',
213 | '000000204c9038f6106e61c36140f472510e5be75ad8d6566b60c391e20668eb00000000140a03aa37efebf0faccc9ed24a6d60c422ea1df336915087e601cf7934174f8f83d0c5f107e0e1944bae361',
214 | '0000002008cf666fd73e510faaa7b90ae09579595c4105376af41c740d00000000000000c4ba374ae1a45a10c01f093402bf791148baeebda235664e01170490ca6b1845213e0c5f107e0e1927c406cc',
215 | '0000002029ba9267986eff2e6934a0c679d8e68996d24b8326ba10860a00000000000000708e2e0820876b03c82a6188e97e3ef77c147b8032a4bcf9773bf2bd8785123ad2420c5fffff001dadc0beda',
216 | '000000207624c17243e56f622e8f5685ab51f44e97622b756ec1382e3cf65dcc00000000ae09a803125635057e7480823581869f5917f80db98c5c3cd4d8a60c708f3beb84470c5fffff001d8946e3e1',
217 | '0000002026d73a7527402391b523e354d996f335f3d133c4586d12a1bd2ec5c100000000ea42cde4ab437ced3b7902bcda6f69dac08a03ce849e7a25c281f97cc9862731364c0c5fffff001d3b3f744b',
218 | '00e0ff377e4e85afd6ab1ba9e4e8c6d8d091f72e315745ca52d22dacdb7efa87000000007e8198d5048c56ea9080f92efa598bf7d90e5d4c7c378f5819fafff0df5a307f814d0c5f107e0e19d0fe2631',
219 | '000000206f3dcf3a5b9d26250a8d985eb2fa8850bdbd8390dad9c1fa000000000000000028c9f071e3d6966bce8788d428c0c0cbe3114bf3f27fa2ffd2057a68f872766b32520c5fffff001d06f4fa04',
220 | '0000002071588ebb39e550a42baf80d4c850d7246933c8ffb0f533ce26d3a3a9000000001247ccb2ed4d424f28cd39d325ae4b82dc3a33d79570ec364b235e07261ec5b3e4560c5fffff001d1ba4e6d7'
221 | ];
222 | const genesisHeight = 1780500;
223 |
224 | beforeEach(async () => {
225 | signers = await ethers.getSigners();
226 | relay = await deploy(signers[0], '0x' + headers[0], genesisHeight);
227 | });
228 |
229 | it('should store genesis', async () => {
230 | // check header was stored correctly
231 | const hash = bitcoin.crypto
232 | .hash256(Buffer.from(headers[0], 'hex'))
233 | .toString('hex');
234 | // expect(hash).to.eq("000000000000000231fc185a92e1d93493c2a68582f4e21cd6503997b4c55328");
235 | const height = await relay.getBlockHeight('0x' + hash);
236 | expect(height).to.eq(genesisHeight);
237 | });
238 |
239 | it('should store 2 headers', async () => {
240 | await relay.submitBlockHeaderBatch(
241 | headers.slice(1, 3).reduce((prev, curr) => {
242 | return Buffer.concat([prev, Buffer.from(curr, 'hex')]);
243 | }, Buffer.alloc(0))
244 | );
245 | });
246 |
247 | it('should store 4 headers', async () => {
248 | await relay.submitBlockHeaderBatch(
249 | headers.slice(1, 5).reduce((prev, curr) => {
250 | return Buffer.concat([prev, Buffer.from(curr, 'hex')]);
251 | }, Buffer.alloc(0))
252 | );
253 | });
254 |
255 | it('should store 8 headers', async () => {
256 | await relay.submitBlockHeaderBatch(
257 | headers.slice(1, 9).reduce((prev, curr) => {
258 | return Buffer.concat([prev, Buffer.from(curr, 'hex')]);
259 | }, Buffer.alloc(0))
260 | );
261 | });
262 |
263 | it('should store 16 headers', async () => {
264 | await relay.submitBlockHeaderBatch(
265 | headers.slice(1, 17).reduce((prev, curr) => {
266 | return Buffer.concat([prev, Buffer.from(curr, 'hex')]);
267 | }, Buffer.alloc(0))
268 | );
269 | });
270 |
271 | it('should store 32 headers', async () => {
272 | await relay.submitBlockHeaderBatch(
273 | headers.slice(1, 33).reduce((prev, curr) => {
274 | return Buffer.concat([prev, Buffer.from(curr, 'hex')]);
275 | }, Buffer.alloc(0))
276 | );
277 | });
278 |
279 | it('should fail with duplicate batch', async () => {
280 | const rawHeaders = headers.slice(1, 7).reduce((prev, curr) => {
281 | return Buffer.concat([prev, Buffer.from(curr, 'hex')]);
282 | }, Buffer.alloc(0));
283 |
284 | await relay.submitBlockHeaderBatch(rawHeaders);
285 |
286 | const result = relay.submitBlockHeaderBatch(rawHeaders);
287 | await expect(result).to.be.revertedWith(ErrorCode.ERR_DUPLICATE_BLOCK);
288 | });
289 |
290 | it('should fail because prev block not stored', async () => {
291 | const result = relay.submitBlockHeaderBatch(
292 | headers.slice(2, 8).reduce((prev, curr) => {
293 | return Buffer.concat([prev, Buffer.from(curr, 'hex')]);
294 | }, Buffer.alloc(0))
295 | );
296 | await expect(result).to.be.revertedWith(ErrorCode.ERR_PREVIOUS_BLOCK);
297 | });
298 | });
299 |
--------------------------------------------------------------------------------
/test/scripts.test.ts:
--------------------------------------------------------------------------------
1 | import {ethers} from 'hardhat';
2 | import {Signer, Wallet} from 'ethers';
3 | import chai from 'chai';
4 | import {deployContract, solidity} from 'ethereum-waffle';
5 | import Artifact from '../artifacts/contracts/ScriptDelegate.sol/ScriptDelegate.json';
6 | import {ScriptDelegate} from '../typechain/ScriptDelegate';
7 | import * as bech32 from 'bech32';
8 |
9 | chai.use(solidity);
10 | const {expect} = chai;
11 |
12 | describe('Scripts', () => {
13 | let signers: Signer[];
14 | let parser: ScriptDelegate;
15 |
16 | beforeEach(async () => {
17 | signers = await ethers.getSigners();
18 | parser = (await deployContract(
19 | signers[0] as Wallet,
20 | Artifact,
21 | []
22 | )) as ScriptDelegate;
23 | });
24 |
25 | it('should accept p2sh script', async () => {
26 | const result = await parser.isP2SH(
27 | '0xa914d8b6fcc85a383261df05423ddf068a8987bf028787'
28 | );
29 | expect(result).to.be.true;
30 |
31 | const hash = await parser.P2SH(
32 | '0xa914d8b6fcc85a383261df05423ddf068a8987bf028787'
33 | );
34 | expect(hash).to.eq('0xd8b6fcc85a383261df05423ddf068a8987bf0287');
35 | });
36 |
37 | it('should reject incorrect p2sh script', async () => {
38 | const result = await parser.isP2SH(
39 | '0x8814d8b6fcc85a383261df05423ddf068a8987bf028787'
40 | );
41 | expect(result).to.be.false;
42 | });
43 |
44 | it('should accept p2pkh script', async () => {
45 | const result = await parser.isP2PKH(
46 | '0x76a91412ab8dc588ca9d5787dde7eb29569da63c3a238c88ac'
47 | );
48 | expect(result).to.be.true;
49 |
50 | const hash = await parser.P2PKH(
51 | '0x76a91412ab8dc588ca9d5787dde7eb29569da63c3a238c88ac'
52 | );
53 | expect(hash).to.eq('0x12ab8dc588ca9d5787dde7eb29569da63c3a238c');
54 | });
55 |
56 | it('should reject incorrect p2pkh script', async () => {
57 | const result = await parser.isP2PKH(
58 | '0x88a91412ab8dc588ca9d5787dde7eb29569da63c3a238c88ac'
59 | );
60 | expect(result).to.be.false;
61 | });
62 |
63 | it('should accept cltv script', async () => {
64 | const result = await parser.isCLTV(
65 | '0x049f7b2a5cb17576a914371c20fb2e9899338ce5e99908e64fd30b78931388ac'
66 | );
67 | expect(result.time).to.eq(1546288031);
68 | expect(result.addr).to.eq('0x371c20fb2e9899338ce5e99908e64fd30b789313');
69 | });
70 |
71 | it('should accept p2wpkh script (testnet)', async () => {
72 | const result = await parser.isP2WPKH(
73 | '0x00145587090c3288b46df8cc928c6910a8c1bbea508f'
74 | );
75 | expect(result).to.be.true;
76 |
77 | const {version, program} = await parser.P2WPKH(
78 | '0x00145587090c3288b46df8cc928c6910a8c1bbea508f'
79 | );
80 |
81 | const words = bech32.toWords(Buffer.from(program.substr(2), 'hex'));
82 | words.unshift(parseInt(version));
83 |
84 | expect(bech32.encode('tb', words)).to.eq(
85 | 'tb1q2krsjrpj3z6xm7xvj2xxjy9gcxa755y0exegh6'
86 | );
87 | });
88 |
89 | it('should accept op_return', async () => {
90 | const result = await parser.isOpReturn(
91 | '0x6a200000000000000000000000000000000000000000000000000000000000000000'
92 | );
93 | expect(result).to.be.true;
94 |
95 | const data = await parser.OpReturn(
96 | '0x6a200000000000000000000000000000000000000000000000000000000000000000'
97 | );
98 |
99 | expect(data).to.eq(
100 | '0x0000000000000000000000000000000000000000000000000000000000000000'
101 | );
102 | });
103 | });
104 |
--------------------------------------------------------------------------------
/test/target.test.ts:
--------------------------------------------------------------------------------
1 | import {ethers} from 'hardhat';
2 | import {Signer, Wallet} from 'ethers';
3 | import chai from 'chai';
4 | import {deployContract, solidity} from 'ethereum-waffle';
5 | import RelayArtifact from '../artifacts/contracts/Relay.sol/Relay.json';
6 | import {Relay} from '../typechain/Relay';
7 |
8 | chai.use(solidity);
9 | const {expect} = chai;
10 |
11 | describe('Retarget', () => {
12 | let signers: Signer[];
13 | let relay: Relay;
14 |
15 | beforeEach(async () => {
16 | signers = await ethers.getSigners();
17 | const genesis =
18 | '0x000040202842774747733a4863b6bbb7b4cfb66baa9287d5ce0d13000000000000000000df550e01d02ee37fce8dd2fbf919a47b8b65684bcb48d4da699078916da2f7decbc7905ebc2013178f58d533';
19 | relay = (await deployContract(signers[0] as Wallet, RelayArtifact, [
20 | genesis,
21 | 1
22 | ])) as Relay;
23 | });
24 |
25 | it('valid difficulty target', async () => {
26 | const epoch = {
27 | start: {
28 | header:
29 | '0x02000000dca825543cefd662b3199e02afa13c6aad4b01890180010400000000000000000187743d481db520170f22701f34a1449384446a55575e2c46e183de28b4854701e690539a855d18b189b5e4',
30 | target: '0x5d859a000000000000000000000000000000000000000000',
31 | time: '0x5390e601'
32 | },
33 | end: {
34 | header:
35 | '0x0200000075e95a670774b501ff619fdb000f504c0ad29d3f083a27510000000000000000b013371f3c2ee20683a8e492547bb0b87da4b3d8a0ac8ad79bd16ad35f3657853c04a1539a855d182522ac98',
36 | target: '0x5d859a000000000000000000000000000000000000000000',
37 | time: '0x53a1043c'
38 | }
39 | };
40 |
41 | const next = {
42 | header:
43 | '0x02000000b2b3d204fbd1fda5f1bfa8e83d6f67be7307c05a64d4441b0000000000000000cd19b368ea76f54a604d5c5222d190112f364df1a5af37fbc24cb3fbd32b97642004a153a2ab5118824d1fac',
44 | target: '0x51aba2000000000000000000000000000000000000000000',
45 | height: 306432
46 | };
47 |
48 | const result = await relay.isCorrectDifficultyTarget(
49 | epoch.start.target,
50 | epoch.start.time,
51 | epoch.end.target,
52 | epoch.end.time,
53 | next.target
54 | );
55 | expect(result).to.eq(true);
56 | });
57 |
58 | it('invalid difficulty target', async () => {
59 | const epoch = {
60 | start: {
61 | header:
62 | '0x02000000dca825543cefd662b3199e02afa13c6aad4b01890180010400000000000000000187743d481db520170f22701f34a1449384446a55575e2c46e183de28b4854701e690539a855d18b189b5e4',
63 | target: '0x5d859a000000000000000000000000000000000000000000',
64 | time: '0x5390e601'
65 | },
66 | end: {
67 | header:
68 | '0x0200000075e95a670774b501ff619fdb000f504c0ad29d3f083a27510000000000000000b013371f3c2ee20683a8e492547bb0b87da4b3d8a0ac8ad79bd16ad35f3657853c04a1539a855d182522ac98',
69 | target: '0x5d859a000000000000000000000000000000000000000000',
70 | time: '0x53a1043c'
71 | }
72 | };
73 |
74 | const next = {
75 | header:
76 | '0x02000000b2b3d204fbd1fda5f1bfa8e83d6f67be7307c05a64d4441b0000000000000000cd19b368ea76f54a604d5c5222d190112f364df1a5af37fbc24cb3fbd32b97642004a153a2ab5218824d1fac',
77 | target: '0x52aba2000000000000000000000000000000000000000000',
78 | height: 306432
79 | };
80 |
81 | const result = await relay.isCorrectDifficultyTarget(
82 | epoch.start.target,
83 | epoch.start.time,
84 | epoch.end.target,
85 | epoch.end.time,
86 | next.target
87 | );
88 | expect(result).to.eq(false);
89 | });
90 |
91 | it('invalid period', async () => {
92 | const epoch = {
93 | start: {
94 | header:
95 | '0x02000000dca825543cefd662b3199e02afa13c6aad4b01890180010400000000000000000187743d481db520170f22701f34a1449384446a55575e2c46e183de28b4854701e690538a855d18b189b5e4',
96 | target: '0x5d858a000000000000000000000000000000000000000000',
97 | time: '0x5390e601'
98 | },
99 | end: {
100 | header:
101 | '0x0200000075e95a670774b501ff619fdb000f504c0ad29d3f083a27510000000000000000b013371f3c2ee20683a8e492547bb0b87da4b3d8a0ac8ad79bd16ad35f3657853c04a1539a855d182522ac98',
102 | target: '0x5d859a000000000000000000000000000000000000000000',
103 | time: '0x53a1043c'
104 | }
105 | };
106 |
107 | const next = {
108 | header:
109 | '0x02000000b2b3d204fbd1fda5f1bfa8e83d6f67be7307c05a64d4441b0000000000000000cd19b368ea76f54a604d5c5222d190112f364df1a5af37fbc24cb3fbd32b97642004a153a2ab5118824d1fac',
110 | target: '0x51aba2000000000000000000000000000000000000000000',
111 | height: 306432
112 | };
113 |
114 | const result = relay.isCorrectDifficultyTarget(
115 | epoch.start.target,
116 | epoch.start.time,
117 | epoch.end.target,
118 | epoch.end.time,
119 | next.target
120 | );
121 | await expect(result).to.be.reverted;
122 | });
123 | });
124 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "module": "commonjs",
5 | "strict": true,
6 | "esModuleInterop": true,
7 | "outDir": "dist",
8 | "declaration": true,
9 | "resolveJsonModule": true
10 | },
11 | "include": [
12 | "typechain",
13 | "scripts",
14 | "test",
15 | "index.ts"
16 | ],
17 | "files": [
18 | "hardhat.config.ts",
19 | "node_modules/@nomiclabs/buidler-ethers/src/type-extensions.d.ts",
20 | "node_modules/@nomiclabs/buidler-etherscan/src/type-extensions.d.ts",
21 | "node_modules/@nomiclabs/buidler-waffle/src/type-extensions.d.ts",
22 | "node_modules/buidler-typechain/src/type-extensions.d.ts"
23 | ]
24 | }
--------------------------------------------------------------------------------