├── .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 | } --------------------------------------------------------------------------------