├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.adoc ├── canister ├── Cargo.toml ├── README.adoc ├── build.rs ├── candid.did ├── src │ ├── adapter-shim.rs │ ├── block.rs │ ├── blockforest.rs │ ├── candid_types.rs │ ├── lib.rs │ ├── main.rs │ ├── proto.proto │ ├── store.rs │ ├── sync_demo.rs │ ├── test_builder.rs │ └── utxoset.rs └── test-data │ └── 100k_blocks.dat ├── dfx.json ├── docker-compose.yml ├── docker ├── adapter │ ├── Dockerfile │ └── regtest.json └── bitcoind │ └── conf │ └── bitcoin.conf ├── examples ├── README.adoc ├── common │ ├── Cargo.lock │ ├── Cargo.toml │ ├── candid.did │ └── src │ │ ├── lib.rs │ │ ├── main.rs │ │ └── types.rs ├── motoko │ ├── README.adoc │ └── src │ │ ├── Main.mo │ │ ├── Types.mo │ │ └── Utils.mo └── rust │ ├── Cargo.lock │ ├── Cargo.toml │ ├── README.adoc │ ├── build.sh │ ├── candid.did │ └── src │ └── main.rs ├── rust-toolchain.toml ├── scripts ├── build-canister.sh ├── build-example-common.sh └── build-example.sh └── types ├── Cargo.lock ├── Cargo.toml └── src └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | # Various IDEs and Editors 2 | .vscode/ 3 | .idea/ 4 | **/*~ 5 | 6 | # Mac OSX temporary files 7 | .DS_Store 8 | **/.DS_Store 9 | 10 | # dfx temporary files 11 | .dfx/ 12 | 13 | # Cargo build output 14 | target/ 15 | out/ 16 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | members = [ 4 | "canister", 5 | "examples/common", 6 | "examples/rust", 7 | "types", 8 | ] 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | Copyright 2022 DFINITY Stiftung. 179 | 180 | Licensed under the Apache License, Version 2.0 (the "License"); 181 | you may not use this file except in compliance with the License. 182 | You may obtain a copy of the License at 183 | 184 | http://www.apache.org/licenses/LICENSE-2.0 185 | 186 | Unless required by applicable law or agreed to in writing, software 187 | distributed under the License is distributed on an "AS IS" BASIS, 188 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 189 | See the License for the specific language governing permissions and 190 | limitations under the License. 191 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | = Bitcoin Integration Developer Preview 2 | 3 | == Overview 4 | 5 | The https://dfinity.org/howitworks/direct-integration-with-bitcoin[integration] 6 | between the Internet Computer and Bitcoin will enable developers to build canisters that: 7 | 8 | 1. *Securely own Bitcoin.* 9 | + 10 | Canisters will be able to have the Internet Computer securely create ECDSA keys 11 | for them via a threshold ECDSA protocol so that they can own Bitcoin. 12 | 13 | 2. *Interact with the Bitcoin network.* 14 | + 15 | Canisters will be able to send transactions, get transaction outputs, as well as 16 | get the balance of any Bitcoin address. 17 | The aforementioned threshold ECDSA protocol is used to securely sign transactions. 18 | 19 | == Differences to Main Release 20 | 21 | The developer preview enables developers to work with the Bitcoin integration features 22 | before the proper launch. 23 | 24 | In the developer preview, all components run *locally*, i.e., no Bitcoin functionality 25 | is deployed on mainnet. 26 | The Bitcoin canister is configured to interact with a local Bitcoin network in `regtest` mode. 27 | 28 | By contrast, the Bitcoin functionality will be exposed through the 29 | https://smartcontracts.org/docs/interface-spec/index.html#ic-management-canister[management canister] 30 | on mainnet. 31 | 32 | The Bitcoin API on mainnet will be quite similar to the <> 33 | in the developer preview. 34 | However, the function signatures will be slightly different. Moreover, the Bitcoin 35 | functionality will be extended, for example, by offering an API for fee management. 36 | 37 | == Components 38 | 39 | The developer preview consists of the following components: 40 | 41 | 1. *The Bitcoin Canister* 42 | + 43 | The Bitcoin canister provides the API that developers can use to interact with the Bitcoin networks. 44 | 45 | + 46 | See the `./canister` directory for more details. 47 | 48 | 2. *An Example Project* 49 | + 50 | The project showcases how developers can achieve the following: 51 | 52 | . Get the balance of a Bitcoin address. 53 | . Get transaction outputs and use them to build a transaction. 54 | . Sign a transaction and send it to the Bitcoin network. 55 | 56 | + 57 | See the `./example` directory for more details. 58 | 59 | NOTE: The developer preview focuses strictly on the interaction with the Bitcoin network. 60 | Securely generating ECDSA keys is beyond the scope of the developer preview. 61 | 62 | == Getting Started 63 | 64 | With the developer preview you'll be able to setup a local Bitcoin network and interact with 65 | that network using canisters. 66 | 67 | There are two ways to set up: 68 | 69 | . <> 70 | . <> 71 | 72 | == Manual Setup 73 | === Prerequisites 74 | 75 | * https://rustup.rs/[Rust] 76 | * https://smartcontracts.org/docs/download.html[dfx] >= 0.8.4 77 | * https://bitcoin.org/en/download[Bitcoin Core]. Mac users are recommended to download the `.tar.gz` version. 78 | * Mac users need to install https://brew.sh/[homebrew] and then use it to install additional packages by running `brew install llvm binaryen cmake` 79 | 80 | NOTE: These instructions assume you're running Linux or MacOS. We do not officially support Windows. 81 | 82 | The first step would be to setup a local Bitcoin network. 83 | 84 | === Setting up a local Bitcoin network 85 | 86 | 1. Unpack the `.tar.gz` file. 87 | 2. Create a directory named `data` inside the unpacked folder. 88 | 3. Create a file called `bitcoin.conf` at the root of the unpacked folder and add the following contents: 89 | + 90 | ``` 91 | # Enable regtest mode. This is required to setup a private bitcoin network. 92 | regtest=1 93 | 94 | # Dummy credentials that are required by `bitcoin-cli`. 95 | rpcuser=btc-dev-preview 96 | rpcpassword=Wjh4u6SAjT4UMJKxPmoZ0AN2r9qbE-ksXQ5I2_-Hm4w= 97 | rpcauth=btc-dev-preview:8555f1162d473af8e1f744aa056fd728$afaf9cb17b8cf0e8e65994d1195e4b3a4348963b08897b4084d210e5ee588bcb 98 | ``` 99 | 4. Run `bitcoind` to start the bitcoin client using the following command: 100 | + 101 | `./bin/bitcoind -conf=$(pwd)/bitcoin.conf -datadir=$(pwd)/data` 102 | 103 | 5. Create a wallet: `./bin/bitcoin-cli -conf=$(pwd)/bitcoin.conf createwallet mywallet` 104 | + 105 | If everything is setup correctly, you should see the following output: 106 | + 107 | ``` 108 | { 109 | "name": "mywallet", 110 | "warning": "" 111 | } 112 | ``` 113 | 114 | 6. Generate a bitcoin address and save it in variable for later reuse: 115 | + 116 | ``` 117 | export BTC_ADDRESS=$(./bin/bitcoin-cli -conf=$(pwd)/bitcoin.conf getnewaddress) 118 | ``` 119 | + 120 | This will generate a bitcoin address for your wallet to receive funds. 121 | 122 | 7. Mine blocks to receive some Bitcoin as a reward. 123 | + 124 | `./bin/bitcoin-cli -conf=$(pwd)/bitcoin.conf generatetoaddress 101 $BTC_ADDRESS` 125 | + 126 | You should see an output that looks similar to, but not exactly like, the following: 127 | + 128 | ``` 129 | [ 130 | "1625281b2595b77276903868a0fe2fc31cb0c624e9bdc269e74a3f319ceb48de", 131 | "1cc5ba7e86fc313333c5448af6c7af44ff249eca3c8b681edc3c275efd3a2d38", 132 | "1d3c85b674497ba08a48d1b955bee5b4dc4505ffe4e9f49b428153e02e3e0764", 133 | ... 134 | "0dfd066985dc001ccc1fe6d7bfa53b7ad4944285dc173615792653bbd52151f1", 135 | "65975f1cd5809164f73b0702cf326204d8fee8b9669bc6bd510cb221cf09db5c", 136 | ] 137 | ``` 138 | 139 | === Running the IC-Bitcoin Adapter 140 | 141 | Now that bitcoin is setup locally, it is time to run the IC-Bitcoin adapter. 142 | 143 | The IC-Bitcoin adapter is a process that fetches headers and blocks from the Bitcoin network 144 | and passes them into the Internet Computer. The ic-bitcoin adapter will be integrated into the 145 | replica with the main release. For the developer preview, it needs to be launched separately. 146 | 147 | Run the following commands to download, build, and run the adapter. 148 | 149 | ```bash 150 | # clone the ic repository and checkout a specific commit. 151 | git clone https://github.com/dfinity/ic.git 152 | cd ic 153 | git checkout 99116f8e872b8765aa609f91eb8c9394914c483d 154 | 155 | # Move into the rs directory and run the adapter. 156 | cd rs 157 | cargo run --bin ic-btc-adapter -- ./bitcoin/adapter/tests/sample/regtest.config.json 158 | ``` 159 | 160 | [[Deploying-the-Bitcoin-Canister]] 161 | === Deploying the Bitcoin Canister 162 | 163 | With `bitcoind` and the adapter running, we can now run a local replica with the Bitcoin canister. 164 | 165 | 1. Clone this repository. 166 | 2. From the root directory of the repository, start the local replica. 167 | + 168 | ```bash 169 | dfx start --clean --background 170 | ``` 171 | 3. Deploy the Bitcoin canister to the local replica in regtest mode. 172 | + 173 | ``` 174 | dfx deploy btc --no-wallet 175 | ``` 176 | 177 | === Running the Adapter Shim 178 | 179 | The shim is the final piece that needs to be started up. 180 | 181 | From this repository, run the following command: 182 | 183 | ```bash 184 | cargo run --features="tokio candid ic-agent garcon tonic tonic-build" --bin adapter-shim $(dfx canister --no-wallet id btc) 185 | ``` 186 | 187 | The shim will start syncing blocks from your local bitcoin setup into the bitcoin canister. 188 | Once that's complete, you'll be able to query the bitcoin canister about the bitcoin state. 189 | See <> for more details and checkout the <>. 190 | 191 | == Docker Setup 192 | 193 | === Prerequisites 194 | 195 | Instead of downloading bitcoin and cloning the `ic` repository, this repository offers an alternate 196 | solution using Docker and `docker-compose`. 197 | 198 | * https://rustup.rs/[Rust] 199 | * https://smartcontracts.org/docs/download.html[dfx] >= 0.8.4 200 | * Mac users need to install https://brew.sh/[homebrew] and then use it to install additional packages by running `brew install llvm binaryen cmake` 201 | * Docker 202 | ** Mac: https://docs.docker.com/desktop/mac/install/[Docker for Mac] 203 | ** Linux: https://docs.docker.com/engine/install/[Docker Engine] and https://docs.docker.com/compose/install/[Docker Compose]. 204 | 205 | === Setting up a local Bitcoin network and the IC-Bitcoin Adapter 206 | 207 | 1. `docker-compose up -d` will start `bitcoind` in the background and begin building a fresh image for the IC-Bitcoin adapter. 208 | 2. Verify that bitcoind is running: `docker-compose exec bitcoind bitcoin-cli -conf=/conf/bitcoin.conf getmininginfo` 209 | + 210 | If everything is setup correctly, you should see the following output: 211 | + 212 | ``` 213 | { 214 | "blocks": 0, 215 | "difficulty": 4.656542373906925e-10, 216 | "networkhashps": 0, 217 | "pooledtx": 0, 218 | "chain": "regtest", 219 | "warnings": "" 220 | } 221 | ``` 222 | 223 | 3. Create a wallet: `docker-compose exec bitcoind bitcoin-cli -conf=/conf/bitcoin.conf createwallet mywallet` 224 | + 225 | If everything is setup correctly, you should see the following output: 226 | + 227 | ``` 228 | { 229 | "name": "mywallet", 230 | "warning": "" 231 | } 232 | ``` 233 | 234 | 4. Generate a bitcoin address and save it in variable for later reuse: 235 | + 236 | ``` 237 | export BTC_ADDRESS=$(docker-compose exec bitcoind bitcoin-cli -conf=/conf/bitcoin.conf getnewaddress | tr -d '\r') 238 | ``` 239 | + 240 | This will generate a bitcoin address for your wallet to receive funds. 241 | 242 | 5. Mine blocks to receive some Bitcoin as a reward. 243 | + 244 | `docker-compose exec bitcoind bitcoin-cli -conf=/conf/bitcoin.conf generatetoaddress 101 $BTC_ADDRESS` 245 | + 246 | You should see an output that looks similar to, but not exactly like, the following: 247 | + 248 | ``` 249 | [ 250 | "1625281b2595b77276903868a0fe2fc31cb0c624e9bdc269e74a3f319ceb48de", 251 | "1cc5ba7e86fc313333c5448af6c7af44ff249eca3c8b681edc3c275efd3a2d38", 252 | "1d3c85b674497ba08a48d1b955bee5b4dc4505ffe4e9f49b428153e02e3e0764", 253 | ... 254 | "0dfd066985dc001ccc1fe6d7bfa53b7ad4944285dc173615792653bbd52151f1", 255 | "65975f1cd5809164f73b0702cf326204d8fee8b9669bc6bd510cb221cf09db5c", 256 | ] 257 | ``` 258 | 6. Verify the adapter is running: `docker-compose logs adapter` 259 | + 260 | You should an output that looks similar to the following: 261 | ``` 262 | adapter_1 | Feb 02 01:01:56.512 INFO Connected to 172.29.0.2:18444 263 | adapter_1 | Feb 02 01:01:57.022 INFO Received version from 172.29.0.2:18444 264 | adapter_1 | Feb 02 01:01:57.022 INFO Completed the version handshake with 172.29.0.2:18444 265 | adapter_1 | Feb 02 01:01:57.022 INFO Adding peer_info with addr : 172.29.0.2:18444 266 | adapter_1 | Feb 02 01:01:57.223 INFO Received verack from 172.29.0.2:18444 267 | ``` 268 | 269 | Continue with the Getting Started directions from <> to complete setup. 270 | 271 | === Viewing `bitcoind` and `IC-Bitcoin Adapter` output 272 | 273 | * To view the logs of the `bitcoind` container: `docker-compose logs -f bitcoind` 274 | * To view the logs of the `adapter` container: `docker-compose logs -f adapter` 275 | 276 | == Using the Bitcoin Canister 277 | 278 | There's an example project in the `./example` directory that showcases how to interact with the Bitcoin canister. 279 | Additionally, you can call the Bitcoin canister directly using `dfx`. Examples: 280 | 281 | **Fetching the balance/UTXOs of an address** 282 | ``` 283 | dfx canister --no-wallet call btc get_balance "(record { address = \"$BTC_ADDRESS\"})" 284 | dfx canister --no-wallet call btc get_utxos "(record { address = \"$BTC_ADDRESS\"})" 285 | ``` 286 | 287 | **Fetching the balance/UTXOs of an address with a minimum of 6 confirmations** 288 | ``` 289 | dfx canister --no-wallet call btc get_balance "(record { address = \"$BTC_ADDRESS\"; min_confirmations = opt 6})" 290 | dfx canister --no-wallet call btc get_utxos "(record { address = \"$BTC_ADDRESS\"; min_confirmations = opt 6})" 291 | ``` 292 | -------------------------------------------------------------------------------- /canister/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "btc" 3 | version = "0.1.0" 4 | edition = "2018" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | bitcoin = "0.27.1" 10 | byteorder = "1.4.3" 11 | ic-btc-types = { path = "../types" } 12 | ic-cdk = "0.3.1" 13 | ic-cdk-macros = "0.3.1" 14 | lazy_static = "1.4.0" 15 | prost = "0.9" 16 | serde = "1.0.132" 17 | 18 | # Optional dependencies that are needed for the test bins, but not for the canister. 19 | candid = {version = "0.7.8", optional = true} 20 | garcon = {version = "0.2.3", optional = true} 21 | ic-agent = {version = "0.10.0", optional = true} 22 | tokio = { version = "1.14", features = ["full"], optional = true } 23 | tonic = { version = "0.6.2", optional = true } 24 | 25 | [build-dependencies] 26 | prost-build = "0.9.0" 27 | tonic-build = { version = "0.6.2", optional = true } 28 | 29 | [[bin]] 30 | name = "sync_demo" 31 | path = "src/sync_demo.rs" 32 | required-features = ["tonic", "tonic-build", "tokio"] 33 | 34 | [[bin]] 35 | name = "canister" 36 | path = "src/main.rs" 37 | 38 | [[bin]] 39 | name = "adapter-shim" 40 | path = "src/adapter-shim.rs" 41 | required-features = ["tonic-build", "tokio", "candid", "ic-agent", "garcon", "tonic"] 42 | 43 | [dev-dependencies] 44 | bitcoin = {version = "0.27.1", features = ["rand"]} # needed for generating secp256k1 keys. 45 | maplit = "1.0.2" 46 | tempfile = "3.2.0" 47 | -------------------------------------------------------------------------------- /canister/README.adoc: -------------------------------------------------------------------------------- 1 | = The Bitcoin Canister 2 | 3 | == Overview 4 | 5 | The Bitcoin canister is the core component of the Bitcoin integration project. 6 | It enables other canisters deployed on the Internet Computer to use Bitcoin and interact with the Bitcoin network. 7 | 8 | To this end, it provides a low-level API with a small set of functions, which 9 | serve as the foundation to build powerful Bitcoin libraries and other development tools, 10 | and Bitcoin smart contracts running on the Internet Computer. 11 | 12 | == API 13 | 14 | The Bitcoin canister exposes the following functions: 15 | 16 | - <>: The function returns the unspent transaction outputs (UTXOs) of a given Bitcoin address. 17 | - <>: The function returns the balance of a given Bitcoin address. 18 | - <>: The function sends the given transaction to the Bitcoin network. 19 | 20 | The full interface description can be found link:candid.did[here], 21 | expressed in https://github.com/dfinity/candid/blob/master/spec/Candid.md[Candid syntax]. 22 | 23 | More details about the functions are provided below. 24 | 25 | === Get Unspent Transaction Outputs of a Bitcoin Address 26 | 27 | Given a https://en.bitcoin.it/wiki/Base58Check_encoding[base58-encoded] address as part of a 28 | `GetUtxosRequest`, the function returns all UTXOs associated with the 29 | provided address. 30 | 31 | ``` 32 | type Satoshi = nat64; 33 | 34 | type OutPoint = record { 35 | txid : blob; 36 | vout : nat32 37 | }; 38 | 39 | type Utxo = record { 40 | outpoint: OutPoint; 41 | value: Satoshi; 42 | height: nat32; 43 | confirmations: nat32; 44 | }; 45 | 46 | type GetUtxosRequest = record { 47 | address : text; 48 | min_confirmations: opt nat32; 49 | offset: opt nat32; 50 | }; 51 | 52 | type GetUtxosError = variant { 53 | MalformedAddress; 54 | // More error types to be added here. 55 | }; 56 | 57 | get_utxos: (GetUtxosRequest) -> (variant { 58 | Ok : record { 59 | utxos: vec Utxo; 60 | total_count: nat32; 61 | }; 62 | Err : opt GetUtxosError; 63 | }); 64 | ``` 65 | 66 | If the call fails, e.g., because the address is malformed, a `GetUtxosError` is returned, 67 | indicating the reason for the failed call. 68 | 69 | The optional `min_confirmations` parameter can be used to limit the returned UTXOs to those with at 70 | least the provided number of confirmations. 71 | If this parameter is not used, the default value is 0. 72 | 73 | The optional `offset` parameter can be used to specify a starting offset in the list of UTXOs. 74 | This parameter is useful for addresses with many UTXOs. + 75 | Note that there is no guarantee that the set of UTXOs will remain unchanged between function calls with different 76 | offsets, i.e., every call will return the UTXOs starting from the provided offset based on the 77 | current view. 78 | If this parameter is not used, the default value is 0. 79 | 80 | === Get the Balance of a Bitcoin Address 81 | 82 | Given a https://en.bitcoin.it/wiki/Base58Check_encoding[base58-encoded] address as part of a 83 | `GetBalanceRequest` , the function returns the current balance of this address in `Satoshi` (100,000,000 Satoshi = 1 Bitcoin). 84 | 85 | ``` 86 | type GetBalanceRequest = record { 87 | address : text; 88 | min_confirmations: opt nat32; 89 | }; 90 | 91 | type GetBalanceError = variant { 92 | MalformedAddress; 93 | // More error types to be added here. 94 | }; 95 | 96 | get_balance: (GetBalanceRequest) -> (variant { 97 | Ok : Satoshi; 98 | Err: opt GetBalanceError; 99 | }); 100 | ``` 101 | 102 | If the call fails, e.g., because the address is malformed, a `GetBalanceError` is returned, 103 | indicating the reason for the failed call. 104 | 105 | The optional `min_confirmations` parameter can be used to limit the set of considered UTXOs 106 | for the calculation of the balance to those with at least the provided number of confirmations. 107 | 108 | === Send a Bitcoin Transaction 109 | 110 | Given a `SendTransactionRequest` containing the the raw bytes of a Bitcoin transaction, 111 | the transaction is forwarded to the Bitcoin network if it passes a set of validity checks. 112 | 113 | ``` 114 | type SendTransactionRequest = record { 115 | transaction: blob; 116 | }; 117 | 118 | type SendTransactionError = variant { 119 | MalformedTransaction; 120 | // More error types to be added here. 121 | }; 122 | 123 | send_transaction: (SendTransactionRequest) -> (variant { 124 | Ok : null; 125 | Err : opt SendTransactionError; 126 | }); 127 | ``` 128 | 129 | The following validity checks are performed: 130 | 131 | - The transaction is well-formed. 132 | - The transaction only consumes unspent outputs. 133 | - All signatures are correct. 134 | - There is a positive transaction fee. 135 | - The transaction does not create dust, i.e., an output that holds a smaller Bitcoin amount 136 | than it costs to spend the Bitcoin in the output. 137 | 138 | NOTE: The Bitcoin canister provided as part of the developer preview *only* checks that the 139 | transaction is well-formed. 140 | 141 | If at least one of these checks fails, a `SendTransactionError` is returned, 142 | indicating the reason for the failed call. 143 | 144 | The Bitcoin canister caches the transaction and periodically forwards the transaction 145 | until the transaction appears in a block or the transaction 146 | times out after 24 hours, at which point the transaction is removed from the cache. 147 | 148 | NOTE: The Bitcoin canister provided as part of the developer preview does *not* 149 | cache transactions. 150 | -------------------------------------------------------------------------------- /canister/build.rs: -------------------------------------------------------------------------------- 1 | use std::io::Result; 2 | fn main() -> Result<()> { 3 | prost_build::compile_protos(&["src/proto.proto"], &["src/"])?; 4 | #[cfg(feature = "tonic-build")] 5 | tonic_build::compile_protos("src/proto.proto")?; 6 | Ok(()) 7 | } 8 | -------------------------------------------------------------------------------- /canister/candid.did: -------------------------------------------------------------------------------- 1 | type Satoshi = nat64; 2 | 3 | type OutPoint = record { 4 | txid : blob; 5 | vout : nat32 6 | }; 7 | 8 | type Utxo = record { 9 | outpoint: OutPoint; 10 | value: Satoshi; 11 | height: nat32; 12 | confirmations: nat32; 13 | }; 14 | 15 | type GetUtxosRequest = record { 16 | address : text; 17 | min_confirmations: opt nat32; 18 | offset: opt nat32; 19 | }; 20 | 21 | type GetUtxosError = variant { 22 | MalformedAddress; 23 | // More error types to be added here. 24 | }; 25 | 26 | type GetBalanceRequest = record { 27 | address : text; 28 | min_confirmations: opt nat32; 29 | }; 30 | 31 | type GetBalanceError = variant { 32 | MalformedAddress; 33 | // More error types to be added here. 34 | }; 35 | 36 | type SendTransactionRequest = record { 37 | transaction: blob; 38 | }; 39 | 40 | type SendTransactionError = variant { 41 | MalformedTransaction; 42 | // More error types to be added here. 43 | }; 44 | 45 | service bitcoin : { 46 | 47 | get_balance: (GetBalanceRequest) -> (variant { 48 | Ok : Satoshi; 49 | Err: opt GetBalanceError; 50 | }); 51 | 52 | get_utxos: (GetUtxosRequest) -> (variant { 53 | Ok : record { 54 | utxos: vec Utxo; 55 | total_count: nat32; 56 | }; 57 | Err : opt GetUtxosError; 58 | }); 59 | 60 | send_transaction: (SendTransactionRequest) -> (variant { 61 | Ok : null; 62 | Err : opt SendTransactionError; 63 | }); 64 | } 65 | -------------------------------------------------------------------------------- /canister/src/adapter-shim.rs: -------------------------------------------------------------------------------- 1 | //! A shim to facilitate communication between the canister and the adapter. 2 | use btc::proto::{ 3 | btc_adapter_client::BtcAdapterClient, GetSuccessorsRequest, SendTransactionRequest, 4 | }; 5 | use candid::{Decode, Encode}; 6 | use ic_agent::{export::Principal, Agent}; 7 | use prost::Message; 8 | use std::env; 9 | use tonic::Request; 10 | 11 | mod proto { 12 | tonic::include_proto!("btc"); 13 | } 14 | 15 | #[tokio::main] 16 | async fn main() { 17 | let args: Vec = env::args().collect(); 18 | if args.len() < 2 { 19 | panic!("BTC Canister ID not specified"); 20 | } 21 | 22 | let btc_canister_id = 23 | Principal::from_text(args[1].clone()).expect(&format!("Invalid canister ID '{}'", args[1])); 24 | 25 | // An agent to connect to the local replica. 26 | let agent = Agent::builder() 27 | .with_url("http://127.0.0.1:8000") 28 | .build() 29 | .unwrap(); 30 | agent 31 | .fetch_root_key() 32 | .await 33 | .expect("Cannot connect to local replica. Are you sure it's running?"); 34 | 35 | let mut rpc_client = BtcAdapterClient::connect("http://127.0.0.1:34254") 36 | .await 37 | .unwrap(); 38 | 39 | let mut current_height = 1; 40 | let mut first: bool = true; 41 | 42 | loop { 43 | // Look up a `get_successors` request from the canister. 44 | let raw_request = { 45 | let req = agent 46 | .query(&btc_canister_id, "get_successors_request") 47 | .with_arg(&Encode!().unwrap()) 48 | .call() 49 | .await 50 | .unwrap(); 51 | Decode!(&req, Vec).unwrap() 52 | }; 53 | 54 | // Send the request to the adapter. 55 | let rpc_request = Request::new( 56 | GetSuccessorsRequest::decode(raw_request.as_slice()) 57 | .map_err(|err| err.to_string()) 58 | .unwrap(), 59 | ); 60 | 61 | match rpc_client.get_successors(rpc_request).await { 62 | Ok(response) => { 63 | // Read the response. 64 | let response_vec = response.into_inner().encode_to_vec(); 65 | 66 | // Send response to canister. 67 | let result = agent 68 | .update(&btc_canister_id, "get_successors_response") 69 | .with_arg(&Encode!(&response_vec).unwrap()) 70 | .call_and_wait(delay()) 71 | .await 72 | .unwrap(); 73 | 74 | let new_height = Decode!(&result, u32).unwrap(); 75 | if current_height == new_height { 76 | if first { 77 | first = false; 78 | println!("No new block received. Tip height: {}", current_height); 79 | } 80 | 81 | // Sleep for a second to not spam the adapter. 82 | std::thread::sleep(std::time::Duration::from_secs(1)); 83 | } else { 84 | first = true; 85 | println!("Processed new blocks. New height: {:?}", new_height); 86 | current_height = new_height; 87 | } 88 | } 89 | Err(err) => { 90 | println!("Error communicating with adapter: {:?}", err); 91 | } 92 | } 93 | 94 | // Are there any outgoing transactions to send? 95 | let has_outgoing_transaction = { 96 | let req = agent 97 | .query(&btc_canister_id, "has_outgoing_transaction") 98 | .with_arg(&Encode!().unwrap()) 99 | .call() 100 | .await 101 | .unwrap(); 102 | Decode!(&req, bool).unwrap() 103 | }; 104 | 105 | if !has_outgoing_transaction { 106 | continue; 107 | } 108 | 109 | // Look up outgoing transactions from the btc canister. 110 | let maybe_raw_tx = agent 111 | .update(&btc_canister_id, "get_outgoing_transaction") 112 | .with_arg(&Encode!().unwrap()) 113 | .call_and_wait(delay()) 114 | .await 115 | .map(|res| Decode!(&res, Option>).unwrap()) 116 | .unwrap(); 117 | 118 | if let Some(raw_tx) = maybe_raw_tx { 119 | println!("Sending tx to the adapter..."); 120 | 121 | let rpc_request = Request::new(SendTransactionRequest { raw_tx }); 122 | match rpc_client.send_transaction(rpc_request).await { 123 | Ok(_) => { 124 | println!("Done."); 125 | } 126 | Err(err) => { 127 | println!("Error sending transaction to adapter: {:?}", err); 128 | } 129 | } 130 | } 131 | 132 | // Sleep for a second to not spam the adapter. 133 | std::thread::sleep(std::time::Duration::from_secs(1)); 134 | } 135 | } 136 | 137 | fn delay() -> garcon::Delay { 138 | garcon::Delay::builder() 139 | .throttle(std::time::Duration::from_millis(500)) 140 | .timeout(std::time::Duration::from_secs(60 * 5)) 141 | .build() 142 | } 143 | -------------------------------------------------------------------------------- /canister/src/block.rs: -------------------------------------------------------------------------------- 1 | use crate::proto; 2 | use bitcoin::{ 3 | hashes::Hash, Block, BlockHash, BlockHeader, OutPoint, Script, Transaction, TxIn, TxMerkleNode, 4 | TxOut, Txid, 5 | }; 6 | 7 | /// Converts a `Block` into a protobuf struct. 8 | pub fn to_proto(block: &Block) -> proto::Block { 9 | proto::Block { 10 | header: Some(proto::BlockHeader { 11 | version: block.header.version, 12 | prev_blockhash: block.header.prev_blockhash.to_vec(), 13 | merkle_root: block.header.merkle_root.to_vec(), 14 | time: block.header.time, 15 | bits: block.header.bits, 16 | nonce: block.header.nonce, 17 | }), 18 | txdata: block 19 | .txdata 20 | .iter() 21 | .map(|t| proto::Transaction { 22 | version: t.version, 23 | lock_time: t.lock_time, 24 | input: t 25 | .input 26 | .iter() 27 | .map(|i| proto::TxIn { 28 | previous_output: Some(proto::OutPoint { 29 | txid: i.previous_output.txid.to_vec(), 30 | vout: i.previous_output.vout, 31 | }), 32 | script_sig: i.script_sig.to_bytes(), 33 | sequence: i.sequence, 34 | witness: i.witness.clone(), 35 | }) 36 | .collect(), 37 | output: t 38 | .output 39 | .iter() 40 | .map(|o| proto::TxOut { 41 | value: o.value, 42 | script_pubkey: o.script_pubkey.to_bytes(), 43 | }) 44 | .collect(), 45 | }) 46 | .collect(), 47 | } 48 | } 49 | 50 | /// Converts a protobuf block into a `Block`. 51 | pub fn from_proto(block: &proto::Block) -> Block { 52 | let header = block.header.as_ref().expect("Block header must exist"); 53 | 54 | Block { 55 | header: BlockHeader { 56 | version: header.version, 57 | prev_blockhash: BlockHash::from_hash(Hash::from_slice(&header.prev_blockhash).unwrap()), 58 | merkle_root: TxMerkleNode::from_hash(Hash::from_slice(&header.merkle_root).unwrap()), 59 | time: header.time, 60 | bits: header.bits, 61 | nonce: header.nonce, 62 | }, 63 | txdata: block 64 | .txdata 65 | .iter() 66 | .map(|t| Transaction { 67 | version: t.version, 68 | lock_time: t.lock_time, 69 | input: t 70 | .input 71 | .iter() 72 | .map(|i| { 73 | let prev_output = i.previous_output.as_ref().unwrap(); 74 | TxIn { 75 | previous_output: OutPoint::new( 76 | Txid::from_hash(Hash::from_slice(&prev_output.txid).unwrap()), 77 | prev_output.vout, 78 | ), 79 | script_sig: Script::from(i.script_sig.clone()), 80 | sequence: i.sequence, 81 | witness: i.witness.clone(), 82 | } 83 | }) 84 | .collect(), 85 | output: t 86 | .output 87 | .iter() 88 | .map(|o| TxOut { 89 | value: o.value, 90 | script_pubkey: Script::from(o.script_pubkey.clone()), 91 | }) 92 | .collect(), 93 | }) 94 | .collect(), 95 | } 96 | } 97 | 98 | #[cfg(test)] 99 | mod test { 100 | use super::*; 101 | use crate::test_builder::{BlockBuilder, TransactionBuilder}; 102 | 103 | #[test] 104 | fn to_from_proto() { 105 | // Generate random blocks and verify that serializing/deserializing is a noop. 106 | let genesis = BlockBuilder::genesis() 107 | .with_transaction(TransactionBuilder::coinbase().build()) 108 | .build(); 109 | assert_eq!(genesis, from_proto(&to_proto(&genesis))); 110 | 111 | for _ in 0..100 { 112 | let block = BlockBuilder::with_prev_header(genesis.header) 113 | .with_transaction(TransactionBuilder::coinbase().build()) 114 | .build(); 115 | assert_eq!(block, from_proto(&to_proto(&block))); 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /canister/src/blockforest.rs: -------------------------------------------------------------------------------- 1 | use crate::{block, proto}; 2 | use bitcoin::{Block, BlockHash}; 3 | 4 | /// A data structure for maintaining all unstable blocks. 5 | /// 6 | /// A block `b` is considered stable if: 7 | /// depth(block) ≥ delta 8 | /// ∀ b', height(b') = height(b): depth(b) - depth(b’) ≥ delta 9 | #[cfg_attr(test, derive(Debug, PartialEq))] 10 | pub struct BlockForest { 11 | delta: u64, 12 | trees: Vec, 13 | } 14 | 15 | impl BlockForest { 16 | pub fn new(delta: u64) -> Self { 17 | Self { 18 | delta, 19 | trees: vec![], 20 | } 21 | } 22 | 23 | /// Pop a block that is a successor to the `anchor` iff the block is stable. 24 | pub fn pop(&mut self, anchor: &BlockHash) -> Option { 25 | let (mut attached_trees, detached_trees): (Vec<_>, Vec<_>) = 26 | std::mem::take(&mut self.trees) 27 | .into_iter() 28 | .partition(|t| t.root().header.prev_blockhash == *anchor); 29 | 30 | // Sort the attached trees by depth. 31 | attached_trees.sort_by(|a, b| a.depth().partial_cmp(&b.depth()).unwrap()); 32 | 33 | match attached_trees.last() { 34 | Some(deepest_tree) => { 35 | if deepest_tree.depth() < self.delta { 36 | // Need a depth of at least >= delta 37 | self.trees = concat(attached_trees, detached_trees); 38 | return None; 39 | } 40 | 41 | if attached_trees.len() >= 2 { 42 | if let Some(second_deepest_tree) = attached_trees.get(attached_trees.len() - 2) 43 | { 44 | if deepest_tree.depth() - second_deepest_tree.depth() < self.delta { 45 | // Difference must be >= delta 46 | self.trees = concat(attached_trees, detached_trees); 47 | return None; 48 | } 49 | } 50 | } 51 | 52 | // The deepest tree is delta-stable. 53 | // Pop the root of the tree, remove all other attached_trees. 54 | let deepest_tree = attached_trees.pop().unwrap(); 55 | let (stable_block, subtrees) = deepest_tree.pop(); 56 | 57 | self.trees = concat(detached_trees, subtrees); 58 | Some(stable_block) 59 | } 60 | None => { 61 | self.trees = concat(attached_trees, detached_trees); 62 | None 63 | } 64 | } 65 | } 66 | 67 | /// Push a new block into the store. 68 | pub fn push(&mut self, mut block: Block) { 69 | let block_hash = block.block_hash(); 70 | let successor_tree = self.take_tree(&block_hash); 71 | 72 | for i in 0..self.trees.len() { 73 | match self.trees[i].extend(block) { 74 | Ok(()) => { 75 | if let Some(successor_tree) = successor_tree { 76 | let block = self.trees[i].find_mut(&block_hash).unwrap(); 77 | block.children.push(successor_tree); 78 | } 79 | 80 | return; 81 | } 82 | Err(BlockNotPartOfTreeError(block_)) => { 83 | block = block_; 84 | } 85 | } 86 | } 87 | 88 | let mut new_block_tree = BlockTree::new(block); 89 | if let Some(successor_tree) = successor_tree { 90 | new_block_tree.children.push(successor_tree); 91 | } 92 | self.trees.push(new_block_tree); 93 | } 94 | 95 | /// Returns the best guess on what the "current" blockchain is. 96 | /// 97 | /// The most likely chain to be "current", we hypothesize, is the longest 98 | /// chain of blocks with an "uncontested" tip. As in, there exists no other 99 | /// block at the same height as the tip. 100 | pub fn get_current_chain(&self, anchor: &BlockHash) -> BlockChain { 101 | // Get all the blockchains that extend the anchor. 102 | let blockchains: Vec = self 103 | .trees 104 | .iter() 105 | .filter(|t| t.root().header.prev_blockhash == *anchor) 106 | .map(|t| t.blockchains()) 107 | .flatten() 108 | .collect(); 109 | 110 | if blockchains.is_empty() { 111 | // No attached blockchains found. 112 | // NOTE: this if condition isn't strictly required, as the following code should 113 | // handle the empty case gracefully. Nonetheless, it's added out of paranoia. 114 | return vec![]; 115 | } 116 | 117 | // Find the length of the longest blockchain. 118 | let mut longest_blockchain_len = 0; 119 | for blockchain in blockchains.iter() { 120 | longest_blockchain_len = longest_blockchain_len.max(blockchain.len()); 121 | } 122 | 123 | // Get all the longest blockchains. 124 | let longest_blockchains: Vec = blockchains 125 | .into_iter() 126 | .filter(|bc| bc.len() == longest_blockchain_len) 127 | .collect(); 128 | 129 | let mut current_chain = vec![]; 130 | for height_idx in 0..longest_blockchain_len { 131 | // If all the blocks on the same height are identical, then this block is part of the 132 | // "current" chain. 133 | let block = longest_blockchains[0][height_idx]; 134 | let block_hash = block.block_hash(); 135 | 136 | for chain in longest_blockchains.iter().skip(1) { 137 | if chain[height_idx].block_hash() != block_hash { 138 | return current_chain; 139 | } 140 | } 141 | 142 | current_chain.push(block); 143 | } 144 | 145 | current_chain 146 | } 147 | 148 | fn take_tree(&mut self, root_block_hash: &BlockHash) -> Option { 149 | for i in 0..self.trees.len() { 150 | if &self.trees[i].root().header.prev_blockhash == root_block_hash { 151 | return Some(self.trees.remove(i)); 152 | } 153 | } 154 | None 155 | } 156 | 157 | pub fn get_blocks(&self) -> Vec<&Block> { 158 | self.trees 159 | .iter() 160 | .map(|t| t.blockchains()) 161 | .flatten() 162 | .flatten() 163 | .collect() 164 | } 165 | 166 | pub fn to_proto(&self) -> proto::BlockForest { 167 | proto::BlockForest { 168 | delta: self.delta, 169 | trees: self.trees.iter().map(|t| t.to_proto()).collect(), 170 | } 171 | } 172 | 173 | pub fn from_proto(block_forest_proto: proto::BlockForest) -> Self { 174 | Self { 175 | delta: block_forest_proto.delta, 176 | trees: block_forest_proto 177 | .trees 178 | .into_iter() 179 | .map(BlockTree::from_proto) 180 | .collect(), 181 | } 182 | } 183 | } 184 | 185 | // Maintains a tree of connected blocks. 186 | #[cfg_attr(test, derive(Debug, PartialEq))] 187 | struct BlockTree { 188 | root: Block, 189 | children: Vec, 190 | } 191 | 192 | type BlockChain<'a> = Vec<&'a Block>; 193 | 194 | // An error thrown when trying to add a block that is not part of the tree. 195 | // See `BlockTree.insert` for more information. 196 | #[derive(Debug)] 197 | struct BlockNotPartOfTreeError(pub Block); 198 | 199 | impl BlockTree { 200 | // Create a new `BlockTree` with the given block as its root. 201 | fn new(root: Block) -> Self { 202 | Self { 203 | root, 204 | children: vec![], 205 | } 206 | } 207 | 208 | // Extends the tree with the given block. 209 | // 210 | // Blocks can extend the tree in the following cases: 211 | // * The block is already present in the tree (no-op). 212 | // * The block is a successor of a block already in the tree. 213 | fn extend(&mut self, block: Block) -> Result<(), BlockNotPartOfTreeError> { 214 | if self.contains(&block) { 215 | // The block is already present in the tree. Nothing to do. 216 | return Ok(()); 217 | } 218 | 219 | // Check if the block is a successor to any of the blocks in the tree. 220 | match self.find_mut(&block.header.prev_blockhash) { 221 | Some(block_tree) => { 222 | assert_eq!(block_tree.root.block_hash(), block.header.prev_blockhash); 223 | // Add the block as a successor. 224 | block_tree.children.push(BlockTree::new(block)); 225 | Ok(()) 226 | } 227 | None => Err(BlockNotPartOfTreeError(block)), 228 | } 229 | } 230 | 231 | fn root(&self) -> &Block { 232 | &self.root 233 | } 234 | 235 | fn pop(self) -> (Block, Vec) { 236 | (self.root, self.children) 237 | } 238 | 239 | // Returns a `BlockTree` where the hash of the root block matches the provided `block_hash` 240 | // if it exists, and `None` otherwise. 241 | fn find_mut(&mut self, blockhash: &BlockHash) -> Option<&mut BlockTree> { 242 | if self.root.block_hash() == *blockhash { 243 | return Some(self); 244 | } 245 | 246 | for child in self.children.iter_mut() { 247 | if let res @ Some(_) = child.find_mut(blockhash) { 248 | return res; 249 | } 250 | } 251 | 252 | None 253 | } 254 | 255 | // Returns all the blockchains in the tree. 256 | fn blockchains(&self) -> Vec { 257 | if self.children.is_empty() { 258 | return vec![vec![&self.root]]; 259 | } 260 | 261 | let mut tips = vec![]; 262 | for child in self.children.iter() { 263 | tips.extend( 264 | child 265 | .blockchains() 266 | .into_iter() 267 | .map(|bc| concat(vec![&self.root], bc)) 268 | .collect::>(), 269 | ); 270 | } 271 | 272 | tips 273 | } 274 | 275 | fn depth(&self) -> u64 { 276 | if self.children.is_empty() { 277 | return 0; 278 | } 279 | 280 | let mut max_child_depth = 0; 281 | 282 | for child in self.children.iter() { 283 | max_child_depth = std::cmp::max(1 + child.depth(), max_child_depth); 284 | } 285 | 286 | max_child_depth 287 | } 288 | 289 | // Returns true if a block exists in the tree, false otherwise. 290 | fn contains(&self, block: &Block) -> bool { 291 | if self.root.block_hash() == block.block_hash() { 292 | return true; 293 | } 294 | 295 | for child in self.children.iter() { 296 | if child.contains(block) { 297 | return true; 298 | } 299 | } 300 | 301 | false 302 | } 303 | 304 | fn to_proto(&self) -> proto::BlockTree { 305 | proto::BlockTree { 306 | root: Some(block::to_proto(&self.root)), 307 | children: self.children.iter().map(|t| t.to_proto()).collect(), 308 | } 309 | } 310 | 311 | fn from_proto(block_tree_proto: proto::BlockTree) -> Self { 312 | Self { 313 | root: block::from_proto(&block_tree_proto.root.unwrap()), 314 | children: block_tree_proto 315 | .children 316 | .into_iter() 317 | .map(BlockTree::from_proto) 318 | .collect(), 319 | } 320 | } 321 | } 322 | 323 | fn concat(mut a: Vec, b: Vec) -> Vec { 324 | a.extend(b); 325 | a 326 | } 327 | 328 | #[cfg(test)] 329 | mod test { 330 | use super::*; 331 | use crate::test_builder::BlockBuilder; 332 | 333 | #[test] 334 | fn empty() { 335 | let block_0 = BlockBuilder::genesis().build(); 336 | 337 | let mut forest = BlockForest::new(1); 338 | assert_eq!(forest.pop(&block_0.block_hash()), None); 339 | } 340 | 341 | #[test] 342 | fn single_chain() { 343 | let block_0 = BlockBuilder::genesis().build(); 344 | let block_1 = BlockBuilder::with_prev_header(block_0.header).build(); 345 | let block_2 = BlockBuilder::with_prev_header(block_1.header).build(); 346 | 347 | let mut forest = BlockForest::new(1); 348 | 349 | forest.push(block_1.clone()); 350 | assert_eq!(forest.pop(&block_0.block_hash()), None); 351 | 352 | forest.push(block_2); 353 | assert_eq!(forest.pop(&block_0.block_hash()), Some(block_1)); 354 | } 355 | 356 | #[test] 357 | fn forks() { 358 | let genesis_block = BlockBuilder::genesis().build(); 359 | let fork_1 = BlockBuilder::with_prev_header(genesis_block.header).build(); 360 | let fork_2 = BlockBuilder::with_prev_header(genesis_block.header).build(); 361 | 362 | let mut forest = BlockForest::new(1); 363 | 364 | forest.push(fork_1); 365 | forest.push(fork_2.clone()); 366 | 367 | // Neither blocks are 1-stable, so we shouldn't get anything. 368 | assert_eq!(forest.pop(&genesis_block.block_hash()), None); 369 | 370 | // Extend fork2 by another block. 371 | forest.push(BlockBuilder::with_prev_header(fork_2.header).build()); 372 | 373 | // Now fork2 should be 1-stable. 374 | assert_eq!( 375 | forest.pop(&genesis_block.block_hash()), 376 | Some(fork_2.clone()) 377 | ); 378 | 379 | // No more 1-stable blocks 380 | assert_eq!(forest.pop(&fork_2.block_hash()), None); 381 | } 382 | 383 | // Test creating a forest that looks like this, where `i` is the successor of block `i - 1`: 384 | // 385 | // * -> 3 386 | // * -> 1 387 | // 388 | // And then we add "2". We expect the trees 1, 2, and 3 to all get merged. 389 | #[test] 390 | fn detached_blocks() { 391 | let block_0 = BlockBuilder::genesis().build(); 392 | let block_1 = BlockBuilder::with_prev_header(block_0.header).build(); 393 | let block_2 = BlockBuilder::with_prev_header(block_1.header).build(); 394 | let block_3 = BlockBuilder::with_prev_header(block_2.header).build(); 395 | 396 | let mut forest = BlockForest::new(1); 397 | 398 | forest.push(block_3); 399 | forest.push(block_1.clone()); 400 | assert_eq!(forest.pop(&block_0.block_hash()), None); 401 | 402 | // There are two trees in the forest. 403 | assert_eq!(forest.trees.len(), 2); 404 | 405 | // Add block2, which should result in all the blocks merging into a single tree. 406 | forest.push(block_2.clone()); 407 | assert_eq!(forest.trees.len(), 1); 408 | 409 | // Getting the blocks should work as expected. 410 | assert_eq!(forest.pop(&block_0.block_hash()), Some(block_1.clone())); 411 | assert_eq!(forest.pop(&block_1.block_hash()), Some(block_2.clone())); 412 | assert_eq!(forest.pop(&block_2.block_hash()), None); 413 | } 414 | 415 | // Test creating a forest that looks like this: 416 | // 417 | // * -> 3 418 | // * -> 0 419 | // 420 | // And then we add "1" and "2". All the trees should be merged into one. 421 | #[test] 422 | fn detached_blocks_2() { 423 | let block_0 = BlockBuilder::genesis().build(); 424 | let block_1 = BlockBuilder::with_prev_header(block_0.header).build(); 425 | let block_2 = BlockBuilder::with_prev_header(block_1.header).build(); 426 | let block_3 = BlockBuilder::with_prev_header(block_2.header).build(); 427 | 428 | let mut forest = BlockForest::new(1); 429 | 430 | forest.push(block_3); 431 | forest.push(block_0); 432 | forest.push(block_1); 433 | forest.push(block_2); 434 | 435 | // There is only one tree in the forest. 436 | assert_eq!(forest.trees.len(), 1); 437 | } 438 | 439 | #[test] 440 | fn insert_predecessor() { 441 | let block_0 = BlockBuilder::genesis().build(); 442 | let block_1 = BlockBuilder::with_prev_header(block_0.header).build(); 443 | 444 | let mut forest = BlockForest::new(1); 445 | 446 | forest.push(block_1); 447 | forest.push(block_0); 448 | 449 | // There is only one tree in the forest. 450 | assert_eq!(forest.trees.len(), 1); 451 | } 452 | 453 | #[test] 454 | fn insert_in_order() { 455 | let block_0 = BlockBuilder::genesis().build(); 456 | let block_1 = BlockBuilder::with_prev_header(block_0.header).build(); 457 | let block_2 = BlockBuilder::with_prev_header(block_1.header).build(); 458 | 459 | let mut forest = BlockForest::new(0); 460 | forest.push(block_1.clone()); 461 | forest.push(block_2.clone()); 462 | 463 | // There is only one tree in the forest. 464 | assert_eq!(forest.trees.len(), 1); 465 | assert_eq!(forest.pop(&block_0.block_hash()), Some(block_1.clone())); 466 | assert_eq!(forest.pop(&block_1.block_hash()), Some(block_2.clone())); 467 | assert_eq!(forest.pop(&block_2.block_hash()), None); 468 | } 469 | 470 | #[test] 471 | fn insert_in_reverse_order() { 472 | let block_0 = BlockBuilder::genesis().build(); 473 | let block_1 = BlockBuilder::with_prev_header(block_0.header).build(); 474 | let block_2 = BlockBuilder::with_prev_header(block_1.header).build(); 475 | 476 | let mut forest = BlockForest::new(0); 477 | forest.push(block_2.clone()); 478 | forest.push(block_1.clone()); 479 | 480 | // There is only one tree in the forest. 481 | assert_eq!(forest.trees.len(), 1); 482 | assert_eq!(forest.pop(&block_0.block_hash()), Some(block_1.clone())); 483 | assert_eq!(forest.pop(&block_1.block_hash()), Some(block_2.clone())); 484 | assert_eq!(forest.pop(&block_2.block_hash()), None); 485 | } 486 | 487 | #[test] 488 | fn tree_single_block() { 489 | let block_tree = BlockTree::new(BlockBuilder::genesis().build()); 490 | 491 | assert_eq!(block_tree.depth(), 0); 492 | assert_eq!(block_tree.blockchains(), vec![vec![&block_tree.root]]); 493 | } 494 | 495 | #[test] 496 | fn tree_multiple_forks() { 497 | let genesis_block = BlockBuilder::genesis().build(); 498 | let genesis_block_header = genesis_block.header; 499 | let mut block_tree = BlockTree::new(genesis_block); 500 | 501 | for i in 1..5 { 502 | // Create different blocks extending the genesis block. 503 | // Each one of these should be a separate fork. 504 | block_tree 505 | .extend(BlockBuilder::with_prev_header(genesis_block_header).build()) 506 | .unwrap(); 507 | assert_eq!(block_tree.blockchains().len(), i); 508 | } 509 | 510 | assert_eq!(block_tree.depth(), 1); 511 | } 512 | 513 | // Creating a forest that looks like this: 514 | // 515 | // * -> 1 -> 2 516 | // 517 | // Both blocks 1 and 2 are part of the current chain. 518 | #[test] 519 | fn get_current_chain_single_blockchain() { 520 | let block_0 = BlockBuilder::genesis().build(); 521 | let block_1 = BlockBuilder::with_prev_header(block_0.header).build(); 522 | let block_2 = BlockBuilder::with_prev_header(block_1.header).build(); 523 | 524 | let mut forest = BlockForest::new(1); 525 | 526 | forest.push(block_1.clone()); 527 | forest.push(block_2.clone()); 528 | assert_eq!( 529 | forest.get_current_chain(&block_0.block_hash()), 530 | vec![&block_1, &block_2] 531 | ); 532 | } 533 | 534 | // Creating a forest that looks like this: 535 | // 536 | // * -> 1 537 | // * -> 2 538 | // 539 | // Both blocks 1 and 2 contest with each other -> current chain is empty. 540 | #[test] 541 | fn get_current_chain_two_contesting_trees() { 542 | let block_0 = BlockBuilder::genesis().build(); 543 | let block_1 = BlockBuilder::with_prev_header(block_0.header).build(); 544 | let block_2 = BlockBuilder::with_prev_header(block_0.header).build(); 545 | 546 | let mut forest = BlockForest::new(1); 547 | 548 | forest.push(block_1); 549 | forest.push(block_2); 550 | assert_eq!( 551 | forest.get_current_chain(&block_0.block_hash()), 552 | Vec::<&Block>::new() 553 | ); 554 | } 555 | 556 | // Creating the following forest: 557 | // 558 | // * -> 1 559 | // * -> 2 -> 3 560 | // 561 | // "2 -> 3" is the longest blockchain and is should be considered "current". 562 | #[test] 563 | fn get_current_chain_longer_fork() { 564 | let block_0 = BlockBuilder::genesis().build(); 565 | let block_1 = BlockBuilder::with_prev_header(block_0.header).build(); 566 | let block_2 = BlockBuilder::with_prev_header(block_0.header).build(); 567 | let block_3 = BlockBuilder::with_prev_header(block_2.header).build(); 568 | 569 | let mut forest = BlockForest::new(1); 570 | 571 | forest.push(block_1); 572 | forest.push(block_2.clone()); 573 | forest.push(block_3.clone()); 574 | assert_eq!( 575 | forest.get_current_chain(&block_0.block_hash()), 576 | vec![&block_2, &block_3] 577 | ); 578 | } 579 | 580 | // Creating the following forest: 581 | // 582 | // * -> 1 -> 2 -> 3 583 | // \-> a -> b 584 | // 585 | // "1" should be returned in this case, as its the longest chain 586 | // without a contested tip. 587 | #[test] 588 | fn get_current_chain_fork_at_first_block() { 589 | let block_0 = BlockBuilder::genesis().build(); 590 | let block_1 = BlockBuilder::with_prev_header(block_0.header).build(); 591 | let block_2 = BlockBuilder::with_prev_header(block_1.header).build(); 592 | let block_3 = BlockBuilder::with_prev_header(block_2.header).build(); 593 | let block_a = BlockBuilder::with_prev_header(block_1.header).build(); 594 | let block_b = BlockBuilder::with_prev_header(block_a.header).build(); 595 | 596 | let mut forest = BlockForest::new(1); 597 | 598 | forest.push(block_1.clone()); 599 | forest.push(block_2); 600 | forest.push(block_3); 601 | forest.push(block_a); 602 | forest.push(block_b); 603 | assert_eq!( 604 | forest.get_current_chain(&block_0.block_hash()), 605 | vec![&block_1] 606 | ); 607 | } 608 | 609 | // Creating the following forest: 610 | // 611 | // * -> 1 -> 2 -> 3 612 | // \-> a -> b 613 | // -> x -> y -> z 614 | // 615 | // All blocks are contested. 616 | // 617 | // Then add block `c` that extends block `b`, at that point 618 | // `1 -> a -> b -> c` becomes the only longest chain, and therefore 619 | // the "current" chain. 620 | #[test] 621 | fn get_current_chain_multiple_forks() { 622 | let block_0 = BlockBuilder::genesis().build(); 623 | let block_1 = BlockBuilder::with_prev_header(block_0.header).build(); 624 | let block_2 = BlockBuilder::with_prev_header(block_1.header).build(); 625 | let block_3 = BlockBuilder::with_prev_header(block_2.header).build(); 626 | let block_a = BlockBuilder::with_prev_header(block_1.header).build(); 627 | let block_b = BlockBuilder::with_prev_header(block_a.header).build(); 628 | let block_x = BlockBuilder::with_prev_header(block_0.header).build(); 629 | let block_y = BlockBuilder::with_prev_header(block_x.header).build(); 630 | let block_z = BlockBuilder::with_prev_header(block_y.header).build(); 631 | 632 | let mut forest = BlockForest::new(1); 633 | 634 | forest.push(block_x); 635 | forest.push(block_y); 636 | forest.push(block_z); 637 | forest.push(block_1.clone()); 638 | forest.push(block_2); 639 | forest.push(block_3); 640 | forest.push(block_a.clone()); 641 | forest.push(block_b.clone()); 642 | assert_eq!( 643 | forest.get_current_chain(&block_0.block_hash()), 644 | Vec::<&Block>::new() 645 | ); 646 | 647 | // Now add block c to b. 648 | let block_c = BlockBuilder::with_prev_header(block_b.header).build(); 649 | forest.push(block_c.clone()); 650 | 651 | // Now the current chain should be "1 -> a -> b -> c" 652 | assert_eq!( 653 | forest.get_current_chain(&block_0.block_hash()), 654 | vec![&block_1, &block_a, &block_b, &block_c] 655 | ); 656 | } 657 | 658 | // Same as the above test, with a different insertion order. 659 | #[test] 660 | fn get_current_chain_multiple_forks_2() { 661 | let block_0 = BlockBuilder::genesis().build(); 662 | let block_1 = BlockBuilder::with_prev_header(block_0.header).build(); 663 | let block_2 = BlockBuilder::with_prev_header(block_1.header).build(); 664 | let block_3 = BlockBuilder::with_prev_header(block_2.header).build(); 665 | let block_a = BlockBuilder::with_prev_header(block_1.header).build(); 666 | let block_b = BlockBuilder::with_prev_header(block_a.header).build(); 667 | let block_x = BlockBuilder::with_prev_header(block_0.header).build(); 668 | let block_y = BlockBuilder::with_prev_header(block_x.header).build(); 669 | let block_z = BlockBuilder::with_prev_header(block_y.header).build(); 670 | 671 | let mut forest = BlockForest::new(1); 672 | 673 | forest.push(block_1); 674 | forest.push(block_2); 675 | forest.push(block_3); 676 | forest.push(block_a); 677 | forest.push(block_b); 678 | forest.push(block_x); 679 | forest.push(block_y); 680 | forest.push(block_z); 681 | assert_eq!( 682 | forest.get_current_chain(&block_0.block_hash()), 683 | Vec::<&Block>::new() 684 | ); 685 | } 686 | 687 | #[test] 688 | fn get_current_chain_empty() { 689 | let block_0 = BlockBuilder::genesis().build(); 690 | let forest = BlockForest::new(1); 691 | 692 | assert_eq!( 693 | forest.get_current_chain(&block_0.block_hash()), 694 | Vec::<&Block>::new() 695 | ); 696 | } 697 | } 698 | -------------------------------------------------------------------------------- /canister/src/candid_types.rs: -------------------------------------------------------------------------------- 1 | //! Types used to support the candid API. 2 | use bitcoin::Network as BitcoinNetwork; 3 | use ic_cdk::export::candid::{CandidType, Deserialize}; 4 | 5 | /// The payload used to initialize the canister. 6 | #[derive(CandidType, Deserialize)] 7 | pub struct InitPayload { 8 | pub delta: u64, 9 | pub network: Network, 10 | } 11 | 12 | /// The supported Bitcoin networks. 13 | /// 14 | /// Note that this is identical to `Network` that's defined in the Bitcoin 15 | /// crate, with the only difference being that it derives a `CandidType`. 16 | #[derive(CandidType, Deserialize, Copy, Clone)] 17 | pub enum Network { 18 | Bitcoin, 19 | Regtest, 20 | Testnet, 21 | Signet, 22 | } 23 | 24 | impl From for BitcoinNetwork { 25 | fn from(network: Network) -> Self { 26 | match network { 27 | Network::Bitcoin => Self::Bitcoin, 28 | Network::Testnet => Self::Testnet, 29 | Network::Signet => Self::Signet, 30 | Network::Regtest => Self::Regtest, 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /canister/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod block; 2 | mod blockforest; 3 | pub mod store; 4 | pub mod test_builder; 5 | mod utxoset; 6 | 7 | pub mod proto { 8 | include!(concat!(env!("OUT_DIR"), "/btc.rs")); 9 | } 10 | -------------------------------------------------------------------------------- /canister/src/main.rs: -------------------------------------------------------------------------------- 1 | use bitcoin::{ 2 | blockdata::constants::genesis_block, util::psbt::serialize::Deserialize, Address, Network, 3 | Transaction, 4 | }; 5 | use btc::{ 6 | proto::{GetSuccessorsRequest, GetSuccessorsResponse}, 7 | store::State, 8 | }; 9 | use ic_btc_types::{ 10 | GetBalanceError, GetBalanceRequest, GetUtxosError, GetUtxosRequest, GetUtxosResponse, OutPoint, 11 | SendTransactionError, SendTransactionRequest, Utxo, 12 | }; 13 | use ic_cdk::api::print; 14 | use ic_cdk::export::candid::candid_method; 15 | use ic_cdk_macros::{query, update}; 16 | use prost::Message; 17 | use std::{cell::RefCell, collections::VecDeque, str::FromStr}; 18 | 19 | thread_local! { 20 | // Initialize the canister to expect blocks from the Regtest network. 21 | static STATE: RefCell = RefCell::new(State::new(1, Network::Regtest, genesis_block(Network::Regtest))); 22 | // A queue of transactions awaiting to be sent. 23 | static OUTGOING_TRANSACTIONS: RefCell>> = RefCell::new(VecDeque::new()); 24 | } 25 | 26 | // Retrieves the balance of the given Bitcoin address. 27 | // 28 | // NOTE: While this endpoint could've been a query, it is exposed as an update call 29 | // for security reasons. 30 | #[update] 31 | #[candid_method(update)] 32 | fn get_balance(request: GetBalanceRequest) -> Result { 33 | if Address::from_str(&request.address).is_err() { 34 | return Err(GetBalanceError::MalformedAddress); 35 | } 36 | 37 | let min_confirmations = request.min_confirmations.unwrap_or(0); 38 | 39 | Ok(STATE.with(|s| s.borrow().get_balance(&request.address, min_confirmations))) 40 | } 41 | 42 | #[update] 43 | #[candid_method(update)] 44 | fn get_utxos(request: GetUtxosRequest) -> Result { 45 | if Address::from_str(&request.address).is_err() { 46 | return Err(GetUtxosError::MalformedAddress); 47 | } 48 | 49 | let min_confirmations = request.min_confirmations.unwrap_or(0); 50 | 51 | STATE.with(|s| { 52 | let main_chain_height = s.borrow().main_chain_height(); 53 | 54 | let utxos: Vec = s 55 | .borrow() 56 | .get_utxos(&request.address, min_confirmations) 57 | .into_iter() 58 | .map(|(outpoint, txout, height)| Utxo { 59 | outpoint: OutPoint { 60 | txid: outpoint.txid.to_vec(), 61 | vout: outpoint.vout, 62 | }, 63 | value: txout.value, 64 | height, 65 | confirmations: main_chain_height - height + 1, 66 | }) 67 | .collect(); 68 | 69 | Ok(GetUtxosResponse { 70 | total_count: utxos.len() as u32, 71 | utxos, 72 | }) 73 | }) 74 | } 75 | 76 | #[update] 77 | #[candid_method(update)] 78 | fn send_transaction(request: SendTransactionRequest) -> Result<(), SendTransactionError> { 79 | if Transaction::deserialize(&request.transaction).is_err() { 80 | return Err(SendTransactionError::MalformedTransaction); 81 | } 82 | 83 | // NOTE: In the final release, transactions will be cached for up to 24 hours and 84 | // occasionally resent to the network until the transaction is observed in a block. 85 | 86 | OUTGOING_TRANSACTIONS.with(|txs| { 87 | txs.borrow_mut().push_back(request.transaction); 88 | }); 89 | 90 | Ok(()) 91 | } 92 | 93 | // Below are helper methods used by the adapter shim. They will not be included in the main 94 | // release. 95 | 96 | // Retrieves a `GetSuccessorsRequest` to send to the adapter. 97 | #[query] 98 | fn get_successors_request() -> Vec { 99 | let block_hashes = STATE.with(|state| { 100 | let state = state.borrow(); 101 | let mut block_hashes: Vec> = state 102 | .get_unstable_blocks() 103 | .iter() 104 | .map(|b| b.block_hash().to_vec()) 105 | .collect(); 106 | 107 | block_hashes.push(state.anchor_hash().to_vec()); 108 | block_hashes 109 | }); 110 | 111 | print(format!("block hashes: {:?}", block_hashes)); 112 | GetSuccessorsRequest { block_hashes }.encode_to_vec() 113 | } 114 | 115 | #[query] 116 | fn has_outgoing_transaction() -> bool { 117 | OUTGOING_TRANSACTIONS.with(|txs| !txs.borrow_mut().is_empty()) 118 | } 119 | 120 | // Retrieve a raw tx to send to the network 121 | #[update] 122 | fn get_outgoing_transaction() -> Option> { 123 | OUTGOING_TRANSACTIONS.with(|txs| txs.borrow_mut().pop_front()) 124 | } 125 | 126 | // Process a (binary) `GetSuccessorsResponse` received from the adapter. 127 | // Returns the height of the chain after the response is processed. 128 | #[update] 129 | fn get_successors_response(response_vec: Vec) -> u32 { 130 | let response = GetSuccessorsResponse::decode(&*response_vec).unwrap(); 131 | 132 | for block_proto in response.blocks { 133 | let block = btc::block::from_proto(&block_proto); 134 | print(&format!( 135 | "Processing block with hash: {}", 136 | block.block_hash() 137 | )); 138 | 139 | STATE.with(|state| { 140 | state.borrow_mut().insert_block(block); 141 | }); 142 | } 143 | 144 | STATE.with(|state| state.borrow().main_chain_height()) 145 | } 146 | 147 | fn main() {} 148 | 149 | #[cfg(test)] 150 | mod test { 151 | use super::*; 152 | use bitcoin::secp256k1::rand::rngs::OsRng; 153 | use bitcoin::secp256k1::Secp256k1; 154 | use bitcoin::{Address, PublicKey}; 155 | use btc::test_builder::{BlockBuilder, TransactionBuilder}; 156 | 157 | #[test] 158 | fn check_candid_interface_compatibility() { 159 | use candid::types::subtype::{subtype, Gamma}; 160 | use candid::types::Type; 161 | use ic_cdk::export::candid::{self}; 162 | use std::io::Write; 163 | use std::path::PathBuf; 164 | 165 | candid::export_service!(); 166 | 167 | let actual_interface = __export_service(); 168 | println!("Generated DID:\n {}", actual_interface); 169 | let mut tmp = tempfile::NamedTempFile::new().expect("failed to create a temporary file"); 170 | write!(tmp, "{}", actual_interface).expect("failed to write interface to a temporary file"); 171 | let (mut env1, t1) = 172 | candid::pretty_check_file(tmp.path()).expect("failed to check generated candid file"); 173 | let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("candid.did"); 174 | let (env2, t2) = 175 | candid::pretty_check_file(path.as_path()).expect("failed to open candid.did file"); 176 | 177 | let (t1_ref, t2) = match (t1.as_ref().unwrap(), t2.unwrap()) { 178 | (Type::Class(_, s1), Type::Class(_, s2)) => (s1.as_ref(), *s2), 179 | (Type::Class(_, s1), s2 @ Type::Service(_)) => (s1.as_ref(), s2), 180 | (s1 @ Type::Service(_), Type::Class(_, s2)) => (s1, *s2), 181 | (t1, t2) => (t1, t2), 182 | }; 183 | 184 | let mut gamma = Gamma::new(); 185 | let t2 = env1.merge_type(env2, t2); 186 | subtype(&mut gamma, &env1, t1_ref, &t2) 187 | .expect("bitcoin canister interface is not compatible with the candid.did file"); 188 | } 189 | 190 | #[test] 191 | fn get_utxos_from_existing_utxo_set() { 192 | for network in [ 193 | Network::Bitcoin, 194 | Network::Regtest, 195 | Network::Testnet, 196 | Network::Signet, 197 | ] 198 | .iter() 199 | { 200 | // Generate an address. 201 | let address = { 202 | let secp = Secp256k1::new(); 203 | let mut rng = OsRng::new().unwrap(); 204 | Address::p2pkh(&PublicKey::new(secp.generate_keypair(&mut rng).1), *network) 205 | }; 206 | 207 | // Create a genesis block where 1000 satoshis are given to the address. 208 | let coinbase_tx = TransactionBuilder::coinbase() 209 | .with_output(&address, 1000) 210 | .build(); 211 | let genesis_block = BlockBuilder::genesis() 212 | .with_transaction(coinbase_tx.clone()) 213 | .build(); 214 | 215 | // Set the state. 216 | STATE.with(|s| s.replace(State::new(0, *network, genesis_block))); 217 | 218 | assert_eq!( 219 | get_utxos(GetUtxosRequest { 220 | address: address.to_string(), 221 | min_confirmations: None 222 | }), 223 | Ok(GetUtxosResponse { 224 | utxos: vec![Utxo { 225 | outpoint: OutPoint { 226 | txid: coinbase_tx.txid().to_vec(), 227 | vout: 0 228 | }, 229 | value: 1000, 230 | height: 1, 231 | confirmations: 1 232 | }], 233 | total_count: 1 234 | }) 235 | ); 236 | } 237 | } 238 | 239 | #[test] 240 | fn get_balance_malformed_address() { 241 | assert_eq!( 242 | get_balance(GetBalanceRequest { 243 | address: String::from("not an address"), 244 | min_confirmations: None 245 | }), 246 | Err(GetBalanceError::MalformedAddress) 247 | ); 248 | } 249 | 250 | #[test] 251 | fn get_utxos_malformed_address() { 252 | assert_eq!( 253 | get_utxos(GetUtxosRequest { 254 | address: String::from("not an address"), 255 | min_confirmations: None 256 | }), 257 | Err(GetUtxosError::MalformedAddress) 258 | ); 259 | } 260 | 261 | #[test] 262 | fn get_balance_test() { 263 | for network in [ 264 | Network::Bitcoin, 265 | Network::Regtest, 266 | Network::Testnet, 267 | Network::Signet, 268 | ] 269 | .iter() 270 | { 271 | // Generate addresses. 272 | let address_1 = { 273 | let secp = Secp256k1::new(); 274 | let mut rng = OsRng::new().unwrap(); 275 | Address::p2pkh(&PublicKey::new(secp.generate_keypair(&mut rng).1), *network) 276 | }; 277 | 278 | let address_2 = { 279 | let secp = Secp256k1::new(); 280 | let mut rng = OsRng::new().unwrap(); 281 | Address::p2pkh(&PublicKey::new(secp.generate_keypair(&mut rng).1), *network) 282 | }; 283 | 284 | // Create a genesis block where 1000 satoshis are given to the address_1, followed 285 | // by a block where address_1 gives 1000 satoshis to address_2. 286 | let coinbase_tx = TransactionBuilder::coinbase() 287 | .with_output(&address_1, 1000) 288 | .build(); 289 | let block_0 = BlockBuilder::genesis() 290 | .with_transaction(coinbase_tx.clone()) 291 | .build(); 292 | let tx = TransactionBuilder::with_input(bitcoin::OutPoint::new(coinbase_tx.txid(), 0)) 293 | .with_output(&address_2, 1000) 294 | .build(); 295 | let block_1 = BlockBuilder::with_prev_header(block_0.header) 296 | .with_transaction(tx.clone()) 297 | .build(); 298 | 299 | // Set the state. 300 | STATE.with(|s| { 301 | s.replace(State::new(2, *network, block_0)); 302 | s.borrow_mut().insert_block(block_1); 303 | }); 304 | 305 | // With up to one confirmation, expect address 2 to have a balance 1000, and 306 | // address 1 to have a balance of 0. 307 | for min_confirmations in [None, Some(0), Some(1)].iter() { 308 | assert_eq!( 309 | get_balance(GetBalanceRequest { 310 | address: address_2.to_string(), 311 | min_confirmations: *min_confirmations 312 | }), 313 | Ok(1000) 314 | ); 315 | 316 | assert_eq!( 317 | get_balance(GetBalanceRequest { 318 | address: address_1.to_string(), 319 | min_confirmations: *min_confirmations 320 | }), 321 | Ok(0) 322 | ); 323 | } 324 | 325 | // With two confirmations, expect address 2 to have a balance of 0, and address 1 to 326 | // have a balance of 1000. 327 | assert_eq!( 328 | get_balance(GetBalanceRequest { 329 | address: address_2.to_string(), 330 | min_confirmations: Some(2) 331 | }), 332 | Ok(0) 333 | ); 334 | assert_eq!( 335 | get_balance(GetBalanceRequest { 336 | address: address_1.to_string(), 337 | min_confirmations: Some(2) 338 | }), 339 | Ok(1000) 340 | ); 341 | 342 | // With >= 2 confirmations, both addresses should have an empty UTXO set. 343 | for i in 3..10 { 344 | assert_eq!( 345 | get_balance(GetBalanceRequest { 346 | address: address_2.to_string(), 347 | min_confirmations: Some(i) 348 | }), 349 | Ok(0) 350 | ); 351 | assert_eq!( 352 | get_balance(GetBalanceRequest { 353 | address: address_1.to_string(), 354 | min_confirmations: Some(i) 355 | }), 356 | Ok(0) 357 | ); 358 | } 359 | } 360 | } 361 | 362 | #[test] 363 | fn get_utxos_min_confirmations() { 364 | for network in [ 365 | Network::Bitcoin, 366 | Network::Regtest, 367 | Network::Testnet, 368 | Network::Signet, 369 | ] 370 | .iter() 371 | { 372 | // Generate addresses. 373 | let address_1 = { 374 | let secp = Secp256k1::new(); 375 | let mut rng = OsRng::new().unwrap(); 376 | Address::p2pkh(&PublicKey::new(secp.generate_keypair(&mut rng).1), *network) 377 | }; 378 | 379 | let address_2 = { 380 | let secp = Secp256k1::new(); 381 | let mut rng = OsRng::new().unwrap(); 382 | Address::p2pkh(&PublicKey::new(secp.generate_keypair(&mut rng).1), *network) 383 | }; 384 | 385 | // Create a genesis block where 1000 satoshis are given to the address_1, followed 386 | // by a block where address_1 gives 1000 satoshis to address_2. 387 | let coinbase_tx = TransactionBuilder::coinbase() 388 | .with_output(&address_1, 1000) 389 | .build(); 390 | let block_0 = BlockBuilder::genesis() 391 | .with_transaction(coinbase_tx.clone()) 392 | .build(); 393 | let tx = TransactionBuilder::with_input(bitcoin::OutPoint::new(coinbase_tx.txid(), 0)) 394 | .with_output(&address_2, 1000) 395 | .build(); 396 | let block_1 = BlockBuilder::with_prev_header(block_0.header) 397 | .with_transaction(tx.clone()) 398 | .build(); 399 | 400 | // Set the state. 401 | STATE.with(|s| { 402 | s.replace(State::new(2, *network, block_0)); 403 | s.borrow_mut().insert_block(block_1); 404 | }); 405 | 406 | // With up to one confirmation, expect address 2 to have one UTXO, and 407 | // address 1 to have no UTXOs. 408 | for min_confirmations in [None, Some(0), Some(1)].iter() { 409 | assert_eq!( 410 | get_utxos(GetUtxosRequest { 411 | address: address_2.to_string(), 412 | min_confirmations: *min_confirmations 413 | }), 414 | Ok(GetUtxosResponse { 415 | utxos: vec![Utxo { 416 | outpoint: OutPoint { 417 | txid: tx.txid().to_vec(), 418 | vout: 0, 419 | }, 420 | value: 1000, 421 | height: 2, 422 | confirmations: 1, 423 | }], 424 | total_count: 1 425 | }) 426 | ); 427 | 428 | assert_eq!( 429 | get_utxos(GetUtxosRequest { 430 | address: address_1.to_string(), 431 | min_confirmations: *min_confirmations 432 | }), 433 | Ok(GetUtxosResponse { 434 | utxos: vec![], 435 | total_count: 0 436 | }) 437 | ); 438 | } 439 | 440 | // With two confirmations, expect address 2 to have no UTXOs, and address 1 to 441 | // have one UTXO. 442 | assert_eq!( 443 | get_utxos(GetUtxosRequest { 444 | address: address_2.to_string(), 445 | min_confirmations: Some(2) 446 | }), 447 | Ok(GetUtxosResponse { 448 | utxos: vec![], 449 | total_count: 0 450 | }) 451 | ); 452 | assert_eq!( 453 | get_utxos(GetUtxosRequest { 454 | address: address_1.to_string(), 455 | min_confirmations: Some(2) 456 | }), 457 | Ok(GetUtxosResponse { 458 | utxos: vec![Utxo { 459 | outpoint: OutPoint { 460 | txid: coinbase_tx.txid().to_vec(), 461 | vout: 0, 462 | }, 463 | value: 1000, 464 | height: 1, 465 | confirmations: 2, 466 | }], 467 | total_count: 1 468 | }) 469 | ); 470 | 471 | // With >= 2 confirmations, both addresses should have an empty UTXO set. 472 | for i in 3..10 { 473 | assert_eq!( 474 | get_utxos(GetUtxosRequest { 475 | address: address_2.to_string(), 476 | min_confirmations: Some(i) 477 | }), 478 | Ok(GetUtxosResponse { 479 | utxos: vec![], 480 | total_count: 0 481 | }) 482 | ); 483 | assert_eq!( 484 | get_utxos(GetUtxosRequest { 485 | address: address_1.to_string(), 486 | min_confirmations: Some(i) 487 | }), 488 | Ok(GetUtxosResponse { 489 | utxos: vec![], 490 | total_count: 0 491 | }) 492 | ); 493 | } 494 | } 495 | } 496 | 497 | #[test] 498 | fn malformed_transaction() { 499 | assert_eq!( 500 | send_transaction(SendTransactionRequest { 501 | transaction: vec![1, 2, 3], 502 | }), 503 | Err(SendTransactionError::MalformedTransaction) 504 | ); 505 | } 506 | } 507 | -------------------------------------------------------------------------------- /canister/src/proto.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package btc; 4 | 5 | enum Network { 6 | BITCOIN = 0; 7 | TESTNET = 1; 8 | SIGNET = 2; 9 | REGTEST = 3; 10 | } 11 | 12 | message State { 13 | uint32 height = 1; 14 | bytes latest_stable_block_hash = 2; 15 | UtxoSet utxos = 3; 16 | BlockForest unstable_blocks = 4; 17 | } 18 | 19 | message UtxoSet { 20 | repeated Utxo utxos = 1; 21 | bool strict = 2; 22 | Network network = 3; 23 | } 24 | 25 | message Utxo { 26 | OutPoint outpoint = 1; 27 | TxOut txout = 2; 28 | uint32 height = 3; 29 | } 30 | 31 | message BlockForest { 32 | uint64 delta = 1; 33 | repeated BlockTree trees = 2; 34 | } 35 | 36 | message BlockTree { 37 | Block root = 1; 38 | repeated BlockTree children = 2; 39 | } 40 | 41 | message Block { 42 | BlockHeader header = 1; 43 | repeated Transaction txdata = 2; 44 | } 45 | 46 | message BlockHeader { 47 | int32 version = 1; 48 | bytes prev_blockhash = 2; 49 | bytes merkle_root = 3; 50 | uint32 time = 4; 51 | uint32 bits = 5; 52 | uint32 nonce = 6; 53 | } 54 | 55 | message Transaction { 56 | int32 version = 1; 57 | uint32 lock_time = 2; 58 | repeated TxIn input = 3; 59 | repeated TxOut output = 4; 60 | } 61 | 62 | message TxIn { 63 | OutPoint previous_output = 1; 64 | bytes script_sig = 2; 65 | uint32 sequence = 3; 66 | repeated bytes witness = 4; 67 | } 68 | 69 | message TxOut { 70 | uint64 value = 1; 71 | bytes script_pubkey = 2; 72 | } 73 | 74 | message OutPoint { 75 | bytes txid = 1; 76 | uint32 vout = 2; 77 | } 78 | 79 | message GetSuccessorsRequest { 80 | repeated bytes block_hashes = 1; 81 | } 82 | 83 | message GetSuccessorsResponse { 84 | repeated Block blocks = 1; 85 | } 86 | 87 | message SendTransactionRequest { 88 | bytes raw_tx = 1; 89 | } 90 | 91 | message SendTransactionResponse {} 92 | 93 | service BtcAdapter { 94 | rpc GetSuccessors(GetSuccessorsRequest) returns (GetSuccessorsResponse); 95 | rpc SendTransaction(SendTransactionRequest) returns (SendTransactionResponse); 96 | } 97 | -------------------------------------------------------------------------------- /canister/src/store.rs: -------------------------------------------------------------------------------- 1 | use crate::{blockforest::BlockForest, proto, utxoset::UtxoSet}; 2 | use bitcoin::hashes::Hash; 3 | use bitcoin::{Block, BlockHash, Network, OutPoint, TxOut, Txid}; 4 | use lazy_static::lazy_static; 5 | use std::collections::HashSet; 6 | use std::str::FromStr; 7 | 8 | lazy_static! { 9 | static ref DUPLICATE_TX_IDS: [Txid; 2] = [ 10 | Txid::from_str("d5d27987d2a3dfc724e359870c6644b40e497bdc0589a033220fe15429d88599").unwrap(), 11 | Txid::from_str("e3bf3d07d4b0375638d5f1db5255fe07ba2c4cb067cd81b84ee974b6585fb468").unwrap() 12 | ]; 13 | } 14 | 15 | type Height = u32; 16 | type Satoshi = u64; 17 | 18 | // A structure used to maintain the entire state. 19 | #[cfg_attr(test, derive(Debug, PartialEq))] 20 | pub struct State { 21 | // The height of the latest block marked as stable. 22 | height: Height, 23 | 24 | // The hash of the latest stable block. 25 | latest_stable_block_hash: BlockHash, 26 | 27 | // The UTXOs of all stable blocks since genesis. 28 | utxos: UtxoSet, 29 | 30 | // Blocks inserted, but are not considered stable yet. 31 | unstable_blocks: BlockForest, 32 | } 33 | 34 | impl State { 35 | /// Create a new blockchain. 36 | /// 37 | /// The `delta` parameter specifies how many confirmations a block needs before 38 | /// it is considered stable. Stable blocks are assumed to be final and are never 39 | /// removed. 40 | pub fn new(delta: u64, network: Network, genesis_block: Block) -> Self { 41 | let mut state = Self { 42 | height: 1, 43 | latest_stable_block_hash: genesis_block.block_hash(), 44 | utxos: UtxoSet::new(true, network), 45 | unstable_blocks: BlockForest::new(delta), 46 | }; 47 | 48 | // Process the txs in the genesis block to include them in the UTXOs. 49 | for tx in genesis_block.txdata { 50 | state.utxos.insert_tx(&tx, 1); 51 | } 52 | 53 | state 54 | } 55 | 56 | /// Returns the balance of a bitcoin address. 57 | pub fn get_balance(&self, address: &str, min_confirmations: u32) -> Satoshi { 58 | // NOTE: It is safe to sum up the balances here without the risk of overflow. 59 | // The maximum number of bitcoins is 2.1 * 10^7, which is 2.1* 10^15 satoshis. 60 | // That is well below the max value of a `u64`. 61 | let mut balance = 0; 62 | for (_, output, _) in self.get_utxos(address, min_confirmations) { 63 | balance += output.value; 64 | } 65 | 66 | balance 67 | } 68 | 69 | /// Returns the set of UTXOs for a given bitcoin address. 70 | /// Transactions with confirmations < `min_confirmations` are not considered. 71 | pub fn get_utxos( 72 | &self, 73 | address: &str, 74 | min_confirmations: u32, 75 | ) -> HashSet<(OutPoint, TxOut, Height)> { 76 | let mut address_utxos = self.utxos.get_utxos(address); 77 | 78 | // Apply unstable blocks to the UTXO set. 79 | for (i, block) in self 80 | .unstable_blocks 81 | .get_current_chain(&self.latest_stable_block_hash) 82 | .iter() 83 | .enumerate() 84 | { 85 | let block_height = self.stable_height() + (i as u32) + 1; 86 | let confirmations = self.main_chain_height() - block_height + 1; 87 | 88 | if confirmations < min_confirmations { 89 | // The block has fewer confirmations than requested. 90 | // We can stop now since all remaining blocks will have fewer confirmations. 91 | break; 92 | } 93 | 94 | for tx in &block.txdata { 95 | address_utxos.insert_tx(tx, block_height); 96 | } 97 | } 98 | 99 | address_utxos 100 | // Filter out UTXOs added in unstable blocks that are not for the given address. 101 | .get_utxos(address) 102 | .into_set() 103 | .into_iter() 104 | // Filter out UTXOs that are below the `min_confirmations` threshold. 105 | .filter(|(_, _, height)| self.main_chain_height() - height + 1 >= min_confirmations) 106 | .collect() 107 | } 108 | 109 | /// Insert a block into the blockchain. 110 | pub fn insert_block(&mut self, block: Block) { 111 | // The block is first inserted into the unstable blocks. 112 | self.unstable_blocks.push(block); 113 | 114 | // Process a stable block, if any. 115 | if let Some(new_stable_block) = self.unstable_blocks.pop(&self.latest_stable_block_hash) { 116 | // Verify the block is extending the previous block. 117 | assert_eq!( 118 | new_stable_block.header.prev_blockhash, self.latest_stable_block_hash, 119 | "Invalid block hash at height: {}", 120 | self.height 121 | ); 122 | 123 | self.latest_stable_block_hash = new_stable_block.block_hash(); 124 | 125 | for tx in &new_stable_block.txdata { 126 | self.utxos.insert_tx(tx, self.height); 127 | } 128 | 129 | self.height += 1; 130 | } 131 | } 132 | 133 | pub fn stable_height(&self) -> Height { 134 | self.height 135 | } 136 | 137 | pub fn main_chain_height(&self) -> Height { 138 | self.unstable_blocks 139 | .get_current_chain(&self.latest_stable_block_hash) 140 | .len() as u32 141 | + self.height 142 | } 143 | 144 | pub fn get_unstable_blocks(&self) -> Vec<&Block> { 145 | self.unstable_blocks.get_blocks() 146 | } 147 | 148 | pub fn to_proto(&self) -> proto::State { 149 | proto::State { 150 | height: self.height, 151 | latest_stable_block_hash: self.latest_stable_block_hash.to_vec(), 152 | utxos: Some(self.utxos.to_proto()), 153 | unstable_blocks: Some(self.unstable_blocks.to_proto()), 154 | } 155 | } 156 | 157 | pub fn from_proto(proto_state: proto::State) -> Self { 158 | Self { 159 | height: proto_state.height, 160 | latest_stable_block_hash: BlockHash::from_hash( 161 | Hash::from_slice(&proto_state.latest_stable_block_hash).unwrap(), 162 | ), 163 | utxos: UtxoSet::from_proto(proto_state.utxos.unwrap()), 164 | unstable_blocks: BlockForest::from_proto(proto_state.unstable_blocks.unwrap()), 165 | } 166 | } 167 | 168 | pub fn anchor_hash(&self) -> BlockHash { 169 | self.latest_stable_block_hash 170 | } 171 | } 172 | 173 | #[cfg(test)] 174 | mod test { 175 | use super::*; 176 | use crate::test_builder::{BlockBuilder, TransactionBuilder}; 177 | use bitcoin::blockdata::constants::genesis_block; 178 | use bitcoin::secp256k1::rand::rngs::OsRng; 179 | use bitcoin::secp256k1::Secp256k1; 180 | use bitcoin::{consensus::Decodable, Address, BlockHash, Network, PublicKey}; 181 | use byteorder::{LittleEndian, ReadBytesExt}; 182 | use maplit::hashset; 183 | use std::fs::File; 184 | use std::str::FromStr; 185 | use std::{collections::HashMap, io::BufReader}; 186 | 187 | fn process_chain(state: &mut State, num_blocks: u32) { 188 | let mut chain: Vec = vec![]; 189 | 190 | let mut blocks: HashMap = HashMap::new(); 191 | 192 | let mut blk_file = BufReader::new(File::open("./test-data/100k_blocks.dat").unwrap()); 193 | 194 | loop { 195 | let magic = match blk_file.read_u32::() { 196 | Err(_) => break, 197 | Ok(magic) => { 198 | if magic == 0 { 199 | // Reached EOF 200 | break; 201 | } 202 | magic 203 | } 204 | }; 205 | assert_eq!(magic, 0xD9B4BEF9); 206 | 207 | let _block_size = blk_file.read_u32::().unwrap(); 208 | 209 | let block = Block::consensus_decode(&mut blk_file).unwrap(); 210 | 211 | blocks.insert(block.header.prev_blockhash, block); 212 | } 213 | 214 | println!("# blocks in file: {}", blocks.len()); 215 | 216 | // Build the chain 217 | chain.push( 218 | blocks 219 | .remove(&genesis_block(Network::Bitcoin).block_hash()) 220 | .unwrap(), 221 | ); 222 | for _ in 0..num_blocks - 1 { 223 | let next_block = blocks.remove(&chain[chain.len() - 1].block_hash()).unwrap(); 224 | chain.push(next_block); 225 | } 226 | 227 | println!("Built chain with length: {}", chain.len()); 228 | 229 | for block in chain.into_iter() { 230 | state.insert_block(block); 231 | } 232 | } 233 | 234 | #[test] 235 | fn to_from_proto() { 236 | use prost::Message; 237 | let mut block = BlockBuilder::genesis() 238 | .with_transaction(TransactionBuilder::coinbase().build()) 239 | .build(); 240 | let mut state = State::new(2, Network::Bitcoin, block.clone()); 241 | 242 | for _ in 0..100 { 243 | block = BlockBuilder::with_prev_header(block.header) 244 | .with_transaction(TransactionBuilder::coinbase().build()) 245 | .build(); 246 | state.insert_block(block.clone()); 247 | } 248 | 249 | let state_proto = state.to_proto(); 250 | let state_proto = proto::State::decode(&*state_proto.encode_to_vec()).unwrap(); 251 | let new_state = State::from_proto(state_proto); 252 | 253 | assert_eq!(new_state, state); 254 | } 255 | 256 | #[test] 257 | fn utxos_forks() { 258 | let secp = Secp256k1::new(); 259 | let mut rng = OsRng::new().unwrap(); 260 | 261 | // Create some BTC addresses. 262 | let address_1 = Address::p2pkh( 263 | &PublicKey::new(secp.generate_keypair(&mut rng).1), 264 | Network::Bitcoin, 265 | ); 266 | let address_2 = Address::p2pkh( 267 | &PublicKey::new(secp.generate_keypair(&mut rng).1), 268 | Network::Bitcoin, 269 | ); 270 | let address_3 = Address::p2pkh( 271 | &PublicKey::new(secp.generate_keypair(&mut rng).1), 272 | Network::Bitcoin, 273 | ); 274 | let address_4 = Address::p2pkh( 275 | &PublicKey::new(secp.generate_keypair(&mut rng).1), 276 | Network::Bitcoin, 277 | ); 278 | 279 | // Create a genesis block where 1000 satoshis are given to address 1. 280 | let coinbase_tx = TransactionBuilder::coinbase() 281 | .with_output(&address_1, 1000) 282 | .build(); 283 | 284 | let block_0 = BlockBuilder::genesis() 285 | .with_transaction(coinbase_tx.clone()) 286 | .build(); 287 | 288 | let mut state = State::new(2, Network::Bitcoin, block_0.clone()); 289 | 290 | let block_0_utxos = maplit::hashset! { 291 | ( 292 | OutPoint { 293 | txid: coinbase_tx.txid(), 294 | vout: 0 295 | }, 296 | TxOut { 297 | value: 1000, 298 | script_pubkey: address_1.script_pubkey() 299 | }, 300 | 1 301 | ) 302 | }; 303 | 304 | // Assert that the UTXOs of address 1 are present. 305 | assert_eq!(state.get_utxos(&address_1.to_string(), 0), block_0_utxos); 306 | 307 | // Extend block 0 with block 1 that spends the 1000 satoshis and gives them to address 2. 308 | let tx = TransactionBuilder::with_input(OutPoint::new(coinbase_tx.txid(), 0)) 309 | .with_output(&address_2, 1000) 310 | .build(); 311 | let block_1 = BlockBuilder::with_prev_header(block_0.header) 312 | .with_transaction(tx.clone()) 313 | .build(); 314 | 315 | state.insert_block(block_1); 316 | 317 | // address 2 should now have the UTXO while address 1 has no UTXOs. 318 | assert_eq!( 319 | state.get_utxos(&address_2.to_string(), 0), 320 | hashset! { 321 | ( 322 | OutPoint::new(tx.txid(), 0), 323 | TxOut { 324 | value: 1000, 325 | script_pubkey: address_2.script_pubkey(), 326 | }, 327 | 2 328 | ) 329 | } 330 | ); 331 | 332 | assert_eq!(state.get_utxos(&address_1.to_string(), 0), hashset! {}); 333 | 334 | // Extend block 0 (again) with block 1 that spends the 1000 satoshis to address 3 335 | // This causes a fork. 336 | let tx = TransactionBuilder::with_input(OutPoint::new(coinbase_tx.txid(), 0)) 337 | .with_output(&address_3, 1000) 338 | .build(); 339 | let block_1_prime = BlockBuilder::with_prev_header(block_0.header) 340 | .with_transaction(tx.clone()) 341 | .build(); 342 | state.insert_block(block_1_prime.clone()); 343 | 344 | // Because block 1 and block 1' contest with each other, neither of them are included 345 | // in the UTXOs. Only the UTXOs of block 0 are returned. 346 | assert_eq!(state.get_utxos(&address_2.to_string(), 0), hashset! {}); 347 | assert_eq!(state.get_utxos(&address_3.to_string(), 0), hashset! {}); 348 | assert_eq!(state.get_utxos(&address_1.to_string(), 0), block_0_utxos); 349 | 350 | // Now extend block 1' with another block that transfers the funds to address 4. 351 | // In this case, the fork of [block 1', block 2'] will be considered the "current" 352 | // chain, and will be part of the UTXOs. 353 | let tx = TransactionBuilder::with_input(OutPoint::new(tx.txid(), 0)) 354 | .with_output(&address_4, 1000) 355 | .build(); 356 | let block_2_prime = BlockBuilder::with_prev_header(block_1_prime.header) 357 | .with_transaction(tx.clone()) 358 | .build(); 359 | state.insert_block(block_2_prime); 360 | 361 | // Address 1 has no UTXOs since they were spent on the current chain. 362 | assert_eq!(state.get_utxos(&address_1.to_string(), 0), hashset! {}); 363 | assert_eq!(state.get_utxos(&address_2.to_string(), 0), hashset! {}); 364 | assert_eq!(state.get_utxos(&address_3.to_string(), 0), hashset! {}); 365 | // The funds are now with address 4. 366 | assert_eq!( 367 | state.get_utxos(&address_4.to_string(), 0), 368 | hashset! { 369 | ( 370 | OutPoint { 371 | txid: tx.txid(), 372 | vout: 0 373 | }, 374 | TxOut { 375 | value: 1000, 376 | script_pubkey: address_4.script_pubkey() 377 | }, 378 | 3 379 | ) 380 | } 381 | ); 382 | } 383 | 384 | #[test] 385 | fn process_100k_blocks() { 386 | let mut state = State::new(0, Network::Bitcoin, genesis_block(Network::Bitcoin)); 387 | 388 | process_chain(&mut state, 100_000); 389 | 390 | let mut total_supply = 0; 391 | for (_, v, _) in state.utxos.clone().into_set() { 392 | total_supply += v.value; 393 | } 394 | 395 | // NOTE: The duplicate transactions cause us to lose some of the supply, 396 | // which we deduct in this assertion. 397 | assert_eq!( 398 | ((state.height as u64) - DUPLICATE_TX_IDS.len() as u64) * 5000000000, 399 | total_supply 400 | ); 401 | 402 | // Check some random addresses that the balance is correct: 403 | 404 | // https://blockexplorer.one/bitcoin/mainnet/address/1PgZsaGjvssNCqHHisshLoCFeUjxPhutTh 405 | assert_eq!( 406 | state.get_balance("1PgZsaGjvssNCqHHisshLoCFeUjxPhutTh", 0), 407 | 4000000 408 | ); 409 | assert_eq!( 410 | state.get_utxos("1PgZsaGjvssNCqHHisshLoCFeUjxPhutTh", 0), 411 | maplit::hashset! { 412 | ( 413 | OutPoint { 414 | txid: Txid::from_str( 415 | "1a592a31c79f817ed787b6acbeef29b0f0324179820949d7da6215f0f4870c42", 416 | ) 417 | .unwrap(), 418 | vout: 1, 419 | }, 420 | TxOut { 421 | value: 4000000, 422 | script_pubkey: Address::from_str("1PgZsaGjvssNCqHHisshLoCFeUjxPhutTh") 423 | .unwrap() 424 | .script_pubkey(), 425 | }, 426 | 75361 427 | ) 428 | } 429 | ); 430 | 431 | // https://blockexplorer.one/bitcoin/mainnet/address/12tGGuawKdkw5NeDEzS3UANhCRa1XggBbK 432 | assert_eq!( 433 | state.get_balance("12tGGuawKdkw5NeDEzS3UANhCRa1XggBbK", 0), 434 | 500000000 435 | ); 436 | assert_eq!( 437 | state.get_utxos("12tGGuawKdkw5NeDEzS3UANhCRa1XggBbK", 0), 438 | maplit::hashset! { 439 | ( 440 | OutPoint { 441 | txid: Txid::from_str( 442 | "3371b3978e7285d962fd54656aca6b3191135a1db838b5c689b8a44a7ede6a31", 443 | ) 444 | .unwrap(), 445 | vout: 0, 446 | }, 447 | TxOut { 448 | value: 500000000, 449 | script_pubkey: Address::from_str("12tGGuawKdkw5NeDEzS3UANhCRa1XggBbK") 450 | .unwrap() 451 | .script_pubkey(), 452 | }, 453 | 66184 454 | ) 455 | } 456 | ); 457 | } 458 | 459 | #[test] 460 | fn get_utxos_min_confirmations_greater_than_chain_height() { 461 | for network in [ 462 | Network::Bitcoin, 463 | Network::Regtest, 464 | Network::Testnet, 465 | Network::Signet, 466 | ] 467 | .iter() 468 | { 469 | // Generate addresses. 470 | let address_1 = { 471 | let secp = Secp256k1::new(); 472 | let mut rng = OsRng::new().unwrap(); 473 | Address::p2pkh(&PublicKey::new(secp.generate_keypair(&mut rng).1), *network) 474 | }; 475 | 476 | // Create a block where 1000 satoshis are given to the address_1. 477 | let block_0 = BlockBuilder::genesis() 478 | .with_transaction( 479 | TransactionBuilder::coinbase() 480 | .with_output(&address_1, 1000) 481 | .build(), 482 | ) 483 | .build(); 484 | 485 | let state = State::new(1, *network, block_0); 486 | 487 | // Expect an empty UTXO set. 488 | assert_eq!(state.main_chain_height(), 1); 489 | assert_eq!(state.get_utxos(&address_1.to_string(), 2), HashSet::new()); 490 | } 491 | } 492 | 493 | #[test] 494 | fn get_utxos_does_not_include_other_addresses() { 495 | for network in [ 496 | Network::Bitcoin, 497 | Network::Regtest, 498 | Network::Testnet, 499 | Network::Signet, 500 | ] 501 | .iter() 502 | { 503 | // Generate addresses. 504 | let address_1 = { 505 | let secp = Secp256k1::new(); 506 | let mut rng = OsRng::new().unwrap(); 507 | Address::p2pkh(&PublicKey::new(secp.generate_keypair(&mut rng).1), *network) 508 | }; 509 | 510 | let address_2 = { 511 | let secp = Secp256k1::new(); 512 | let mut rng = OsRng::new().unwrap(); 513 | Address::p2pkh(&PublicKey::new(secp.generate_keypair(&mut rng).1), *network) 514 | }; 515 | 516 | // Create a genesis block where 1000 satoshis are given to the address_1, followed 517 | // by a block where address_1 gives 1000 satoshis to address_2. 518 | let coinbase_tx = TransactionBuilder::coinbase() 519 | .with_output(&address_1, 1000) 520 | .build(); 521 | let block_0 = BlockBuilder::genesis() 522 | .with_transaction(coinbase_tx.clone()) 523 | .build(); 524 | let tx = TransactionBuilder::with_input(bitcoin::OutPoint::new(coinbase_tx.txid(), 0)) 525 | .with_output(&address_2, 1000) 526 | .build(); 527 | let block_1 = BlockBuilder::with_prev_header(block_0.header) 528 | .with_transaction(tx.clone()) 529 | .build(); 530 | 531 | let mut state = State::new(2, *network, block_0); 532 | state.insert_block(block_1); 533 | 534 | // Address 1 should have no UTXOs at zero confirmations. 535 | assert_eq!( 536 | state.get_utxos(&address_1.to_string(), 0), 537 | maplit::hashset! {} 538 | ); 539 | } 540 | } 541 | } 542 | -------------------------------------------------------------------------------- /canister/src/sync_demo.rs: -------------------------------------------------------------------------------- 1 | use bitcoin::{blockdata::constants::genesis_block, Network, OutPoint, TxOut}; 2 | use btc::{ 3 | proto::{btc_adapter_client::BtcAdapterClient, GetSuccessorsRequest}, 4 | store::State, 5 | }; 6 | use prost::Message; 7 | use std::io; 8 | use std::io::Write; 9 | use std::sync::{Arc, RwLock}; 10 | use std::time::SystemTime; 11 | use tonic::Request; 12 | 13 | mod proto { 14 | tonic::include_proto!("btc"); 15 | } 16 | 17 | const DELTA: u64 = 6; 18 | 19 | #[tokio::main] 20 | async fn main() { 21 | let args: Vec = std::env::args().collect(); 22 | 23 | // Initialize the state with the mainnet genesis block. 24 | let mut state = Arc::new(RwLock::new(State::new( 25 | DELTA, 26 | Network::Bitcoin, 27 | genesis_block(Network::Bitcoin), 28 | ))); 29 | 30 | if args.len() > 1 { 31 | // A state file was specified in the first argument. Load that state. 32 | println!("Reading state from disk..."); 33 | let now = SystemTime::now(); 34 | let state_from_disk = std::fs::read(&args[1]).unwrap(); 35 | println!( 36 | "Done. Duration: {} seconds", 37 | now.elapsed().unwrap().as_secs() 38 | ); 39 | 40 | println!("Deserializing State..."); 41 | let now = SystemTime::now(); 42 | let decoded_state = btc::proto::State::decode(&*state_from_disk).unwrap(); 43 | state = Arc::new(RwLock::new(State::from_proto(decoded_state))); 44 | println!( 45 | "Done. Duration: {} seconds", 46 | now.elapsed().unwrap().as_secs() 47 | ); 48 | } 49 | 50 | // A reference to the lock of the state for use in another thread. 51 | let state_2 = Arc::clone(&state); 52 | 53 | tokio::spawn(async move { 54 | let mut rpc_client = BtcAdapterClient::connect("http://127.0.0.1:34254") 55 | .await 56 | .unwrap(); 57 | 58 | loop { 59 | let block_hashes = { 60 | let state_read = state_2 61 | .read() 62 | .expect("Cannot get read-only access to state"); 63 | 64 | let mut block_hashes: Vec> = state_read 65 | .get_unstable_blocks() 66 | .iter() 67 | .map(|b| b.block_hash().to_vec()) 68 | .collect(); 69 | 70 | block_hashes.push(state_read.anchor_hash().to_vec()); 71 | 72 | block_hashes 73 | }; 74 | 75 | // Start requesting more blocks. 76 | let rpc_request = Request::new(GetSuccessorsRequest { block_hashes }); 77 | 78 | // Send the request to the BTC adapter. We assume that the TCP can 79 | // accept connections in this hard-coded port. 80 | match rpc_client.get_successors(rpc_request).await { 81 | Ok(tonic_response) => { 82 | let mut state_write = state_2.write().unwrap(); 83 | let response = tonic_response.into_inner(); 84 | 85 | for block_proto in response.blocks { 86 | let block = btc::block::from_proto(&block_proto); 87 | println!("Processing block with hash: {}", block.block_hash()); 88 | state_write.insert_block(block); 89 | println!("New mainchain height: {}", state_write.main_chain_height()); 90 | } 91 | } 92 | Err(_) => {} 93 | } 94 | 95 | // Sleep for a second to not spam the adapter. 96 | std::thread::sleep(std::time::Duration::from_secs(1)); 97 | } 98 | }); 99 | 100 | loop { 101 | print!(">> "); 102 | let mut input = String::new(); 103 | io::stdout().flush().unwrap(); 104 | io::stdin() 105 | .read_line(&mut input) 106 | .expect("Error reading from STDIN"); 107 | 108 | // Command for getting utxos of an address. 109 | // e.g. "utxos 12tGGuawKdkw5NeDEzS3UANhCRa1XggBbK" 110 | if input.contains("utxos") { 111 | let state_read = state.read().expect("Cannot get read-only access to state"); 112 | let address_str = input.as_str().split(" ").collect::>()[1].trim(); 113 | println!( 114 | "{:#?}", 115 | state_read 116 | .get_utxos(address_str, 0) 117 | .into_iter() 118 | .map(|x| (x.0, x.1, x.2)) 119 | .collect::>() 120 | ); 121 | } 122 | // Command for getting the balance of an address. 123 | // e.g. "balance 12tGGuawKdkw5NeDEzS3UANhCRa1XggBbK" 124 | else if input.contains("balance") { 125 | let state_read = state.read().expect("Cannot get read-only access to state"); 126 | let address_str = input.as_str().split(" ").collect::>()[1].trim(); 127 | let balance = state_read.get_balance(address_str, 0); 128 | let decimals = balance % 100_000_000; 129 | let whole = balance / 100_000_000; 130 | println!("{}.{:0>8}", whole, decimals); 131 | } 132 | // Command for saving the state. 133 | // e.g. "save file_name" 134 | else if input.contains("save") { 135 | let file_name = input.as_str().split(" ").collect::>()[1].trim(); 136 | let state_read = state.read().expect("Cannot get read-only access to state"); 137 | 138 | let mut file = std::fs::File::create(file_name).expect("create failed"); 139 | file.write_all(&state_read.to_proto().encode_to_vec()) 140 | .expect("write failed"); 141 | println!("Save complete"); 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /canister/src/test_builder.rs: -------------------------------------------------------------------------------- 1 | use bitcoin::{ 2 | secp256k1::rand::rngs::OsRng, secp256k1::Secp256k1, util::uint::Uint256, Address, Block, 3 | BlockHash, BlockHeader, Network, OutPoint, PublicKey, Script, Transaction, TxIn, TxMerkleNode, 4 | TxOut, 5 | }; 6 | 7 | pub struct BlockBuilder { 8 | prev_header: Option, 9 | transactions: Vec, 10 | } 11 | 12 | impl BlockBuilder { 13 | pub fn genesis() -> Self { 14 | Self { 15 | prev_header: None, 16 | transactions: vec![], 17 | } 18 | } 19 | 20 | pub fn with_prev_header(prev_header: BlockHeader) -> Self { 21 | Self { 22 | prev_header: Some(prev_header), 23 | transactions: vec![], 24 | } 25 | } 26 | 27 | pub fn with_transaction(mut self, transaction: Transaction) -> Self { 28 | self.transactions.push(transaction); 29 | self 30 | } 31 | 32 | pub fn build(self) -> Block { 33 | let txdata = if self.transactions.is_empty() { 34 | // Create a random coinbase transaction. 35 | vec![TransactionBuilder::coinbase().build()] 36 | } else { 37 | self.transactions 38 | }; 39 | 40 | let merkle_root = 41 | bitcoin::util::hash::bitcoin_merkle_root(txdata.iter().map(|tx| tx.txid().as_hash())); 42 | let merkle_root = TxMerkleNode::from_hash(merkle_root); 43 | 44 | let header = match self.prev_header { 45 | Some(prev_header) => header(&prev_header, merkle_root), 46 | None => genesis(merkle_root), 47 | }; 48 | 49 | Block { header, txdata } 50 | } 51 | } 52 | 53 | fn genesis(merkle_root: TxMerkleNode) -> BlockHeader { 54 | let target = Uint256([ 55 | 0xffffffffffffffffu64, 56 | 0xffffffffffffffffu64, 57 | 0xffffffffffffffffu64, 58 | 0x7fffffffffffffffu64, 59 | ]); 60 | let bits = BlockHeader::compact_target_from_u256(&target); 61 | 62 | let mut header = BlockHeader { 63 | version: 1, 64 | time: 0, 65 | nonce: 0, 66 | bits, 67 | merkle_root, 68 | prev_blockhash: BlockHash::default(), 69 | }; 70 | solve(&mut header); 71 | 72 | header 73 | } 74 | 75 | pub struct TransactionBuilder { 76 | input: Option, 77 | output_value: Option, 78 | output_address: Option
, 79 | } 80 | 81 | impl TransactionBuilder { 82 | pub fn coinbase() -> Self { 83 | Self { 84 | input: None, 85 | output_value: None, 86 | output_address: None, 87 | } 88 | } 89 | 90 | pub fn with_input(input: OutPoint) -> Self { 91 | Self { 92 | input: Some(input), 93 | output_value: None, 94 | output_address: None, 95 | } 96 | } 97 | 98 | pub fn with_output(mut self, address: &Address, value: u64) -> Self { 99 | self.output_address = Some(address.clone()); 100 | self.output_value = Some(value); 101 | self 102 | } 103 | 104 | pub fn build(self) -> Transaction { 105 | let input = match self.input { 106 | None => vec![], 107 | Some(input) => vec![TxIn { 108 | previous_output: input, 109 | script_sig: Script::new(), 110 | sequence: 0xffffffff, 111 | witness: vec![], 112 | }], 113 | }; 114 | 115 | // Use default of 50 BTC 116 | let output_value = self.output_value.unwrap_or(50_0000_0000); 117 | 118 | let output_address = match self.output_address { 119 | Some(address) => address, 120 | None => { 121 | // Generate a random address. 122 | let secp = Secp256k1::new(); 123 | let mut rng = OsRng::new().unwrap(); 124 | Address::p2pkh( 125 | &PublicKey::new(secp.generate_keypair(&mut rng).1), 126 | Network::Regtest, 127 | ) 128 | } 129 | }; 130 | 131 | Transaction { 132 | version: 1, 133 | lock_time: 0, 134 | input, 135 | output: vec![TxOut { 136 | value: output_value, 137 | script_pubkey: output_address.script_pubkey(), 138 | }], 139 | } 140 | } 141 | } 142 | 143 | fn header(prev_header: &BlockHeader, merkle_root: TxMerkleNode) -> BlockHeader { 144 | let time = prev_header.time + 60 * 10; // 10 minutes. 145 | let bits = BlockHeader::compact_target_from_u256(&prev_header.target()); 146 | 147 | let mut header = BlockHeader { 148 | version: 1, 149 | time, 150 | nonce: 0, 151 | bits, 152 | merkle_root, 153 | prev_blockhash: prev_header.block_hash(), 154 | }; 155 | solve(&mut header); 156 | 157 | header 158 | } 159 | 160 | fn solve(header: &mut BlockHeader) { 161 | let target = header.target(); 162 | while header.validate_pow(&target).is_err() { 163 | header.nonce += 1; 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /canister/src/utxoset.rs: -------------------------------------------------------------------------------- 1 | use crate::proto; 2 | use bitcoin::hashes::Hash; 3 | use bitcoin::{Address, Network, OutPoint, Script, Transaction, TxOut, Txid}; 4 | use std::collections::{BTreeMap, HashMap, HashSet}; 5 | use std::str::FromStr; 6 | 7 | type Height = u32; 8 | 9 | lazy_static::lazy_static! { 10 | static ref DUPLICATE_TX_IDS: [Txid; 2] = [ 11 | Txid::from_str("d5d27987d2a3dfc724e359870c6644b40e497bdc0589a033220fe15429d88599").unwrap(), 12 | Txid::from_str("e3bf3d07d4b0375638d5f1db5255fe07ba2c4cb067cd81b84ee974b6585fb468").unwrap() 13 | ]; 14 | } 15 | 16 | #[cfg_attr(test, derive(Clone, Debug, PartialEq))] 17 | pub struct UtxoSet { 18 | utxos: HashMap, 19 | network: Network, 20 | // An index for fast retrievals of an address's UTXOs. 21 | address_to_outpoints: BTreeMap>, 22 | // If true, a transaction's inputs must all be present in the UTXO for it to be accepted. 23 | strict: bool, 24 | } 25 | 26 | impl UtxoSet { 27 | pub fn new(strict: bool, network: Network) -> Self { 28 | Self { 29 | utxos: HashMap::default(), 30 | address_to_outpoints: BTreeMap::default(), 31 | strict, 32 | network, 33 | } 34 | } 35 | 36 | /// Returns the `UtxoSet` of a given bitcoin address. 37 | pub fn get_utxos(&self, address: &str) -> UtxoSet { 38 | // Since we're returning a partial UTXO, we need not be strict. 39 | let mut utxos = Self::new(false, self.network); 40 | for outpoint in self.address_to_outpoints.get(address).unwrap_or(&vec![]) { 41 | let (tx_out, height) = self.utxos.get(outpoint).expect("outpoint must exist"); 42 | utxos.insert_outpoint(*outpoint, tx_out.clone(), *height); 43 | } 44 | 45 | utxos 46 | } 47 | 48 | pub fn insert_tx(&mut self, tx: &Transaction, height: Height) { 49 | self.remove_spent_txs(tx); 50 | self.insert_unspent_txs(tx, height); 51 | } 52 | 53 | pub fn into_set(self) -> HashSet<(OutPoint, TxOut, Height)> { 54 | self.utxos.into_iter().map(|(k, v)| (k, v.0, v.1)).collect() 55 | } 56 | 57 | // Iterates over transaction inputs and removes spent outputs. 58 | fn remove_spent_txs(&mut self, tx: &Transaction) { 59 | if tx.is_coin_base() { 60 | return; 61 | } 62 | 63 | for input in &tx.input { 64 | // Verify that we've seen the outpoint before. 65 | match self.utxos.remove(&input.previous_output) { 66 | Some((txout, _)) => { 67 | if let Some(address) = Address::from_script(&txout.script_pubkey, self.network) 68 | { 69 | let address = address.to_string(); 70 | let address_outpoints = 71 | self.address_to_outpoints.get_mut(&address).unwrap(); 72 | 73 | let mut found = false; 74 | for (index, outpoint) in address_outpoints.iter().enumerate() { 75 | if outpoint == &input.previous_output { 76 | address_outpoints.remove(index); 77 | found = true; 78 | break; 79 | } 80 | } 81 | 82 | if !found && self.strict { 83 | panic!("Outpoint {:?} not found in index.", input.previous_output); 84 | } 85 | 86 | if address_outpoints.is_empty() { 87 | self.address_to_outpoints.remove(&address); 88 | } 89 | } 90 | } 91 | None => { 92 | if self.strict { 93 | panic!("Outpoint {:?} not found.", input.previous_output); 94 | } 95 | } 96 | } 97 | } 98 | } 99 | 100 | // Iterates over transaction outputs and adds unspents. 101 | fn insert_unspent_txs(&mut self, tx: &Transaction, height: Height) { 102 | for (vout, output) in tx.output.iter().enumerate() { 103 | self.insert_outpoint( 104 | OutPoint::new(tx.txid(), vout as u32), 105 | output.clone(), 106 | height, 107 | ); 108 | } 109 | } 110 | 111 | fn insert_outpoint(&mut self, outpoint: OutPoint, output: TxOut, height: Height) { 112 | // Verify that we haven't seen the outpoint before. 113 | // NOTE: There was a bug where there were duplicate transactions. These transactions 114 | // we overwrite. 115 | // 116 | // See: https://en.bitcoin.it/wiki/BIP_0030 117 | // https://bitcoinexplorer.org/tx/d5d27987d2a3dfc724e359870c6644b40e497bdc0589a033220fe15429d88599 118 | // https://bitcoinexplorer.org/tx/e3bf3d07d4b0375638d5f1db5255fe07ba2c4cb067cd81b84ee974b6585fb468 119 | if self.utxos.contains_key(&outpoint) && !DUPLICATE_TX_IDS.contains(&outpoint.txid) { 120 | panic!( 121 | "Cannot insert outpoint {:?} because it was already inserted. Block height: {}", 122 | outpoint, height 123 | ); 124 | } 125 | 126 | // Insert the outpoint. 127 | if let Some(address) = Address::from_script(&output.script_pubkey, self.network) { 128 | // Add the address to the index if we can parse it. 129 | self.address_to_outpoints 130 | .entry(address.to_string()) 131 | .or_insert_with(Vec::new) 132 | .push(outpoint); 133 | } 134 | 135 | self.utxos.insert(outpoint, (output, height)); 136 | } 137 | 138 | pub fn to_proto(&self) -> proto::UtxoSet { 139 | proto::UtxoSet { 140 | utxos: self 141 | .utxos 142 | .iter() 143 | .map(|(outpoint, (txout, height))| proto::Utxo { 144 | outpoint: Some(proto::OutPoint { 145 | txid: outpoint.txid.to_vec(), 146 | vout: outpoint.vout, 147 | }), 148 | txout: Some(proto::TxOut { 149 | value: txout.value, 150 | script_pubkey: txout.script_pubkey.to_bytes(), 151 | }), 152 | height: *height, 153 | }) 154 | .collect(), 155 | strict: self.strict, 156 | network: match self.network { 157 | Network::Bitcoin => 0, 158 | Network::Testnet => 1, 159 | Network::Signet => 2, 160 | Network::Regtest => 3, 161 | }, 162 | } 163 | } 164 | 165 | pub fn from_proto(utxos_proto: proto::UtxoSet) -> Self { 166 | let mut utxo_set = Self { 167 | utxos: HashMap::default(), 168 | address_to_outpoints: BTreeMap::default(), 169 | strict: utxos_proto.strict, 170 | network: match utxos_proto.network { 171 | 0 => Network::Bitcoin, 172 | 1 => Network::Testnet, 173 | 2 => Network::Signet, 174 | 3 => Network::Regtest, 175 | _ => panic!("Invalid network ID"), 176 | }, 177 | }; 178 | 179 | for utxo in utxos_proto.utxos.into_iter() { 180 | let outpoint = utxo 181 | .outpoint 182 | .map(|o| OutPoint::new(Txid::from_hash(Hash::from_slice(&o.txid).unwrap()), o.vout)) 183 | .unwrap(); 184 | 185 | let tx_out = utxo 186 | .txout 187 | .map(|t| TxOut { 188 | value: t.value, 189 | script_pubkey: Script::from(t.script_pubkey), 190 | }) 191 | .unwrap(); 192 | 193 | utxo_set.insert_outpoint(outpoint, tx_out, utxo.height); 194 | } 195 | 196 | utxo_set 197 | } 198 | } 199 | 200 | #[cfg(test)] 201 | mod test { 202 | use super::*; 203 | use crate::test_builder::TransactionBuilder; 204 | use bitcoin::secp256k1::rand::rngs::OsRng; 205 | use bitcoin::secp256k1::Secp256k1; 206 | use bitcoin::{Address, PublicKey, TxOut}; 207 | 208 | #[test] 209 | fn coinbase_tx() { 210 | for network in [Network::Bitcoin, Network::Regtest, Network::Testnet].iter() { 211 | let secp = Secp256k1::new(); 212 | let mut rng = OsRng::new().unwrap(); 213 | 214 | let address = 215 | Address::p2pkh(&PublicKey::new(secp.generate_keypair(&mut rng).1), *network); 216 | 217 | let coinbase_tx = TransactionBuilder::coinbase() 218 | .with_output(&address, 1000) 219 | .build(); 220 | 221 | let mut utxo = UtxoSet::new(true, *network); 222 | utxo.insert_tx(&coinbase_tx, 0); 223 | 224 | let expected = maplit::hashset! { 225 | ( 226 | OutPoint { 227 | txid: coinbase_tx.txid(), 228 | vout: 0 229 | }, 230 | TxOut { 231 | value: 1000, 232 | script_pubkey: address.script_pubkey() 233 | }, 234 | 0 235 | ) 236 | }; 237 | 238 | assert_eq!(utxo.clone().into_set(), expected); 239 | assert_eq!(utxo.get_utxos(&address.to_string()).into_set(), expected); 240 | } 241 | } 242 | 243 | #[test] 244 | fn spending() { 245 | for network in [Network::Bitcoin, Network::Regtest, Network::Testnet].iter() { 246 | let secp = Secp256k1::new(); 247 | let mut rng = OsRng::new().unwrap(); 248 | let address_1 = 249 | Address::p2pkh(&PublicKey::new(secp.generate_keypair(&mut rng).1), *network); 250 | let address_2 = 251 | Address::p2pkh(&PublicKey::new(secp.generate_keypair(&mut rng).1), *network); 252 | 253 | let mut utxo = UtxoSet::new(true, *network); 254 | 255 | let coinbase_tx = TransactionBuilder::coinbase() 256 | .with_output(&address_1, 1000) 257 | .build(); 258 | utxo.insert_tx(&coinbase_tx, 0); 259 | let expected = maplit::hashset! { 260 | ( 261 | OutPoint { 262 | txid: coinbase_tx.txid(), 263 | vout: 0 264 | }, 265 | TxOut { 266 | value: 1000, 267 | script_pubkey: address_1.script_pubkey() 268 | }, 269 | 0 270 | ) 271 | }; 272 | 273 | assert_eq!(utxo.get_utxos(&address_1.to_string()).into_set(), expected); 274 | assert_eq!( 275 | utxo.address_to_outpoints, 276 | maplit::btreemap! { 277 | address_1.to_string() => vec![OutPoint { 278 | txid: coinbase_tx.txid(), 279 | vout: 0 280 | }] 281 | } 282 | ); 283 | 284 | // Spend the output to address 2. 285 | let tx = TransactionBuilder::with_input(OutPoint::new(coinbase_tx.txid(), 0)) 286 | .with_output(&address_2, 1000) 287 | .build(); 288 | utxo.insert_tx(&tx, 1); 289 | 290 | assert_eq!( 291 | utxo.get_utxos(&address_1.to_string()).into_set(), 292 | maplit::hashset! {} 293 | ); 294 | assert_eq!( 295 | utxo.get_utxos(&address_2.to_string()).into_set(), 296 | maplit::hashset! { 297 | ( 298 | OutPoint { 299 | txid: tx.txid(), 300 | vout: 0 301 | }, 302 | TxOut { 303 | value: 1000, 304 | script_pubkey: address_2.script_pubkey() 305 | }, 306 | 1 307 | ) 308 | } 309 | ); 310 | assert_eq!( 311 | utxo.address_to_outpoints, 312 | maplit::btreemap! { 313 | address_2.to_string() => vec![OutPoint { 314 | txid: tx.txid(), 315 | vout: 0 316 | }] 317 | } 318 | ); 319 | } 320 | } 321 | } 322 | -------------------------------------------------------------------------------- /canister/test-data/100k_blocks.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/bitcoin-developer-preview/7056a47f587b967dfcf8c37fef5883ef7ded1c87/canister/test-data/100k_blocks.dat -------------------------------------------------------------------------------- /dfx.json: -------------------------------------------------------------------------------- 1 | { 2 | "dfx": "0.8.4", 3 | "canisters": { 4 | "btc": { 5 | "type": "custom", 6 | "candid": "canister/candid.did", 7 | "wasm": "target/wasm32-unknown-unknown/release/canister.wasm", 8 | "build": "scripts/build-canister.sh" 9 | }, 10 | "btc-example-rust": { 11 | "type": "custom", 12 | "candid": "examples/rust/candid.did", 13 | "wasm": "target/wasm32-unknown-unknown/release/example.wasm", 14 | "build": "scripts/build-example.sh" 15 | }, 16 | "btc-example-common": { 17 | "type": "custom", 18 | "candid": "examples/common/candid.did", 19 | "wasm": "target/wasm32-unknown-unknown/release/example-common.wasm", 20 | "build": "scripts/build-example-common.sh" 21 | }, 22 | "btc-example-motoko": { 23 | "type": "motoko", 24 | "candid": "examples/motoko/candid.did", 25 | "main": "examples/motoko/src/Main.mo", 26 | "dependencies": [ 27 | "btc-example-common" 28 | ] 29 | } 30 | }, 31 | "defaults": { 32 | "build": { 33 | "packtool": "" 34 | } 35 | }, 36 | "networks": { 37 | "local": { 38 | "bind": "127.0.0.1:8000", 39 | "type": "ephemeral" 40 | } 41 | }, 42 | "version": 1 43 | } 44 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | services: 3 | bitcoind: 4 | image: ruimarinho/bitcoin-core 5 | ports: 6 | - 18443:18443 7 | - 18444:18444 8 | command: 9 | -conf=/conf/bitcoin.conf 10 | -printtoconsole 11 | -rest 12 | -rpcbind=0.0.0.0 13 | -rpcport=18443 14 | -server 15 | volumes: 16 | - ./docker/bitcoind/conf:/conf 17 | adapter: 18 | image: ic-btc-adapter 19 | build: 20 | dockerfile: docker/adapter/Dockerfile 21 | context: . 22 | depends_on: 23 | - bitcoind 24 | ports: 25 | - 34254:34254 26 | -------------------------------------------------------------------------------- /docker/adapter/Dockerfile: -------------------------------------------------------------------------------- 1 | # Clones the `ic` repository and builds the `ic-btc-adapter`. 2 | FROM ubuntu:20.04 as builder 3 | 4 | ARG DEBIAN_FRONTEND=noninteractive 5 | ARG rust_version=1.55 6 | ARG ic_revision=99116f8e872b8765aa609f91eb8c9394914c483d 7 | 8 | ENV TZ=UTC 9 | 10 | WORKDIR /opt 11 | 12 | RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone && \ 13 | apt -yq update && \ 14 | apt -yqq install --no-install-recommends curl ca-certificates \ 15 | build-essential pkg-config libssl-dev llvm-dev liblmdb-dev clang cmake \ 16 | git 17 | 18 | ENV RUSTUP_HOME=/opt/rustup \ 19 | CARGO_HOME=/opt/cargo \ 20 | PATH=/opt/cargo/bin:$PATH 21 | 22 | RUN curl --fail https://sh.rustup.rs -sSf \ 23 | | sh -s -- -y --default-toolchain ${rust_version}-x86_64-unknown-linux-gnu --no-modify-path && \ 24 | rustup default ${rust_version}-x86_64-unknown-linux-gnu 25 | 26 | ENV PATH=/cargo/bin:$PATH 27 | 28 | RUN git clone https://github.com/dfinity/ic.git && \ 29 | cd ic/rs && \ 30 | git checkout ${ic_revision} && \ 31 | cargo build -p ic-btc-adapter --release 32 | 33 | # The actual image the user will interact with. 34 | FROM ubuntu:20.04 as release 35 | 36 | ARG DEBIAN_FRONTEND=noninteractive 37 | ARG USER=app 38 | ARG UID=3000 39 | ARG GID=3000 40 | ARG BINPATH=/opt/ic/rs/target/release/ic-btc-adapter 41 | 42 | WORKDIR /app 43 | 44 | RUN groupadd -g $GID -o $USER 45 | RUN useradd -m -u $UID -g $GID -o -s /bin/bash $USER 46 | 47 | COPY docker/adapter/regtest.json /etc/regtest.json 48 | 49 | COPY --chown=${USER}:${USER} \ 50 | --from=builder [ \ 51 | "${BINPATH}", \ 52 | "./"] 53 | 54 | ENV PATH="/app:${PATH}" 55 | 56 | CMD ["ic-btc-adapter", "/etc/regtest.json"] 57 | -------------------------------------------------------------------------------- /docker/adapter/regtest.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 70001, 3 | "network": "regtest", 4 | "nodes": [ 5 | "bitcoind:18444" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /docker/bitcoind/conf/bitcoin.conf: -------------------------------------------------------------------------------- 1 | # Enable regtest mode. This is required to setup a private bitcoin network. 2 | regtest=1 3 | 4 | # Dummy credentials that are required by `bitcoin-cli`. 5 | rpcuser=btc-dev-preview 6 | rpcpassword=Wjh4u6SAjT4UMJKxPmoZ0AN2r9qbE-ksXQ5I2_-Hm4w= 7 | rpcauth=btc-dev-preview:8555f1162d473af8e1f744aa056fd728$afaf9cb17b8cf0e8e65994d1195e4b3a4348963b08897b4084d210e5ee588bcb 8 | -------------------------------------------------------------------------------- /examples/README.adoc: -------------------------------------------------------------------------------- 1 | = Example Project 2 | 3 | This directory contains an example project, written in both Motoko and Rust, 4 | to showcase how to: 5 | 6 | . Get the balance of a Bitcoin address. 7 | . Get transaction outputs and use them to build a transaction. 8 | . Sign a transaction and send it to the Bitcoin network. 9 | 10 | NOTE: The example project contains a hard-coded ECDSA key for demonstration purposes. The 11 | mainnet release will offer the functionality for canisters to securely generate ECDSA keys. 12 | 13 | == Deployment 14 | 15 | After going through the <<../README.adoc#getting-started,initial setup>>, you have 16 | the choice of either deploying the Rust or the Motoko example. 17 | 18 | === Rust 19 | 20 | To deploy the Rust example: 21 | 22 | ``` 23 | dfx deploy btc-example-rust --no-wallet --argument "(record { bitcoin_canister_id = principal \"$(dfx canister --no-wallet id btc)\" })" --mode=reinstall 24 | ``` 25 | 26 | === Motoko 27 | 28 | To deploy the Motoko example: 29 | 30 | ```bash 31 | dfx deploy btc-example-motoko --no-wallet --argument "(record { bitcoin_canister_id = principal \"$(dfx canister --no-wallet id btc)\" })" 32 | ``` 33 | 34 | To reinstall the Motoko example: 35 | 36 | ```bash 37 | dfx deploy btc-example-motoko --no-wallet --argument "(record { bitcoin_canister_id = principal \"$(dfx canister --no-wallet id btc)\" })" --mode=reinstall 38 | ``` 39 | 40 | [NOTE] 41 | ==== 42 | Many crypto primitives are still missing in Motoko, and that 43 | makes some fundamental operations, such as computing a Bitcoin address and 44 | signing a transaction, impossible. 45 | 46 | These primitives are actively being built. In the meantime, as a work-around 47 | until the crypto primitives become available, we wrap all the functionality 48 | requiring cryptography into a "common" canister, which is deployed along with 49 | and used by the Motoko example. 50 | ==== 51 | 52 | == Endpoints 53 | 54 | The example provides the following endpoints: 55 | 56 | * Retrieve the canister's BTC address. 57 | 58 | ```bash 59 | dfx canister --no-wallet call btc-example-rust btc_address 60 | dfx canister --no-wallet call btc-example-motoko btc_address 61 | ``` 62 | 63 | * Retrieve the canister's balance. 64 | 65 | ```bash 66 | # Using the Rust example. 67 | dfx canister --no-wallet call btc-example-rust balance 68 | 69 | # Using the Motoko example. 70 | dfx canister --no-wallet call btc-example-motoko balance 71 | ``` 72 | 73 | * Retrieve the canister's UTXOs. 74 | 75 | ```bash 76 | # Using the Rust example. 77 | dfx canister --no-wallet call btc-example-rust get_utxos 78 | 79 | # Using the Motoko example. 80 | dfx canister --no-wallet call btc-example-motoko get_utxos 81 | ``` 82 | 83 | * Send Bitcoin (in Satoshi) from the canister to a destination address. 84 | 85 | ```bash 86 | # Using the Rust example. 87 | dfx canister --no-wallet call btc-example-rust send "(1_0000_0000, \"DESTINATION_ADDRESS\")" 88 | 89 | # Using the Motoko example. 90 | dfx canister --no-wallet call btc-example-motoko send "(1_0000_0000, \"DESTINATION_ADDRESS\")" 91 | ``` 92 | 93 | For the transaction to be processed, you'll need to mine a new block using the command below. 94 | The new block will contain the newly sent transaction. 95 | 96 | ```bash 97 | ./bin/bitcoin-cli -conf=$(pwd)/bitcoin.conf generatetoaddress 1 $BTC_ADDRESS 98 | ``` 99 | 100 | == Sending bitcoin to the example canister 101 | 102 | To top up the example canister with Bitcoin, run the following: 103 | 104 | ``` 105 | # The canister's BTC address. 106 | # This can be retrieved using the canister's "balance" endpoint. 107 | export CANISTER_BTC_ADDRESS=mmdoAzumgjbvAJjVGg7fkQmtvDNFd2wjjH 108 | 109 | # Send a transaction that transfers 10 BTC to the canister. 110 | ./bin/bitcoin-cli -conf=$(pwd)/bitcoin.conf -datadir=$(pwd)/data sendtoaddress $CANISTER_BTC_ADDRESS 10 "" "" true true null "unset" null 1.1 111 | 112 | # Mine a block that contains the transaction. 113 | ./bin/bitcoin-cli -conf=$(pwd)/bitcoin.conf generatetoaddress 1 $BTC_ADDRESS 114 | ``` 115 | 116 | If successful, querying the `balance` endpoint of the canister should return 117 | the updated balance. 118 | -------------------------------------------------------------------------------- /examples/common/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-common" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | bitcoin = "0.27.1" 8 | hex = "0.4.3" 9 | ic-btc-types = { path = "../../types" } 10 | ic-cdk = "0.3.1" 11 | ic-cdk-macros = "0.3.1" 12 | serde = "1.0.132" 13 | 14 | [dev-dependencies] 15 | bitcoin = { version = "0.27.1", features = ["rand"] } 16 | tempfile = "3.2.0" 17 | -------------------------------------------------------------------------------- /examples/common/candid.did: -------------------------------------------------------------------------------- 1 | type Satoshi = nat64; 2 | 3 | type Network = variant { 4 | Bitcoin; 5 | Regtest; 6 | Testnet; 7 | Signet 8 | }; 9 | 10 | type OutPoint = record { 11 | txid : blob; 12 | vout : nat32 13 | }; 14 | 15 | // An unspent transaction output. 16 | type Utxo = record { 17 | outpoint: OutPoint; 18 | value: Satoshi; 19 | height: nat32; 20 | confirmations: nat32; 21 | }; 22 | 23 | type BuildTransactionError = variant { 24 | MalformedDestinationAddress; 25 | InsufficientBalance; 26 | MalformedSourceAddress; 27 | }; 28 | 29 | type SignTransactionError = variant { 30 | MalformedSourceAddress; 31 | MalformedTransaction; 32 | InvalidPrivateKeyWif; 33 | }; 34 | 35 | service: { 36 | // Given the private key, compute the P2PKH address. 37 | get_p2pkh_address: (private_key_wif: text, Network) -> (text) query; 38 | 39 | // Builds an unsigned transaction and returns the indices of used UTXOs. 40 | build_transaction: (utxos: vec Utxo, 41 | source_address: text, 42 | destination_address: text, 43 | amount: Satoshi, 44 | fees: Satoshi 45 | ) -> (variant { 46 | Ok : record { blob; vec nat64 }; 47 | Err : BuildTransactionError; 48 | }) query; 49 | 50 | // Creates a signed Bitcoin transaction from a previously built transaction. 51 | sign_transaction: ( 52 | private_key_wif: text, 53 | serialized_transaction: blob, 54 | source_address: text 55 | ) -> (variant { 56 | Ok : blob; 57 | Err: SignTransactionError 58 | }) query; 59 | 60 | } 61 | -------------------------------------------------------------------------------- /examples/common/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod types; 2 | 3 | use bitcoin::{ 4 | blockdata::script::Builder, 5 | hashes::Hash, 6 | secp256k1::{Message, Secp256k1}, 7 | Address, AddressType, Network, OutPoint, PrivateKey, Script, SigHashType, Transaction, TxIn, 8 | TxOut, Txid, 9 | }; 10 | use ic_btc_types::Utxo; 11 | use ic_cdk::print; 12 | 13 | // The signature hash type that is always used. 14 | const SIG_HASH_TYPE: SigHashType = SigHashType::All; 15 | 16 | pub fn get_p2pkh_address(private_key: &PrivateKey, network: Network) -> Address { 17 | let public_key = private_key.public_key(&Secp256k1::new()); 18 | Address::p2pkh(&public_key, network) 19 | } 20 | 21 | // Builds a transaction that sends the given `amount` of satoshis to the `destination` address. 22 | pub fn build_transaction( 23 | utxos: Vec, 24 | source: Address, 25 | destination: Address, 26 | amount: u64, 27 | fees: u64, 28 | ) -> Result { 29 | // Assume that any amount below this threshold is dust. 30 | const DUST_THRESHOLD: u64 = 10_000; 31 | 32 | // Select which UTXOs to spend. For now, we naively spend the first available UTXOs, 33 | // even if they were previously spent in a transaction. 34 | let mut utxos_to_spend = vec![]; 35 | let mut total_spent = 0; 36 | for utxo in utxos.into_iter() { 37 | total_spent += utxo.value; 38 | utxos_to_spend.push(utxo); 39 | if total_spent >= amount + fees { 40 | // We have enough inputs to cover the amount we want to spend. 41 | break; 42 | } 43 | } 44 | 45 | print(&format!("UTXOs to spend: {:?}", utxos_to_spend)); 46 | 47 | if total_spent < amount { 48 | return Err("Insufficient balance".to_string()); 49 | } 50 | 51 | let inputs: Vec = utxos_to_spend 52 | .into_iter() 53 | .map(|utxo| TxIn { 54 | previous_output: OutPoint { 55 | txid: Txid::from_hash(Hash::from_slice(&utxo.outpoint.txid).unwrap()), 56 | vout: utxo.outpoint.vout, 57 | }, 58 | sequence: 0xffffffff, 59 | witness: Vec::new(), 60 | script_sig: Script::new(), 61 | }) 62 | .collect(); 63 | 64 | let mut outputs = vec![TxOut { 65 | script_pubkey: destination.script_pubkey(), 66 | value: amount, 67 | }]; 68 | 69 | let remaining_amount = total_spent - amount - fees; 70 | 71 | if remaining_amount >= DUST_THRESHOLD { 72 | outputs.push(TxOut { 73 | script_pubkey: source.script_pubkey(), 74 | value: remaining_amount, 75 | }); 76 | } 77 | 78 | Ok(Transaction { 79 | input: inputs, 80 | output: outputs, 81 | lock_time: 0, 82 | version: 2, 83 | }) 84 | } 85 | 86 | /// Sign a bitcoin transaction given the private key and the source address of the funds. 87 | /// 88 | /// Constraints: 89 | /// * All the inputs are referencing outpoints that are owned by `src_address`. 90 | /// * `src_address` is a P2PKH address. 91 | pub fn sign_transaction( 92 | mut transaction: Transaction, 93 | private_key: PrivateKey, 94 | src_address: Address, 95 | ) -> Transaction { 96 | // Verify that the address is P2PKH. The signature algorithm below is specific to P2PKH. 97 | match src_address.address_type() { 98 | Some(AddressType::P2pkh) => {} 99 | _ => panic!("This demo supports signing p2pkh addresses only."), 100 | }; 101 | 102 | let secp = Secp256k1::new(); 103 | let txclone = transaction.clone(); 104 | let public_key = private_key.public_key(&Secp256k1::new()); 105 | 106 | for (index, input) in transaction.input.iter_mut().enumerate() { 107 | let sighash = 108 | txclone.signature_hash(index, &src_address.script_pubkey(), SIG_HASH_TYPE.as_u32()); 109 | 110 | let signature = secp 111 | .sign( 112 | &Message::from_slice(&sighash[..]).unwrap(), 113 | &private_key.key, 114 | ) 115 | .serialize_der(); 116 | 117 | let mut sig_with_hashtype = signature.to_vec(); 118 | sig_with_hashtype.push(SIG_HASH_TYPE.as_u32() as u8); 119 | input.script_sig = Builder::new() 120 | .push_slice(sig_with_hashtype.as_slice()) 121 | .push_slice(public_key.to_bytes().as_slice()) 122 | .into_script(); 123 | input.witness.clear(); 124 | } 125 | 126 | transaction 127 | } 128 | -------------------------------------------------------------------------------- /examples/common/src/main.rs: -------------------------------------------------------------------------------- 1 | //! A simple candid API for Motoko canisters 2 | //! 3 | //! Some fundamental crypto primitives are still missing in Motoko, and that 4 | //! makes some fundamental operations, like computing a Bitcoin address and 5 | //! signing a transaction impossible in Motoko. 6 | //! 7 | //! The following is a work-around until the crypto primitives become available 8 | //! to canisters. We wrap all the functionality requiring cryptography into a 9 | //! canister, and Motoko developers can deploy this canister and interact with it 10 | //! for address computation and transaction signing. 11 | use bitcoin::{ 12 | consensus::deserialize, util::psbt::serialize::Serialize, Address, Network, PrivateKey, 13 | Transaction, 14 | }; 15 | use example_common::types::{BuildTransactionError, SignTransactionError}; 16 | use ic_btc_types::{OutPoint, Utxo}; 17 | use ic_cdk::export::candid::{candid_method, CandidType, Deserialize}; 18 | use ic_cdk_macros::query; 19 | use std::{collections::HashMap, str::FromStr}; 20 | 21 | #[derive(CandidType, Deserialize, Copy, Clone)] 22 | pub enum NetworkCandid { 23 | Bitcoin, 24 | Regtest, 25 | Testnet, 26 | Signet, 27 | } 28 | 29 | // Returns the P2PKH address of the given private key. 30 | #[query] 31 | #[candid_method(query)] 32 | fn get_p2pkh_address(private_key_wif: String, network: NetworkCandid) -> String { 33 | let private_key = PrivateKey::from_wif(&private_key_wif).expect("Invalid private key WIF"); 34 | let network = match network { 35 | NetworkCandid::Bitcoin => Network::Bitcoin, 36 | NetworkCandid::Testnet => Network::Testnet, 37 | NetworkCandid::Regtest => Network::Regtest, 38 | NetworkCandid::Signet => Network::Signet, 39 | }; 40 | example_common::get_p2pkh_address(&private_key, network).to_string() 41 | } 42 | 43 | // Returns the transaction as serialized bytes and the UTXO indices used for the transaction. 44 | #[query] 45 | #[candid_method(query)] 46 | fn build_transaction( 47 | utxos: Vec, 48 | source_address: String, 49 | destination_address: String, 50 | amount: u64, 51 | fees: u64, 52 | ) -> Result<(Vec, Vec), BuildTransactionError> { 53 | let source_address = Address::from_str(&source_address) 54 | .map_err(|_| BuildTransactionError::MalformedSourceAddress)?; 55 | let destination_address = Address::from_str(&destination_address) 56 | .map_err(|_| BuildTransactionError::MalformedDestinationAddress)?; 57 | let outpoint_to_index: HashMap = utxos 58 | .iter() 59 | .enumerate() 60 | .map(|(idx, utxo)| (utxo.outpoint.clone(), idx)) 61 | .collect(); 62 | let tx = example_common::build_transaction( 63 | utxos, 64 | source_address.clone(), 65 | destination_address, 66 | amount, 67 | fees, 68 | ) 69 | .map_err(|_| BuildTransactionError::InsufficientBalance)?; 70 | let used_utxo_indices: Vec = tx 71 | .input 72 | .iter() 73 | .filter_map(|tx_in| { 74 | let outpoint = OutPoint { 75 | txid: tx_in.previous_output.txid.to_vec(), 76 | vout: tx_in.previous_output.vout, 77 | }; 78 | outpoint_to_index.get(&outpoint).cloned() 79 | }) 80 | .collect(); 81 | 82 | Ok((tx.serialize(), used_utxo_indices)) 83 | } 84 | 85 | #[query] 86 | #[candid_method(query)] 87 | fn sign_transaction( 88 | private_key_wif: String, 89 | serialized_transaction: Vec, 90 | source_address: String, 91 | ) -> Result, SignTransactionError> { 92 | let private_key = PrivateKey::from_wif(&private_key_wif) 93 | .map_err(|_| SignTransactionError::InvalidPrivateKeyWif)?; 94 | let source_address = Address::from_str(&source_address) 95 | .map_err(|_| SignTransactionError::MalformedSourceAddress)?; 96 | let tx: Transaction = deserialize(serialized_transaction.as_slice()) 97 | .map_err(|_| SignTransactionError::MalformedTransaction)?; 98 | Ok(example_common::sign_transaction(tx, private_key, source_address).serialize()) 99 | } 100 | 101 | fn main() {} 102 | 103 | #[cfg(test)] 104 | mod test { 105 | use super::*; 106 | 107 | #[test] 108 | fn check_candid_interface_compatibility() { 109 | use candid::types::subtype::{subtype, Gamma}; 110 | use candid::types::Type; 111 | use ic_cdk::export::candid::{self}; 112 | use std::io::Write; 113 | use std::path::PathBuf; 114 | 115 | candid::export_service!(); 116 | 117 | let actual_interface = __export_service(); 118 | println!("Generated DID:\n {}", actual_interface); 119 | let mut tmp = tempfile::NamedTempFile::new().expect("failed to create a temporary file"); 120 | write!(tmp, "{}", actual_interface).expect("failed to write interface to a temporary file"); 121 | let (mut env1, t1) = 122 | candid::pretty_check_file(tmp.path()).expect("failed to check generated candid file"); 123 | let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("candid.did"); 124 | let (env2, t2) = 125 | candid::pretty_check_file(path.as_path()).expect("failed to open candid.did file"); 126 | 127 | let (t1_ref, t2) = match (t1.as_ref().unwrap(), t2.unwrap()) { 128 | (Type::Class(_, s1), Type::Class(_, s2)) => (s1.as_ref(), *s2), 129 | (Type::Class(_, s1), s2 @ Type::Service(_)) => (s1.as_ref(), s2), 130 | (s1 @ Type::Service(_), Type::Class(_, s2)) => (s1, *s2), 131 | (t1, t2) => (t1, t2), 132 | }; 133 | 134 | let mut gamma = Gamma::new(); 135 | let t2 = env1.merge_type(env2, t2); 136 | subtype(&mut gamma, &env1, t1_ref, &t2) 137 | .expect("canister interface is not compatible with the candid.did file"); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /examples/common/src/types.rs: -------------------------------------------------------------------------------- 1 | use ic_cdk::export::candid::{CandidType, Deserialize}; 2 | 3 | #[derive(CandidType, Deserialize)] 4 | pub enum BuildTransactionError { 5 | InsufficientBalance, 6 | MalformedDestinationAddress, 7 | MalformedSourceAddress, 8 | } 9 | 10 | #[derive(CandidType, Deserialize)] 11 | pub enum SignTransactionError { 12 | InvalidPrivateKeyWif, 13 | MalformedSourceAddress, 14 | MalformedTransaction, 15 | } 16 | -------------------------------------------------------------------------------- /examples/motoko/README.adoc: -------------------------------------------------------------------------------- 1 | = Motoko Example 2 | 3 | The README in `./examples` contains the documentation. 4 | -------------------------------------------------------------------------------- /examples/motoko/src/Main.mo: -------------------------------------------------------------------------------- 1 | import Array "mo:base/Array"; 2 | import Debug "mo:base/Debug"; 3 | import Error "mo:base/Error"; 4 | import Principal "mo:base/Principal"; 5 | import Nat64 "mo:base/Nat64"; 6 | import Result "mo:base/Result"; 7 | import TrieSet "mo:base/TrieSet"; 8 | 9 | import Common "canister:btc-example-common"; 10 | import Types "Types"; 11 | import Utils "Utils"; 12 | 13 | 14 | actor class Self(payload : Types.InitPayload) { 15 | 16 | // Actor definition to handle interactions with the BTC canister. 17 | type BTC = actor { 18 | // Gets the balance from the BTC canister. 19 | get_balance : Types.GetBalanceRequest -> async Types.GetBalanceResponse; 20 | // Retrieves the UTXOs from the BTC canister. 21 | get_utxos : Types.GetUtxosRequest -> async Types.GetUtxosResponse; 22 | // Sends a transaction to the BTC canister. 23 | send_transaction : (Types.SendTransactionRequest) -> async Types.SendTransactionResponse; 24 | }; 25 | 26 | // The canister's private key in "Wallet Import Format". 27 | let PRIVATE_KEY_WIF : Text = "L2C1QgyKqNgfV7BpEPAm6PVn2xW8zpXq6MojSbWdH18nGQF2wGsT"; 28 | // Used to interact with the BTC canister. 29 | let btc : BTC = actor(Principal.toText(payload.bitcoin_canister_id)); 30 | // Stores outpoints the have been spent. 31 | let spent_outpoints : Utils.OutPointSet = Utils.OutPointSet(); 32 | 33 | // Retrieves the BTC address using the common canister. 34 | public func btc_address() : async Text { 35 | await Common.get_p2pkh_address(PRIVATE_KEY_WIF, #Regtest) 36 | }; 37 | 38 | // Retrieves the canister's balance from the BTC canister. 39 | public func balance() : async Result.Result { 40 | let address : Text = await btc_address(); 41 | switch (await btc.get_balance({ address=address; min_confirmations=?0 })) { 42 | case (#Ok(satoshi)) { 43 | #ok(satoshi) 44 | }; 45 | case (#Err(err)) { 46 | #err(err) 47 | }; 48 | } 49 | }; 50 | 51 | // Used to retrieve the UTXOs and process the response. 52 | func get_utxos_internal(address : Text) : async Result.Result { 53 | let result = await btc.get_utxos({ 54 | address=address; 55 | min_confirmations=?0 56 | }); 57 | switch (result) { 58 | case (#Ok(response)) { 59 | #ok(response) 60 | }; 61 | case (#Err(err)) { 62 | #err(err) 63 | }; 64 | } 65 | }; 66 | 67 | // Exposes the `get_utxos_internal` as and endpoint. 68 | public func get_utxos() : async Result.Result { 69 | let address : Text = await btc_address(); 70 | await get_utxos_internal(address) 71 | }; 72 | 73 | func is_spent_outpoint(utxo : Types.Utxo) : Bool { 74 | not spent_outpoints.contains(utxo.outpoint) 75 | }; 76 | 77 | // Allows Bitcoin to be sent from the canister to a BTC address. 78 | public func send(amount: Types.Satoshi, destination: Text) : async Result.Result<(), Types.SendError> { 79 | // Assuming a fixed fee of 10k satoshis. 80 | let fees : Nat64 = 10_000; 81 | let source : Text = await btc_address(); 82 | let utxos_response = await get_utxos_internal(source); 83 | let utxos_data = switch (utxos_response) { 84 | case (#ok(data)) { 85 | data 86 | }; 87 | case (#err(?error)) { 88 | switch (error) { 89 | case (#MalformedAddress) { 90 | return #err(#MalformedSourceAddress); 91 | } 92 | } 93 | }; 94 | case (#err(null)) { 95 | return #err(#Unknown); 96 | }; 97 | }; 98 | 99 | let filtered_utxos = Array.filter(utxos_data.utxos, is_spent_outpoint); 100 | if (filtered_utxos.size() == 0) { 101 | return #err(#InsufficientBalance); 102 | }; 103 | 104 | let build_transaction_result = await Common.build_transaction(filtered_utxos, source, destination, amount, fees); 105 | let (tx, used_utxo_indices) = switch (build_transaction_result) { 106 | case (#Ok(result)) { 107 | result 108 | }; 109 | case (#Err(error)) { 110 | return #err(error); 111 | }; 112 | }; 113 | 114 | for (index in used_utxo_indices.vals()) { 115 | let i : Nat = Nat64.toNat(index); 116 | spent_outpoints.add(filtered_utxos[i].outpoint); 117 | }; 118 | 119 | let sign_transaction_result = await Common.sign_transaction(PRIVATE_KEY_WIF, tx, source); 120 | let signed_tx = switch (sign_transaction_result) { 121 | case (#Ok(signed_tx)) { 122 | signed_tx 123 | }; 124 | case (#Err(error)) { 125 | return #err(error); 126 | }; 127 | }; 128 | 129 | let send_transaction_response = await btc.send_transaction({ transaction=signed_tx }); 130 | switch (send_transaction_response) { 131 | case (#Ok) { 132 | #ok(()) 133 | }; 134 | case (#Err(?error)) { 135 | #err(error); 136 | }; 137 | case (#Err(null)) { 138 | #err(#Unknown); 139 | }; 140 | } 141 | }; 142 | }; 143 | -------------------------------------------------------------------------------- /examples/motoko/src/Types.mo: -------------------------------------------------------------------------------- 1 | // This module contains types needed to interact with the BTC and Common canisters 2 | // along with the typing needed for this canister. 3 | module Types { 4 | 5 | // Used to initialize the example dapp. 6 | public type InitPayload = { 7 | bitcoin_canister_id : Principal; 8 | }; 9 | 10 | public type SendError = { 11 | #MalformedSourceAddress; 12 | #MalformedDestinationAddress; 13 | #MalformedTransaction; 14 | #InsufficientBalance; 15 | #InvalidPrivateKeyWif; 16 | #Unknown; 17 | }; 18 | 19 | // Types to interact with the Bitcoin and Common canisters. 20 | 21 | // A single unit of Bitcoin 22 | public type Satoshi = Nat64; 23 | 24 | // The type of Bitcoin network the dapp will be interacting with. 25 | public type Network = { 26 | #Bitcoin; 27 | #Regtest; 28 | #Testnet; 29 | #Signet; 30 | }; 31 | 32 | // A reference to a transaction output. 33 | public type OutPoint = { 34 | txid : Blob; 35 | vout : Nat32; 36 | }; 37 | 38 | // An unspent transaction output. 39 | public type Utxo = { 40 | outpoint : OutPoint; 41 | value : Satoshi; 42 | height : Nat32; 43 | confirmations : Nat32; 44 | }; 45 | 46 | public type GetUtxosRequest = { 47 | address : Text; 48 | min_confirmations : ?Nat32; 49 | }; 50 | 51 | public type GetUtxosData = { 52 | utxos : [Utxo]; 53 | total_count : Nat32; 54 | }; 55 | 56 | public type GetUtxosResponse = { 57 | #Ok : GetUtxosData; 58 | #Err : ?GetUtxosError; 59 | }; 60 | 61 | public type GetUtxosError = { 62 | #MalformedAddress; 63 | }; 64 | 65 | public type GetBalanceRequest = { 66 | address : Text; 67 | min_confirmations : ?Nat32; 68 | }; 69 | 70 | public type GetBalanceResponse = { 71 | #Ok : Nat64; 72 | #Err : ?GetBalanceError; 73 | }; 74 | 75 | public type GetBalanceError = { 76 | #MalformedAddress; 77 | }; 78 | 79 | public type SendTransactionResponse = { 80 | #Ok; 81 | #Err : ?SendTransactionError; 82 | }; 83 | 84 | public type SendTransactionRequest = { 85 | transaction : Blob; 86 | }; 87 | 88 | public type SendTransactionError = { 89 | #MalformedTransaction; 90 | }; 91 | 92 | } 93 | -------------------------------------------------------------------------------- /examples/motoko/src/Utils.mo: -------------------------------------------------------------------------------- 1 | import Array "mo:base/Array"; 2 | import Blob "mo:base/Blob"; 3 | import Hash "mo:base/Hash"; 4 | import Iter "mo:base/Iter"; 5 | import Nat "mo:base/Nat"; 6 | import Nat8 "mo:base/Nat8"; 7 | import Nat32 "mo:base/Nat32"; 8 | import TrieSet "mo:base/TrieSet"; 9 | 10 | import Types "Types"; 11 | 12 | module { 13 | 14 | func nat8ToNat32 (n: Nat8) : Nat32 { 15 | Nat32.fromNat(Nat8.toNat(n)) 16 | }; 17 | 18 | /// Returns a hash obtained by using the `djb2` algorithm from http://www.cse.yorku.ca/~oz/hash.html 19 | /// 20 | /// Modified version of Text.hash for Types.OutPoint. 21 | public func hashOutPoint(outpoint : Types.OutPoint) : Hash.Hash { 22 | let outpoint_data : [Nat32] = Array.append(Array.map(Blob.toArray(outpoint.txid), nat8ToNat32), [outpoint.vout]); 23 | var x : Nat32 = 5381; 24 | for (c in outpoint_data.vals()) { 25 | x := ((x << 5) +% x) +% c; 26 | }; 27 | x 28 | }; 29 | 30 | /// Checks if the outpoints are equal. 31 | public func areOutPointsEqual(o1 : Types.OutPoint, o2 : Types.OutPoint) : Bool { 32 | if (o1.vout != o2.vout) { 33 | return false; 34 | }; 35 | 36 | Blob.equal(o1.txid, o2.txid) 37 | }; 38 | 39 | /// A set that contains outpoints for tracking if an outpoint has been spent. 40 | public class OutPointSet () { 41 | 42 | var _set : TrieSet.Set = TrieSet.empty(); 43 | 44 | /// Adds an outpoint to the set. 45 | public func add(outpoint : Types.OutPoint) { 46 | let s2 = TrieSet.put(_set, outpoint, hashOutPoint(outpoint), areOutPointsEqual); 47 | _set := s2; 48 | }; 49 | 50 | /// Checks if the outpoint is in the set. 51 | public func contains(outpoint : Types.OutPoint) : Bool { 52 | TrieSet.mem(_set, outpoint, hashOutPoint(outpoint), areOutPointsEqual) 53 | }; 54 | 55 | }; 56 | 57 | } 58 | -------------------------------------------------------------------------------- /examples/rust/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | bitcoin = "0.27.1" 10 | example-common = { path = "../common" } 11 | hex = "0.4.3" 12 | ic-btc-types = { path = "../../types" } 13 | ic-cdk = "0.3.1" 14 | ic-cdk-macros = "0.3.1" 15 | serde = "1.0.132" 16 | 17 | [dev-dependencies] 18 | bitcoin = { version = "0.27.1", features = ["rand"] } 19 | -------------------------------------------------------------------------------- /examples/rust/README.adoc: -------------------------------------------------------------------------------- 1 | = Rust Example 2 | 3 | The README in `./examples` contains the documentation. 4 | 5 | -------------------------------------------------------------------------------- /examples/rust/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | TARGET="wasm32-unknown-unknown" 5 | SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 6 | 7 | pushd $SCRIPT_DIR 8 | 9 | # NOTE: On macOS a specific version of llvm-ar and clang need to be set here. 10 | # Otherwise the wasm compilation of rust-secp256k1 will fail. 11 | if [ "$(uname)" == "Darwin" ]; then 12 | # On macs we need to use the brew versions 13 | AR="/usr/local/opt/llvm/bin/llvm-ar" CC="/usr/local/opt/llvm/bin/clang" cargo build --target $TARGET --release 14 | else 15 | cargo build --target $TARGET --release 16 | fi 17 | 18 | cargo install ic-cdk-optimizer --version 0.3.1 --root "$SCRIPT_DIR"/../target 19 | STATUS=$? 20 | 21 | if [ "$STATUS" -eq "0" ]; then 22 | "$SCRIPT_DIR"/../target/bin/ic-cdk-optimizer \ 23 | "$SCRIPT_DIR"/target/$TARGET/release/example.wasm \ 24 | -o "$SCRIPT_DIR"/target/$TARGET/release/example.wasm 25 | true 26 | else 27 | echo Could not install ic-cdk-optimizer. 28 | false 29 | fi 30 | 31 | popd 32 | -------------------------------------------------------------------------------- /examples/rust/candid.did: -------------------------------------------------------------------------------- 1 | type Satoshi = nat64; 2 | 3 | type InitPayload = record { 4 | bitcoin_canister_id : principal; 5 | }; 6 | 7 | type OutPoint = record { 8 | txid : blob; 9 | vout : nat32 10 | }; 11 | 12 | type Utxo = record { 13 | outpoint: OutPoint; 14 | value: Satoshi; 15 | height: nat32; 16 | confirmations: nat32; 17 | }; 18 | 19 | service: (InitPayload) -> { 20 | btc_address: () -> (text) query; 21 | balance: () -> (nat64); 22 | get_utxos: () -> (vec Utxo); 23 | send: (amount: nat64, destination: text) -> (); 24 | } 25 | 26 | -------------------------------------------------------------------------------- /examples/rust/src/main.rs: -------------------------------------------------------------------------------- 1 | use bitcoin::{util::psbt::serialize::Serialize, Address, Network, PrivateKey}; 2 | use example_common::{build_transaction, get_p2pkh_address, sign_transaction}; 3 | use ic_btc_types::{ 4 | GetBalanceError, GetBalanceRequest, GetUtxosError, GetUtxosRequest, GetUtxosResponse, OutPoint, 5 | SendTransactionRequest, Utxo, 6 | }; 7 | use ic_cdk::{ 8 | api::call::RejectionCode, 9 | call, 10 | export::{ 11 | candid::{CandidType, Deserialize}, 12 | Principal, 13 | }, 14 | print, trap, 15 | }; 16 | use ic_cdk_macros::{init, query, update}; 17 | use std::{cell::RefCell, collections::HashSet, str::FromStr}; 18 | 19 | // A private key in WIF (wallet import format). This is only for demonstrational purposes. 20 | // When the Bitcoin integration is released on mainnet, canisters will have the ability 21 | // to securely generate ECDSA keys. 22 | const BTC_PRIVATE_KEY_WIF: &str = "L2C1QgyKqNgfV7BpEPAm6PVn2xW8zpXq6MojSbWdH18nGQF2wGsT"; 23 | 24 | thread_local! { 25 | static BTC_PRIVATE_KEY: RefCell = 26 | RefCell::new(PrivateKey::from_wif(BTC_PRIVATE_KEY_WIF).unwrap()); 27 | 28 | // The ID of the bitcoin canister that is installed locally. 29 | // The value here is initialized with a dummy value, which will be overwritten in `init`. 30 | static BTC_CANISTER_ID: RefCell = RefCell::new(Principal::management_canister()); 31 | 32 | // A cache of spent outpoints. Needed to avoid double spending. 33 | static SPENT_TXOS: RefCell> = RefCell::new(HashSet::new()); 34 | } 35 | 36 | #[derive(CandidType, Deserialize)] 37 | struct InitPayload { 38 | bitcoin_canister_id: Principal, 39 | } 40 | 41 | #[init] 42 | fn init(payload: InitPayload) { 43 | BTC_CANISTER_ID.with(|id| { 44 | // Set the ID fo the bitcoin canister. 45 | id.replace(payload.bitcoin_canister_id); 46 | }) 47 | } 48 | 49 | /// Returns the regtest P2PKH address derived from the private key as a string. 50 | /// P2PKH was chosen for demonstrational purposes. Other address types can also be used. 51 | #[query(name = "btc_address")] 52 | pub fn btc_address_str() -> String { 53 | btc_address().to_string() 54 | } 55 | 56 | /// Returns the UTXOs of the canister's BTC address. 57 | #[update] 58 | pub async fn get_utxos() -> Vec { 59 | let btc_canister_id = BTC_CANISTER_ID.with(|id| *id.borrow()); 60 | #[allow(clippy::type_complexity)] 61 | let res: Result< 62 | (Result>,), 63 | (RejectionCode, String), 64 | > = call( 65 | btc_canister_id, 66 | "get_utxos", 67 | (GetUtxosRequest { 68 | address: btc_address_str(), 69 | min_confirmations: Some(0), 70 | },), 71 | ) 72 | .await; 73 | 74 | match res { 75 | // Return the UTXOs to the caller. 76 | Ok((Ok(data),)) => data.utxos, 77 | 78 | // The call to `get_utxos` returned an error. 79 | Ok((Err(err),)) => trap(&format!("Received error from Bitcoin canister: {:?}", err)), 80 | 81 | // The call to `get_utxos` was rejected. 82 | // This is only likely to happen if there's a bug in the bitcoin canister. 83 | Err((rejection_code, message)) => trap(&format!( 84 | "Received a reject from Bitcoin canister.\nRejection Code: {:?}\nMessage: '{}'", 85 | rejection_code, message 86 | )), 87 | } 88 | } 89 | 90 | /// Returns the canister's balance. 91 | #[update] 92 | pub async fn balance() -> u64 { 93 | let btc_canister_id = BTC_CANISTER_ID.with(|id| *id.borrow()); 94 | let res: Result<(Result,), (RejectionCode, String)> = call( 95 | btc_canister_id, 96 | "get_balance", 97 | (GetBalanceRequest { 98 | address: btc_address_str(), 99 | min_confirmations: Some(0), 100 | },), 101 | ) 102 | .await; 103 | 104 | match res { 105 | // Return the balance to the caller. 106 | Ok((Ok(balance),)) => balance, 107 | 108 | // The call to `get_balance` returned an error. 109 | Ok((Err(err),)) => trap(&format!("Received error from Bitcoin canister: {:?}", err)), 110 | 111 | // The call to `get_balance` was rejected. 112 | // This is only likely to happen if there's a bug in the bitcoin canister. 113 | Err((rejection_code, message)) => trap(&format!( 114 | "Received a reject from Bitcoin canister.\nRejection Code: {:?}\nMessage: '{}'", 115 | rejection_code, message 116 | )), 117 | } 118 | } 119 | 120 | /// Send the `amount` of satoshis provided to the `destination` address. 121 | /// 122 | /// Notes: 123 | /// * Fees are hard-coded to 10k satoshis. 124 | /// * Input UTXOs are not being selected in any smart way. 125 | /// * A dust threshold of 10k satoshis is used. 126 | #[update] 127 | pub async fn send(amount: u64, destination: String) { 128 | let fees: u64 = 10_000; 129 | 130 | if amount <= fees { 131 | trap("Amount must be higher than the fee of 10,000 satoshis") 132 | } 133 | 134 | let destination = match Address::from_str(&destination) { 135 | Ok(destination) => destination, 136 | Err(_) => trap("Invalid destination address"), 137 | }; 138 | 139 | // Fetch our UTXOs. 140 | let utxos = get_utxos().await; 141 | 142 | // Remove any spent UTXOs that were already used for past transactions. 143 | let utxos = utxos 144 | .into_iter() 145 | .filter(|utxo| SPENT_TXOS.with(|spent_txos| !spent_txos.borrow().contains(&utxo.outpoint))) 146 | .collect(); 147 | 148 | let spending_transaction = build_transaction(utxos, btc_address(), destination, amount, fees) 149 | .unwrap_or_else(|err| { 150 | trap(&format!("Error building transaction: {}", err)); 151 | }); 152 | 153 | // Cache the spent outputs to not use them for future transactions. 154 | for tx_in in spending_transaction.input.iter() { 155 | SPENT_TXOS.with(|spent_txos| { 156 | print(&format!("Caching {:?}", tx_in.previous_output.txid.to_vec())); 157 | spent_txos.borrow_mut().insert(OutPoint { 158 | txid: tx_in.previous_output.txid.to_vec(), 159 | vout: tx_in.previous_output.vout, 160 | }) 161 | }); 162 | } 163 | 164 | print(&format!( 165 | "Transaction to sign: {}", 166 | hex::encode(spending_transaction.serialize()) 167 | )); 168 | 169 | // Sign transaction 170 | let private_key = BTC_PRIVATE_KEY.with(|private_key| *private_key.borrow()); 171 | let signed_transaction = sign_transaction(spending_transaction, private_key, btc_address()); 172 | 173 | let signed_transaction_bytes = signed_transaction.serialize(); 174 | print(&format!( 175 | "Signed transaction: {}", 176 | hex::encode(signed_transaction_bytes.clone()) 177 | )); 178 | 179 | let btc_canister_id = BTC_CANISTER_ID.with(|id| *id.borrow()); 180 | 181 | print("Sending transaction"); 182 | 183 | let _: Result<(), (RejectionCode, String)> = call( 184 | btc_canister_id, 185 | "send_transaction", 186 | (SendTransactionRequest { 187 | transaction: signed_transaction_bytes, 188 | },), 189 | ) 190 | .await; 191 | } 192 | 193 | // Returns the regtest P2PKH address derived from the private key. 194 | fn btc_address() -> Address { 195 | BTC_PRIVATE_KEY.with(|private_key| get_p2pkh_address(&private_key.borrow(), Network::Regtest)) 196 | } 197 | 198 | fn main() {} 199 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.57.0" 3 | targets = ["wasm32-unknown-unknown"] 4 | -------------------------------------------------------------------------------- /scripts/build-canister.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | TARGET="wasm32-unknown-unknown" 5 | SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 6 | 7 | pushd $SCRIPT_DIR/.. 8 | 9 | # NOTE: On macOS a specific version of llvm-ar and clang need to be set here. 10 | # Otherwise the wasm compilation of rust-secp256k1 will fail. 11 | if [ "$(uname)" == "Darwin" ]; then 12 | # On macs we need to use the brew versions 13 | AR="/usr/local/opt/llvm/bin/llvm-ar" CC="/usr/local/opt/llvm/bin/clang" cargo build --bin canister --target $TARGET --release 14 | else 15 | cargo build --bin canister --target $TARGET --release 16 | fi 17 | 18 | cargo install ic-cdk-optimizer --version 0.3.1 --root ./target 19 | STATUS=$? 20 | 21 | if [ "$STATUS" -eq "0" ]; then 22 | ./target/bin/ic-cdk-optimizer \ 23 | ./target/$TARGET/release/canister.wasm \ 24 | -o ./target/$TARGET/release/canister.wasm 25 | true 26 | else 27 | echo Could not install ic-cdk-optimizer. 28 | false 29 | fi 30 | 31 | popd 32 | 33 | -------------------------------------------------------------------------------- /scripts/build-example-common.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -xeuo pipefail 3 | 4 | TARGET="wasm32-unknown-unknown" 5 | SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 6 | 7 | pushd $SCRIPT_DIR/.. 8 | 9 | # NOTE: On macOS a specific version of llvm-ar and clang need to be set here. 10 | # Otherwise the wasm compilation of rust-secp256k1 will fail. 11 | if [ "$(uname)" == "Darwin" ]; then 12 | # On macs we need to use the brew versions 13 | AR="/usr/local/opt/llvm/bin/llvm-ar" CC="/usr/local/opt/llvm/bin/clang" cargo build --bin example-common --target $TARGET --release 14 | else 15 | cargo build --bin example-common --target $TARGET --release 16 | fi 17 | 18 | cargo install ic-cdk-optimizer --version 0.3.1 --root ./target 19 | STATUS=$? 20 | 21 | if [ "$STATUS" -eq "0" ]; then 22 | ./target/bin/ic-cdk-optimizer \ 23 | ./target/$TARGET/release/example-common.wasm \ 24 | -o ./target/$TARGET/release/example-common.wasm 25 | true 26 | else 27 | echo Could not install ic-cdk-optimizer. 28 | false 29 | fi 30 | 31 | popd 32 | -------------------------------------------------------------------------------- /scripts/build-example.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -xeuo pipefail 3 | 4 | TARGET="wasm32-unknown-unknown" 5 | SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 6 | 7 | pushd $SCRIPT_DIR/.. 8 | 9 | # NOTE: On macOS a specific version of llvm-ar and clang need to be set here. 10 | # Otherwise the wasm compilation of rust-secp256k1 will fail. 11 | if [ "$(uname)" == "Darwin" ]; then 12 | # On macs we need to use the brew versions 13 | AR="/usr/local/opt/llvm/bin/llvm-ar" CC="/usr/local/opt/llvm/bin/clang" cargo build --bin example --target $TARGET --release 14 | else 15 | cargo build --bin example --target $TARGET --release 16 | fi 17 | 18 | cargo install ic-cdk-optimizer --version 0.3.1 --root ./target 19 | STATUS=$? 20 | 21 | if [ "$STATUS" -eq "0" ]; then 22 | ./target/bin/ic-cdk-optimizer \ 23 | ./target/$TARGET/release/example.wasm \ 24 | -o ./target/$TARGET/release/example.wasm 25 | true 26 | else 27 | echo Could not install ic-cdk-optimizer. 28 | false 29 | fi 30 | 31 | popd 32 | -------------------------------------------------------------------------------- /types/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "0.7.18" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "anyhow" 16 | version = "1.0.52" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "84450d0b4a8bd1ba4144ce8ce718fbc5d071358b1e5384bace6536b3d1f2d5b3" 19 | 20 | [[package]] 21 | name = "arrayvec" 22 | version = "0.5.2" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" 25 | 26 | [[package]] 27 | name = "ascii-canvas" 28 | version = "3.0.0" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "8824ecca2e851cec16968d54a01dd372ef8f95b244fb84b84e70128be347c3c6" 31 | dependencies = [ 32 | "term", 33 | ] 34 | 35 | [[package]] 36 | name = "atty" 37 | version = "0.2.14" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 40 | dependencies = [ 41 | "hermit-abi", 42 | "libc", 43 | "winapi", 44 | ] 45 | 46 | [[package]] 47 | name = "autocfg" 48 | version = "1.0.1" 49 | source = "registry+https://github.com/rust-lang/crates.io-index" 50 | checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" 51 | 52 | [[package]] 53 | name = "base32" 54 | version = "0.4.0" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" 57 | 58 | [[package]] 59 | name = "beef" 60 | version = "0.5.1" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | checksum = "bed554bd50246729a1ec158d08aa3235d1b69d94ad120ebe187e28894787e736" 63 | 64 | [[package]] 65 | name = "binread" 66 | version = "2.2.0" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "16598dfc8e6578e9b597d9910ba2e73618385dc9f4b1d43dd92c349d6be6418f" 69 | dependencies = [ 70 | "binread_derive", 71 | "lazy_static", 72 | "rustversion", 73 | ] 74 | 75 | [[package]] 76 | name = "binread_derive" 77 | version = "2.1.0" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "1d9672209df1714ee804b1f4d4f68c8eb2a90b1f7a07acf472f88ce198ef1fed" 80 | dependencies = [ 81 | "either", 82 | "proc-macro2", 83 | "quote", 84 | "syn", 85 | ] 86 | 87 | [[package]] 88 | name = "bit-set" 89 | version = "0.5.2" 90 | source = "registry+https://github.com/rust-lang/crates.io-index" 91 | checksum = "6e11e16035ea35e4e5997b393eacbf6f63983188f7a2ad25bfb13465f5ad59de" 92 | dependencies = [ 93 | "bit-vec", 94 | ] 95 | 96 | [[package]] 97 | name = "bit-vec" 98 | version = "0.6.3" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" 101 | 102 | [[package]] 103 | name = "bitflags" 104 | version = "1.3.2" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 107 | 108 | [[package]] 109 | name = "block-buffer" 110 | version = "0.9.0" 111 | source = "registry+https://github.com/rust-lang/crates.io-index" 112 | checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" 113 | dependencies = [ 114 | "generic-array", 115 | ] 116 | 117 | [[package]] 118 | name = "byteorder" 119 | version = "1.4.3" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 122 | 123 | [[package]] 124 | name = "candid" 125 | version = "0.7.10" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "12970d8d0620d2bdb7e81a5b13ed11e41fcdfeba53d61e45b5853afcbf9611fd" 128 | dependencies = [ 129 | "anyhow", 130 | "binread", 131 | "byteorder", 132 | "candid_derive", 133 | "codespan-reporting", 134 | "hex", 135 | "ic-types", 136 | "lalrpop", 137 | "lalrpop-util", 138 | "leb128", 139 | "logos", 140 | "num-bigint", 141 | "num-traits", 142 | "num_enum", 143 | "paste", 144 | "pretty", 145 | "serde", 146 | "serde_bytes", 147 | "thiserror", 148 | ] 149 | 150 | [[package]] 151 | name = "candid_derive" 152 | version = "0.4.5" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "2e02c03c4d547674a3f3f3109538fb49871fbe636216daa019f06a62faca9061" 155 | dependencies = [ 156 | "lazy_static", 157 | "proc-macro2", 158 | "quote", 159 | "syn", 160 | ] 161 | 162 | [[package]] 163 | name = "cfg-if" 164 | version = "1.0.0" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 167 | 168 | [[package]] 169 | name = "codespan-reporting" 170 | version = "0.11.1" 171 | source = "registry+https://github.com/rust-lang/crates.io-index" 172 | checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" 173 | dependencies = [ 174 | "termcolor", 175 | "unicode-width", 176 | ] 177 | 178 | [[package]] 179 | name = "cpufeatures" 180 | version = "0.2.1" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469" 183 | dependencies = [ 184 | "libc", 185 | ] 186 | 187 | [[package]] 188 | name = "crc32fast" 189 | version = "1.3.0" 190 | source = "registry+https://github.com/rust-lang/crates.io-index" 191 | checksum = "738c290dfaea84fc1ca15ad9c168d083b05a714e1efddd8edaab678dc28d2836" 192 | dependencies = [ 193 | "cfg-if", 194 | ] 195 | 196 | [[package]] 197 | name = "crunchy" 198 | version = "0.2.2" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" 201 | 202 | [[package]] 203 | name = "diff" 204 | version = "0.1.12" 205 | source = "registry+https://github.com/rust-lang/crates.io-index" 206 | checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499" 207 | 208 | [[package]] 209 | name = "digest" 210 | version = "0.9.0" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" 213 | dependencies = [ 214 | "generic-array", 215 | ] 216 | 217 | [[package]] 218 | name = "dirs-next" 219 | version = "2.0.0" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" 222 | dependencies = [ 223 | "cfg-if", 224 | "dirs-sys-next", 225 | ] 226 | 227 | [[package]] 228 | name = "dirs-sys-next" 229 | version = "0.1.2" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" 232 | dependencies = [ 233 | "libc", 234 | "redox_users", 235 | "winapi", 236 | ] 237 | 238 | [[package]] 239 | name = "either" 240 | version = "1.6.1" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" 243 | 244 | [[package]] 245 | name = "ena" 246 | version = "0.14.0" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "d7402b94a93c24e742487327a7cd839dc9d36fec9de9fb25b09f2dae459f36c3" 249 | dependencies = [ 250 | "log", 251 | ] 252 | 253 | [[package]] 254 | name = "fixedbitset" 255 | version = "0.2.0" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "37ab347416e802de484e4d03c7316c48f1ecb56574dfd4a46a80f173ce1de04d" 258 | 259 | [[package]] 260 | name = "fnv" 261 | version = "1.0.7" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 264 | 265 | [[package]] 266 | name = "generic-array" 267 | version = "0.14.5" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" 270 | dependencies = [ 271 | "typenum", 272 | "version_check", 273 | ] 274 | 275 | [[package]] 276 | name = "getrandom" 277 | version = "0.2.4" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c" 280 | dependencies = [ 281 | "cfg-if", 282 | "libc", 283 | "wasi", 284 | ] 285 | 286 | [[package]] 287 | name = "hashbrown" 288 | version = "0.11.2" 289 | source = "registry+https://github.com/rust-lang/crates.io-index" 290 | checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" 291 | 292 | [[package]] 293 | name = "hermit-abi" 294 | version = "0.1.19" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 297 | dependencies = [ 298 | "libc", 299 | ] 300 | 301 | [[package]] 302 | name = "hex" 303 | version = "0.4.3" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 306 | 307 | [[package]] 308 | name = "ic-btc-types" 309 | version = "0.1.0" 310 | dependencies = [ 311 | "candid", 312 | "serde", 313 | ] 314 | 315 | [[package]] 316 | name = "ic-types" 317 | version = "0.3.0" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "0e78ec6f58886cdc252d6f912dc794211bd6bbc39ddc9dcda434b2dc16c335b3" 320 | dependencies = [ 321 | "base32", 322 | "crc32fast", 323 | "hex", 324 | "serde", 325 | "serde_bytes", 326 | "sha2", 327 | "thiserror", 328 | ] 329 | 330 | [[package]] 331 | name = "indexmap" 332 | version = "1.8.0" 333 | source = "registry+https://github.com/rust-lang/crates.io-index" 334 | checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223" 335 | dependencies = [ 336 | "autocfg", 337 | "hashbrown", 338 | ] 339 | 340 | [[package]] 341 | name = "instant" 342 | version = "0.1.12" 343 | source = "registry+https://github.com/rust-lang/crates.io-index" 344 | checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" 345 | dependencies = [ 346 | "cfg-if", 347 | ] 348 | 349 | [[package]] 350 | name = "itertools" 351 | version = "0.10.3" 352 | source = "registry+https://github.com/rust-lang/crates.io-index" 353 | checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" 354 | dependencies = [ 355 | "either", 356 | ] 357 | 358 | [[package]] 359 | name = "lalrpop" 360 | version = "0.19.6" 361 | source = "registry+https://github.com/rust-lang/crates.io-index" 362 | checksum = "b15174f1c529af5bf1283c3bc0058266b483a67156f79589fab2a25e23cf8988" 363 | dependencies = [ 364 | "ascii-canvas", 365 | "atty", 366 | "bit-set", 367 | "diff", 368 | "ena", 369 | "itertools", 370 | "lalrpop-util", 371 | "petgraph", 372 | "pico-args", 373 | "regex", 374 | "regex-syntax", 375 | "string_cache", 376 | "term", 377 | "tiny-keccak", 378 | "unicode-xid", 379 | ] 380 | 381 | [[package]] 382 | name = "lalrpop-util" 383 | version = "0.19.6" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "d3e58cce361efcc90ba8a0a5f982c741ff86b603495bb15a998412e957dcd278" 386 | dependencies = [ 387 | "regex", 388 | ] 389 | 390 | [[package]] 391 | name = "lazy_static" 392 | version = "1.4.0" 393 | source = "registry+https://github.com/rust-lang/crates.io-index" 394 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 395 | 396 | [[package]] 397 | name = "leb128" 398 | version = "0.2.5" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" 401 | 402 | [[package]] 403 | name = "libc" 404 | version = "0.2.112" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125" 407 | 408 | [[package]] 409 | name = "lock_api" 410 | version = "0.4.5" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109" 413 | dependencies = [ 414 | "scopeguard", 415 | ] 416 | 417 | [[package]] 418 | name = "log" 419 | version = "0.4.14" 420 | source = "registry+https://github.com/rust-lang/crates.io-index" 421 | checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" 422 | dependencies = [ 423 | "cfg-if", 424 | ] 425 | 426 | [[package]] 427 | name = "logos" 428 | version = "0.12.0" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | checksum = "427e2abca5be13136da9afdbf874e6b34ad9001dd70f2b103b083a85daa7b345" 431 | dependencies = [ 432 | "logos-derive", 433 | ] 434 | 435 | [[package]] 436 | name = "logos-derive" 437 | version = "0.12.0" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | checksum = "56a7d287fd2ac3f75b11f19a1c8a874a7d55744bd91f7a1b3e7cf87d4343c36d" 440 | dependencies = [ 441 | "beef", 442 | "fnv", 443 | "proc-macro2", 444 | "quote", 445 | "regex-syntax", 446 | "syn", 447 | "utf8-ranges", 448 | ] 449 | 450 | [[package]] 451 | name = "memchr" 452 | version = "2.4.1" 453 | source = "registry+https://github.com/rust-lang/crates.io-index" 454 | checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" 455 | 456 | [[package]] 457 | name = "new_debug_unreachable" 458 | version = "1.0.4" 459 | source = "registry+https://github.com/rust-lang/crates.io-index" 460 | checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" 461 | 462 | [[package]] 463 | name = "num-bigint" 464 | version = "0.4.3" 465 | source = "registry+https://github.com/rust-lang/crates.io-index" 466 | checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" 467 | dependencies = [ 468 | "autocfg", 469 | "num-integer", 470 | "num-traits", 471 | ] 472 | 473 | [[package]] 474 | name = "num-integer" 475 | version = "0.1.44" 476 | source = "registry+https://github.com/rust-lang/crates.io-index" 477 | checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" 478 | dependencies = [ 479 | "autocfg", 480 | "num-traits", 481 | ] 482 | 483 | [[package]] 484 | name = "num-traits" 485 | version = "0.2.14" 486 | source = "registry+https://github.com/rust-lang/crates.io-index" 487 | checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" 488 | dependencies = [ 489 | "autocfg", 490 | ] 491 | 492 | [[package]] 493 | name = "num_enum" 494 | version = "0.5.6" 495 | source = "registry+https://github.com/rust-lang/crates.io-index" 496 | checksum = "720d3ea1055e4e4574c0c0b0f8c3fd4f24c4cdaf465948206dea090b57b526ad" 497 | dependencies = [ 498 | "num_enum_derive", 499 | ] 500 | 501 | [[package]] 502 | name = "num_enum_derive" 503 | version = "0.5.6" 504 | source = "registry+https://github.com/rust-lang/crates.io-index" 505 | checksum = "0d992b768490d7fe0d8586d9b5745f6c49f557da6d81dc982b1d167ad4edbb21" 506 | dependencies = [ 507 | "proc-macro-crate", 508 | "proc-macro2", 509 | "quote", 510 | "syn", 511 | ] 512 | 513 | [[package]] 514 | name = "opaque-debug" 515 | version = "0.3.0" 516 | source = "registry+https://github.com/rust-lang/crates.io-index" 517 | checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" 518 | 519 | [[package]] 520 | name = "parking_lot" 521 | version = "0.11.2" 522 | source = "registry+https://github.com/rust-lang/crates.io-index" 523 | checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" 524 | dependencies = [ 525 | "instant", 526 | "lock_api", 527 | "parking_lot_core", 528 | ] 529 | 530 | [[package]] 531 | name = "parking_lot_core" 532 | version = "0.8.5" 533 | source = "registry+https://github.com/rust-lang/crates.io-index" 534 | checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" 535 | dependencies = [ 536 | "cfg-if", 537 | "instant", 538 | "libc", 539 | "redox_syscall", 540 | "smallvec", 541 | "winapi", 542 | ] 543 | 544 | [[package]] 545 | name = "paste" 546 | version = "1.0.6" 547 | source = "registry+https://github.com/rust-lang/crates.io-index" 548 | checksum = "0744126afe1a6dd7f394cb50a716dbe086cb06e255e53d8d0185d82828358fb5" 549 | 550 | [[package]] 551 | name = "petgraph" 552 | version = "0.5.1" 553 | source = "registry+https://github.com/rust-lang/crates.io-index" 554 | checksum = "467d164a6de56270bd7c4d070df81d07beace25012d5103ced4e9ff08d6afdb7" 555 | dependencies = [ 556 | "fixedbitset", 557 | "indexmap", 558 | ] 559 | 560 | [[package]] 561 | name = "phf_shared" 562 | version = "0.8.0" 563 | source = "registry+https://github.com/rust-lang/crates.io-index" 564 | checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" 565 | dependencies = [ 566 | "siphasher", 567 | ] 568 | 569 | [[package]] 570 | name = "pico-args" 571 | version = "0.4.2" 572 | source = "registry+https://github.com/rust-lang/crates.io-index" 573 | checksum = "db8bcd96cb740d03149cbad5518db9fd87126a10ab519c011893b1754134c468" 574 | 575 | [[package]] 576 | name = "precomputed-hash" 577 | version = "0.1.1" 578 | source = "registry+https://github.com/rust-lang/crates.io-index" 579 | checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" 580 | 581 | [[package]] 582 | name = "pretty" 583 | version = "0.10.0" 584 | source = "registry+https://github.com/rust-lang/crates.io-index" 585 | checksum = "ad9940b913ee56ddd94aec2d3cd179dd47068236f42a1a6415ccf9d880ce2a61" 586 | dependencies = [ 587 | "arrayvec", 588 | "typed-arena", 589 | ] 590 | 591 | [[package]] 592 | name = "proc-macro-crate" 593 | version = "1.1.0" 594 | source = "registry+https://github.com/rust-lang/crates.io-index" 595 | checksum = "1ebace6889caf889b4d3f76becee12e90353f2b8c7d875534a71e5742f8f6f83" 596 | dependencies = [ 597 | "thiserror", 598 | "toml", 599 | ] 600 | 601 | [[package]] 602 | name = "proc-macro2" 603 | version = "1.0.36" 604 | source = "registry+https://github.com/rust-lang/crates.io-index" 605 | checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" 606 | dependencies = [ 607 | "unicode-xid", 608 | ] 609 | 610 | [[package]] 611 | name = "quote" 612 | version = "1.0.14" 613 | source = "registry+https://github.com/rust-lang/crates.io-index" 614 | checksum = "47aa80447ce4daf1717500037052af176af5d38cc3e571d9ec1c7353fc10c87d" 615 | dependencies = [ 616 | "proc-macro2", 617 | ] 618 | 619 | [[package]] 620 | name = "redox_syscall" 621 | version = "0.2.10" 622 | source = "registry+https://github.com/rust-lang/crates.io-index" 623 | checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" 624 | dependencies = [ 625 | "bitflags", 626 | ] 627 | 628 | [[package]] 629 | name = "redox_users" 630 | version = "0.4.0" 631 | source = "registry+https://github.com/rust-lang/crates.io-index" 632 | checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" 633 | dependencies = [ 634 | "getrandom", 635 | "redox_syscall", 636 | ] 637 | 638 | [[package]] 639 | name = "regex" 640 | version = "1.5.4" 641 | source = "registry+https://github.com/rust-lang/crates.io-index" 642 | checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" 643 | dependencies = [ 644 | "aho-corasick", 645 | "memchr", 646 | "regex-syntax", 647 | ] 648 | 649 | [[package]] 650 | name = "regex-syntax" 651 | version = "0.6.25" 652 | source = "registry+https://github.com/rust-lang/crates.io-index" 653 | checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" 654 | 655 | [[package]] 656 | name = "rustversion" 657 | version = "1.0.6" 658 | source = "registry+https://github.com/rust-lang/crates.io-index" 659 | checksum = "f2cc38e8fa666e2de3c4aba7edeb5ffc5246c1c2ed0e3d17e560aeeba736b23f" 660 | 661 | [[package]] 662 | name = "scopeguard" 663 | version = "1.1.0" 664 | source = "registry+https://github.com/rust-lang/crates.io-index" 665 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 666 | 667 | [[package]] 668 | name = "serde" 669 | version = "1.0.133" 670 | source = "registry+https://github.com/rust-lang/crates.io-index" 671 | checksum = "97565067517b60e2d1ea8b268e59ce036de907ac523ad83a0475da04e818989a" 672 | dependencies = [ 673 | "serde_derive", 674 | ] 675 | 676 | [[package]] 677 | name = "serde_bytes" 678 | version = "0.11.5" 679 | source = "registry+https://github.com/rust-lang/crates.io-index" 680 | checksum = "16ae07dd2f88a366f15bd0632ba725227018c69a1c8550a927324f8eb8368bb9" 681 | dependencies = [ 682 | "serde", 683 | ] 684 | 685 | [[package]] 686 | name = "serde_derive" 687 | version = "1.0.133" 688 | source = "registry+https://github.com/rust-lang/crates.io-index" 689 | checksum = "ed201699328568d8d08208fdd080e3ff594e6c422e438b6705905da01005d537" 690 | dependencies = [ 691 | "proc-macro2", 692 | "quote", 693 | "syn", 694 | ] 695 | 696 | [[package]] 697 | name = "sha2" 698 | version = "0.9.9" 699 | source = "registry+https://github.com/rust-lang/crates.io-index" 700 | checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" 701 | dependencies = [ 702 | "block-buffer", 703 | "cfg-if", 704 | "cpufeatures", 705 | "digest", 706 | "opaque-debug", 707 | ] 708 | 709 | [[package]] 710 | name = "siphasher" 711 | version = "0.3.7" 712 | source = "registry+https://github.com/rust-lang/crates.io-index" 713 | checksum = "533494a8f9b724d33625ab53c6c4800f7cc445895924a8ef649222dcb76e938b" 714 | 715 | [[package]] 716 | name = "smallvec" 717 | version = "1.8.0" 718 | source = "registry+https://github.com/rust-lang/crates.io-index" 719 | checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" 720 | 721 | [[package]] 722 | name = "string_cache" 723 | version = "0.8.2" 724 | source = "registry+https://github.com/rust-lang/crates.io-index" 725 | checksum = "923f0f39b6267d37d23ce71ae7235602134b250ace715dd2c90421998ddac0c6" 726 | dependencies = [ 727 | "lazy_static", 728 | "new_debug_unreachable", 729 | "parking_lot", 730 | "phf_shared", 731 | "precomputed-hash", 732 | ] 733 | 734 | [[package]] 735 | name = "syn" 736 | version = "1.0.85" 737 | source = "registry+https://github.com/rust-lang/crates.io-index" 738 | checksum = "a684ac3dcd8913827e18cd09a68384ee66c1de24157e3c556c9ab16d85695fb7" 739 | dependencies = [ 740 | "proc-macro2", 741 | "quote", 742 | "unicode-xid", 743 | ] 744 | 745 | [[package]] 746 | name = "term" 747 | version = "0.7.0" 748 | source = "registry+https://github.com/rust-lang/crates.io-index" 749 | checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" 750 | dependencies = [ 751 | "dirs-next", 752 | "rustversion", 753 | "winapi", 754 | ] 755 | 756 | [[package]] 757 | name = "termcolor" 758 | version = "1.1.2" 759 | source = "registry+https://github.com/rust-lang/crates.io-index" 760 | checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" 761 | dependencies = [ 762 | "winapi-util", 763 | ] 764 | 765 | [[package]] 766 | name = "thiserror" 767 | version = "1.0.30" 768 | source = "registry+https://github.com/rust-lang/crates.io-index" 769 | checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" 770 | dependencies = [ 771 | "thiserror-impl", 772 | ] 773 | 774 | [[package]] 775 | name = "thiserror-impl" 776 | version = "1.0.30" 777 | source = "registry+https://github.com/rust-lang/crates.io-index" 778 | checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" 779 | dependencies = [ 780 | "proc-macro2", 781 | "quote", 782 | "syn", 783 | ] 784 | 785 | [[package]] 786 | name = "tiny-keccak" 787 | version = "2.0.2" 788 | source = "registry+https://github.com/rust-lang/crates.io-index" 789 | checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" 790 | dependencies = [ 791 | "crunchy", 792 | ] 793 | 794 | [[package]] 795 | name = "toml" 796 | version = "0.5.8" 797 | source = "registry+https://github.com/rust-lang/crates.io-index" 798 | checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" 799 | dependencies = [ 800 | "serde", 801 | ] 802 | 803 | [[package]] 804 | name = "typed-arena" 805 | version = "2.0.1" 806 | source = "registry+https://github.com/rust-lang/crates.io-index" 807 | checksum = "0685c84d5d54d1c26f7d3eb96cd41550adb97baed141a761cf335d3d33bcd0ae" 808 | 809 | [[package]] 810 | name = "typenum" 811 | version = "1.15.0" 812 | source = "registry+https://github.com/rust-lang/crates.io-index" 813 | checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" 814 | 815 | [[package]] 816 | name = "unicode-width" 817 | version = "0.1.9" 818 | source = "registry+https://github.com/rust-lang/crates.io-index" 819 | checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" 820 | 821 | [[package]] 822 | name = "unicode-xid" 823 | version = "0.2.2" 824 | source = "registry+https://github.com/rust-lang/crates.io-index" 825 | checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" 826 | 827 | [[package]] 828 | name = "utf8-ranges" 829 | version = "1.0.4" 830 | source = "registry+https://github.com/rust-lang/crates.io-index" 831 | checksum = "b4ae116fef2b7fea257ed6440d3cfcff7f190865f170cdad00bb6465bf18ecba" 832 | 833 | [[package]] 834 | name = "version_check" 835 | version = "0.9.4" 836 | source = "registry+https://github.com/rust-lang/crates.io-index" 837 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 838 | 839 | [[package]] 840 | name = "wasi" 841 | version = "0.10.2+wasi-snapshot-preview1" 842 | source = "registry+https://github.com/rust-lang/crates.io-index" 843 | checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" 844 | 845 | [[package]] 846 | name = "winapi" 847 | version = "0.3.9" 848 | source = "registry+https://github.com/rust-lang/crates.io-index" 849 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 850 | dependencies = [ 851 | "winapi-i686-pc-windows-gnu", 852 | "winapi-x86_64-pc-windows-gnu", 853 | ] 854 | 855 | [[package]] 856 | name = "winapi-i686-pc-windows-gnu" 857 | version = "0.4.0" 858 | source = "registry+https://github.com/rust-lang/crates.io-index" 859 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 860 | 861 | [[package]] 862 | name = "winapi-util" 863 | version = "0.1.5" 864 | source = "registry+https://github.com/rust-lang/crates.io-index" 865 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 866 | dependencies = [ 867 | "winapi", 868 | ] 869 | 870 | [[package]] 871 | name = "winapi-x86_64-pc-windows-gnu" 872 | version = "0.4.0" 873 | source = "registry+https://github.com/rust-lang/crates.io-index" 874 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 875 | -------------------------------------------------------------------------------- /types/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ic-btc-types" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | ic-cdk = "0.3.1" 8 | serde = "1.0.132" 9 | -------------------------------------------------------------------------------- /types/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Types used to support the candid API. 2 | 3 | use ic_cdk::export::candid::{CandidType, Deserialize}; 4 | 5 | pub type Satoshi = u64; 6 | 7 | /// A reference to a transaction output. 8 | #[derive(CandidType, Clone, Debug, Deserialize, PartialEq, Eq, Hash)] 9 | pub struct OutPoint { 10 | pub txid: Vec, 11 | pub vout: u32, 12 | } 13 | 14 | /// An unspent transaction output. 15 | #[derive(CandidType, Debug, Deserialize, PartialEq)] 16 | pub struct Utxo { 17 | pub outpoint: OutPoint, 18 | pub value: Satoshi, 19 | pub height: u32, 20 | pub confirmations: u32, 21 | } 22 | 23 | /// A request for getting the UTXOs for a given address. 24 | #[derive(CandidType, Debug, Deserialize, PartialEq)] 25 | pub struct GetUtxosRequest { 26 | pub address: String, 27 | pub min_confirmations: Option, 28 | } 29 | 30 | /// Errors when processing a `get_utxos` request. 31 | #[derive(CandidType, Debug, Deserialize, PartialEq)] 32 | pub struct GetUtxosResponse { 33 | pub utxos: Vec, 34 | pub total_count: u32, 35 | } 36 | 37 | /// Errors when processing a `get_utxos` request. 38 | #[derive(CandidType, Debug, Deserialize, PartialEq)] 39 | pub enum GetUtxosError { 40 | MalformedAddress, 41 | } 42 | 43 | #[derive(CandidType, Debug, Deserialize, PartialEq)] 44 | pub struct GetBalanceRequest { 45 | pub address: String, 46 | pub min_confirmations: Option, 47 | } 48 | 49 | #[derive(CandidType, Debug, Deserialize, PartialEq)] 50 | pub enum GetBalanceError { 51 | MalformedAddress, 52 | } 53 | 54 | impl From for GetBalanceError { 55 | fn from(err: GetUtxosError) -> Self { 56 | match err { 57 | GetUtxosError::MalformedAddress => Self::MalformedAddress, 58 | } 59 | } 60 | } 61 | 62 | #[derive(CandidType, Debug, Deserialize, PartialEq)] 63 | pub struct SendTransactionRequest { 64 | pub transaction: Vec, 65 | } 66 | 67 | #[derive(CandidType, Debug, Deserialize, PartialEq)] 68 | pub enum SendTransactionError { 69 | MalformedTransaction, 70 | } 71 | --------------------------------------------------------------------------------