├── .github ├── bitcoin.conf └── workflows │ ├── build.yaml │ ├── coverage.yaml │ └── lint.yaml ├── .gitignore ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── LICENSE.md ├── README.md ├── codecov.yaml ├── src ├── contracts.rs ├── direct_send.rs ├── directory_servers.rs ├── error.rs ├── fidelity_bonds.rs ├── funding_tx.rs ├── lib.rs ├── main.rs ├── maker_protocol.rs ├── messages.rs ├── offerbook_sync.rs ├── taker_protocol.rs ├── wallet_sync.rs ├── watchtower_client.rs └── watchtower_protocol.rs └── tests ├── init.sh └── test_standard_coinswap.rs /.github/bitcoin.conf: -------------------------------------------------------------------------------- 1 | # bitcoind configuration 2 | 3 | # remove the following line to enable Bitcoin mainnet 4 | regtest=1 5 | fallbackfee=0.0001 6 | # Bitcoind options 7 | server=1 8 | 9 | 10 | # Connection settings 11 | rpcuser=regtestrpcuser 12 | rpcpassword=regtestrpcpass 13 | # blockfilterindex=1 14 | # peerblockfilters=1 15 | 16 | [regtest] 17 | rpcbind=0.0.0.0 18 | rpcallowip=0.0.0.0/0 19 | 20 | zmqpubrawblock=tcp://127.0.0.1:28332 21 | zmqpubrawtx=tcp://127.0.0.1:28333 -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: continuous_builds 4 | 5 | jobs: 6 | 7 | build: 8 | name: Build 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | rust: 13 | - stable 14 | - nightly 15 | features: 16 | - default 17 | steps: 18 | - name: checkout 19 | uses: actions/checkout@v2 20 | - name: Generate cache key 21 | run: echo "${{ matrix.rust }} ${{ matrix.features }}" | tee .cache_key 22 | - name: cache 23 | uses: actions/cache@v2 24 | with: 25 | path: | 26 | ~/.cargo/registry 27 | ~/.cargo/git 28 | target 29 | key: ${{ runner.os }}-cargo-${{ hashFiles('.cache_key') }}-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }} 30 | - name: Set default toolchain 31 | run: rustup default ${{ matrix.rust }} 32 | - name: Set profile 33 | run: rustup set profile minimal 34 | - name: Add clippy 35 | run: rustup component add clippy 36 | - name: Update toolchain 37 | run: rustup update 38 | - name: Build 39 | run: cargo build --features ${{ matrix.features }} --no-default-features 40 | # Uncomment clippy check after failures are removed 41 | # - name: Clippy 42 | # run: cargo clippy --all-targets --features ${{ matrix.features }} --no-default-features -- -D warnings -------------------------------------------------------------------------------- /.github/workflows/coverage.yaml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: test_codecov 4 | 5 | jobs: 6 | test_with_codecov: 7 | name: Run tests with coverage reporting 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v2 12 | 13 | - name: Set default toolchain 14 | run: rustup default nightly 15 | - name: Set profile 16 | run: rustup set profile minimal 17 | 18 | - name: Install bitcoind 19 | run: | 20 | wget https://bitcoincore.org/bin/bitcoin-core-0.21.1/bitcoin-0.21.1-x86_64-linux-gnu.tar.gz 21 | tar -xvf bitcoin-0.21.1-x86_64-linux-gnu.tar.gz 22 | sudo cp bitcoin-0.21.1/bin/* /usr/local/bin 23 | which bitcoind 24 | 25 | # bitcoind setups are required for integration test 26 | # TODO: Separate unit and integration tests to different process. 27 | - name: Run and bitcoind 28 | run: | 29 | mkdir -p .bitcoin 30 | cp .github/bitcoin.conf .bitcoin/ 31 | bitcoind -daemon -datadir=.bitcoin 32 | 33 | - name: Sleep for 5 secs # Wait until bitcoind starts 34 | uses: juliangruber/sleep-action@v1 35 | with: 36 | time: 5s 37 | 38 | - name: Setup core wallet 39 | run: | 40 | bitcoin-cli -datadir=.bitcoin createwallet teleport 41 | addrs=$(bitcoin-cli -datadir=.bitcoin getnewaddress) 42 | bitcoin-cli -datadir=.bitcoin generatetoaddress 101 $addrs 43 | 44 | # Pin grcov to v0.8.2 because of build failure at 0.8.3 45 | - name: Install grcov 46 | run: cargo install grcov --force --version 0.8.2 47 | 48 | # Tests are run with code coverage support 49 | - name: Run cargo test 50 | env: 51 | CARGO_INCREMENTAL: '0' 52 | RUSTFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off' 53 | RUSTDOCFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off' 54 | run: cargo test 55 | - id: coverage 56 | name: Generate coverage 57 | uses: actions-rs/grcov@v0.1.5 58 | 59 | # Upload coverage report 60 | - name: Upload coverage to Codecov 61 | uses: codecov/codecov-action@v1 62 | with: 63 | file: ${{ steps.coverage.outputs.report }} 64 | directory: ./coverage/reports/ -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | 2 | on: [push, pull_request] 3 | 4 | name: lint 5 | 6 | jobs: 7 | 8 | fmt: 9 | name: rust fmt 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | - name: Set default toolchain 15 | run: rustup default stable 16 | - name: Set profile 17 | run: rustup set profile minimal 18 | - name: Add rustfmt 19 | run: rustup component add rustfmt 20 | - name: Update toolchain 21 | run: rustup update 22 | - name: Check fmt 23 | run: cargo fmt --all -- --check -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | *.swp 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "teleport" 3 | version = "0.1.0" 4 | authors = ["chris-belcher "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | bitcoincore-rpc = "0.13" 11 | #bitcoincore-rpc = {git="https://github.com/rust-bitcoin/rust-bitcoincore-rpc"} 12 | bitcoin-wallet = "1.1.0" 13 | bitcoin = "0.26" 14 | serde = { version = "1.0", features = ["derive"] } 15 | serde_json = "1.0" 16 | tokio = { version = "1.16.1", features = ["full"] } 17 | log = "^0.4" 18 | env_logger = "0.7" 19 | futures = "0.3" 20 | rand = "0.7.3" 21 | itertools = "0.9.0" 22 | structopt = "0.3.21" 23 | dirs = "3.0.1" 24 | tokio-socks = "0.5" 25 | reqwest = { version = "0.11", features = ["socks"] } 26 | chrono = "0.4" 27 | 28 | #Empty default feature set, (helpful to generalise in github actions) 29 | [features] 30 | default = [] 31 | 32 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 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 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | This software is licensed under [Apache 2.0](LICENSE-APACHE) or 2 | [MIT](LICENSE-MIT), at your option. 3 | 4 | Some files retain their own copyright notice, however, for full authorship 5 | information, see version control history. 6 | 7 | Except as otherwise noted in individual files, all files in this repository are 8 | licensed under the Apache License, Version 2.0 or the MIT license , at your option. 11 | 12 | You may not use, copy, modify, merge, publish, distribute, sublicense, and/or 13 | sell copies of this software or any files in this repository except in 14 | accordance with one or both of these licenses. 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Teleport Transactions 2 | 3 | Teleport Transactions is software aiming to improve the [privacy](https://en.bitcoin.it/wiki/Privacy) of [Bitcoin](https://en.bitcoin.it/wiki/Main_Page). 4 | 5 | Suppose Alice has bitcoin and wants to send them with maximal privacy, so she creates a special kind of transaction. For anyone looking at the blockchain her transaction appears completely normal with her coins seemingly going from Bitcoin address A to address B. But in reality her coins end up in address Z which is entirely unconnected to either A or B. 6 | 7 | Now imagine another user, Carol, who isn't too bothered by privacy and sends her bitcoin using a regular wallet. But because Carol's transaction looks exactly the same as Alice's, anybody analyzing the blockchain must now deal with the possibility that Carol's transaction actually sent her coins to a totally unconnected address. So Carol's privacy is improved even though she didn't change her behaviour, and perhaps had never even heard of this software. 8 | 9 | In a world where advertisers, social media and other institutions want to collect all of Alice's and Carol's data, such privacy improvement is incredibly valuable. And the doubt added to every transaction would greatly boost the [fungibility of Bitcoin](https://en.bitcoin.it/wiki/Fungibility) and so make it a better form of money. 10 | 11 | Project design document: [Design for a CoinSwap Implementation for Massively Improving Bitcoin Privacy and Fungibility](https://gist.github.com/chris-belcher/9144bd57a91c194e332fb5ca371d0964) 12 | 13 | ## Contents 14 | 15 | - [State of the project](#state-of-the-project) 16 | - [How to create a CoinSwap on regtest with yourself](#how-to-create-a-coinswap-on-regtest-with-yourself) 17 | - [How to create a CoinSwap on networks other than regtest](#how-to-create-a-coinswap-on-networks-other-than-regtest) 18 | - [How to recover from a failed coinswap](#how-to-recover-from-a-failed-coinswap) 19 | - [Developer resources](#developer-resources) 20 | - [Protocol between takers and makers](#protocol-between-takers-and-makers) 21 | - [Notes on architecture](#notes-on-architecture) 22 | - [Chris Belcher's personal roadmap for the project](#chris-belchers-personal-roadmap-for-the-project) 23 | - [Community](#community) 24 | 25 | ## State of the project 26 | 27 | The project is nearly usable, though it doesnt have all the necessary features yet. The code written so far is published for developers and power users to play around with. It doesn't have config files yet so you have to edit the source files to configure stuff. It is possible to run it on mainnet, but only the brave will attempt that, and only with small amounts. 28 | 29 | ## How to create a CoinSwap on regtest with yourself 30 | 31 | * Install [rust](https://www.rust-lang.org/) on your machine. 32 | 33 | * Start up Bitcoin Core in regtest mode. Make sure the RPC server is enabled with `server=1` and that rpc username and password are set with `rpcuser=yourrpcusername` and `rpcpassword=yourrpcpassword` in the configuration file. 34 | 35 | * Download the [latest release](https://github.com/bitcoin-teleport/teleport-transactions/releases). Open the file `src/lib.rs` and edit the RPC username and password right at the top of the file. Make sure your Bitcoin Core has a wallet called `teleport`, or edit the name in the same place. 36 | 37 | * Create three teleport wallets by running `cargo run -- --wallet-file-name= generate-wallet` thrice. Instead of ``, use something like `maker1.teleport`, `maker2.teleport` and `taker.teleport`. 38 | 39 | * Use `cargo run -- --wallet-file-name=maker1.teleport get-receive-invoice` to obtain 3 addresses of the maker1 wallet, and send regtest bitcoin to each of them (amount 5000000 satoshi or 0.05 BTC in this example). Also do this for the `maker2.teleport` and `taker.teleport` wallets. Get the transactions confirmed. 40 | 41 | * Check the wallet balances with `cargo run -- --wallet-file-name=maker1.teleport wallet-balance`. Example: 42 | 43 | ``` 44 | $ cargo run -- --wallet-file-name=maker1.teleport wallet-balance 45 | coin address type conf value 46 | 8f6ee5..74e813:0 bcrt1q0vn5....nrjdqljtaq seed 1 0.05000000 BTC 47 | d548a8..cadd5e:0 bcrt1qaylc....vnw4ay98jq seed 1 0.05000000 BTC 48 | 604ca6..4ab5f0:1 bcrt1qt3jy....df6pmewmzs seed 1 0.05000000 BTC 49 | coin count = 3 50 | total balance = 0.15000000 BTC 51 | ``` 52 | 53 | ``` 54 | $ cargo run -- --wallet-file-name=maker2.teleport wallet-balance 55 | coin address type conf value 56 | d33f06..30dd07:0 bcrt1qh6kq....e0tlfrzgxa seed 1 0.05000000 BTC 57 | 8aaa89..ef5613:0 bcrt1q9vyj....plh8x37n7g seed 1 0.05000000 BTC 58 | 383ffe..127065:1 bcrt1qlwzv....pdqtrg0xuu seed 1 0.05000000 BTC 59 | coin count = 3 60 | total balance = 0.15000000 BTC 61 | ``` 62 | 63 | ``` 64 | $ cargo run -- --wallet-file-name=taker.teleport wallet-balance 65 | coin address type conf value 66 | 5f4331..d53f14:0 bcrt1qmflt....q2ucgf2teu seed 1 0.05000000 BTC 67 | 6252ee..d827b0:0 bcrt1qu9mk....pwpedjyl9u seed 1 0.05000000 BTC 68 | ac88da..e3ead6:0 bcrt1q3xdx....e7gxtcgrfg seed 1 0.05000000 BTC 69 | coin count = 3 70 | total balance = 0.15000000 BTC 71 | ``` 72 | 73 | * On another terminal run a watchtower with `cargo run -- run-watchtower`. You should see the message `Starting teleport watchtower`. In the teleport project, contracts are enforced with one or more watchtowers which are required for the coinswap protocol to be secure against the maker's coins being stolen. 74 | 75 | * On one terminal run a maker server with `cargo run -- --wallet-file-name=maker1.teleport run-yield-generator 6102`. You should see the message `Listening on port 6102`. 76 | 77 | * On another terminal run another maker server with `cargo run -- --wallet-file-name=maker2.teleport run-yield-generator 16102`. You should see the message `Listening on port 16102`. 78 | 79 | * On another terminal start a coinswap with `cargo run -- --wallet-file-name=taker.teleport do-coinswap 500000`. When you see the terminal messages `waiting for funding transaction to confirm` and `waiting for maker's funding transaction to confirm` then tell regtest to generate another block (or just wait if you're using testnet). 80 | 81 | * Once you see the message `successfully completed coinswap` on all terminals then check the wallet balance again to see the result of the coinswap. Example: 82 | 83 | ``` 84 | $ cargo run -- --wallet-file-name=maker1.teleport wallet-balance 85 | coin address type conf value 86 | 9bfeec..0cc468:0 bcrt1qx49k....9cqqrp3kt0 swapcoin 2 0.00134344 BTC 87 | 973ab4..48f5b7:1 bcrt1qdu4j....ru3qmw4gcf swapcoin 2 0.00224568 BTC 88 | 2edf14..74c3b9:0 bcrt1qfw6z....msrsdx9sl0 swapcoin 2 0.00131088 BTC 89 | bd6321..217707:0 bcrt1q35g8....rt6al6kz7s seed 1 0.04758551 BTC 90 | c6564e..40fb64:0 bcrt1qrnzc....czs840p4np seed 1 0.04947775 BTC 91 | 08e857..c8c67b:0 bcrt1qdxdg....k7882f0ya2 seed 1 0.04808502 BTC 92 | coin count = 6 93 | total balance = 0.15004828 BTC 94 | ``` 95 | 96 | ``` 97 | $ cargo run -- --wallet-file-name=maker2.teleport wallet-balance 98 | coin address type conf value 99 | 9d8895..e32645:1 bcrt1qm73u....3h6swyege3 swapcoin 3 0.00046942 BTC 100 | 7cab11..07ff62:1 bcrt1quumg....gtjs29jt8t swapcoin 3 0.00009015 BTC 101 | 289a13..ab4672:0 bcrt1qsavn....t5dsac43tl swapcoin 3 0.00444043 BTC 102 | 9bfeec..0cc468:1 bcrt1q24f8....443ts4rzz0 seed 2 0.04863932 BTC 103 | 973ab4..48f5b7:0 bcrt1q5klz....jhhtlyjpkg seed 2 0.04773708 BTC 104 | 2edf14..74c3b9:1 bcrt1qh2aw....7xx8wft658 seed 2 0.04867188 BTC 105 | coin count = 6 106 | total balance = 0.15004828 BTC 107 | ``` 108 | 109 | ``` 110 | $ cargo run -- --wallet-file-name=taker.teleport wallet-balance 111 | coin address type conf value 112 | 9d8895..e32645:0 bcrt1qevgn....6nhl2yswa7 seed 3 0.04951334 BTC 113 | 7cab11..07ff62:0 bcrt1qxs5f....0j8khru45s seed 3 0.04989261 BTC 114 | 289a13..ab4672:1 bcrt1qkwka....g9ts2ch392 seed 3 0.04554233 BTC 115 | bd6321..217707:1 bcrt1qat5h....vytquawwke swapcoin 1 0.00239725 BTC 116 | c6564e..40fb64:1 bcrt1qshwp....3x8qjtwdf6 swapcoin 1 0.00050501 BTC 117 | 08e857..c8c67b:1 bcrt1q37lf....5tvqndktw6 swapcoin 1 0.00189774 BTC 118 | coin count = 6 119 | total balance = 0.14974828 BTC 120 | ``` 121 | 122 | ## How to create a CoinSwap on networks other than regtest 123 | 124 | * This is done in pretty much the same way as on the regtest network. On public networks you don't always have to coinswap with yourself by creating and funding multiple wallets, instead you could coinswap with other users out there. 125 | 126 | * Teleport detects which network it's on by asking the Bitcoin node it's connected to via json-rpc. So to switch between networks like regtest, signet, testnet or mainnet (for the brave), make sure the RPC host and port are correct in `src/lib.rs`. 127 | 128 | * You will need Tor running on the same machine, then open the file `src/directory_servers.rs` and make sure the const `TOR_ADDR` has the correct Tor port. 129 | 130 | * To see all the advertised offers out there, use the `download-offers` subroutine: `cargo run -- download-offers`: 131 | 132 | ``` 133 | $ cargo run -- download-offers 134 | n maker address max size min size abs fee amt rel fee time rel fee minlocktime 135 | 0 5wlgs4tmkc7vmzsqetpjyuz2qbhzydq6d7dotuvbven2cuqjbd2e2oyd.onion:6102 348541 10000 1000 10000000 100000 48 136 | 1 eitmocpmxolciziezpp6vzvhufg6djlq2y4oxpm436w5kpzx4tvfgead.onion:16102 314180 10000 1000 10000000 100000 48 137 | ``` 138 | 139 | * To run a yield generator (maker) on any network apart from regtest, you will need to create a tor hidden service for your maker. Search the web for "setup tor hidden service", a good article is [this one](https://www.linuxjournal.com/content/tor-hidden-services). When you have your hidden service hostname, copy it into the field near the top of the file `src/maker_protocol.rs`. Run with `cargo run -- --wallet-file-name=maker.teleport run-yield-generator` (note that you can omit the port number, the default port is 6102, specifying a different port number is only really needed for regtest where multiple makers are running on the same machine). 140 | 141 | * After a successful coinswap created with `do-coinswap`, the coins will still be in the wallet. You can send them out somewhere else using the command `direct-send` and providing the coin(s). For example `cargo run -- --wallet-file-name=taker.teleport direct-send max 9bfeec..0cc468:0`. Coins in the wallet can be found by running `wallet-balance` as above. 142 | 143 | ## How to recover from a failed coinswap 144 | 145 | * CoinSwaps can sometimes fail. Nobody will lose their funds, but they can have their time wasted and have spent miner fees without achieving any privacy gain (or even making their privacy worse, at least until scriptless script contracts are implemented). Everybody is incentivized so that this doesnt happen, and takers are coded to be very persistent in reestablishing a connection with makers before giving up, but sometimes failures will still happen. 146 | 147 | * The major way that CoinSwaps can fail is if a taker locks up funds in a 2-of-2 multisig with a maker, but then that maker becomes non-responsive and so the CoinSwap doesn't complete. The taker is left with their money in a multisig and has to use their pre-signed contract transaction to get their money back after a timeout. This section explains how to do that. 148 | 149 | * Failed or incomplete coinswaps will show up in wallet display in another section: `cargo run -- --wallet-file-name=taker.teleport wallet-balance`. Example: 150 | 151 | ``` 152 | = spendable wallet balance = 153 | coin address type conf value 154 | 9cd867..f80d57:1 bcrt1qgscq....xkxg68mq02 seed 212 0.11103591 BTC 155 | 13a0f4..947ab8:1 bcrt1qwfyl....wf0eyf5kuf seed 212 0.07666832 BTC 156 | 901514..10713b:0 bcrt1qghs3....qsg8al2ch4 seed 95 0.04371040 BTC 157 | 2fe664..db1a59:0 bcrt1ql83h....hht5vc97dl seed 94 0.50990000 BTC 158 | coin count = 4 159 | total balance = 0.74131463 BTC 160 | = incomplete coinswaps = 161 | coin type preimage locktime/blocks conf value 162 | 10149d..0d0314:1 timelock unknown 9 24 0.00029472 BTC 163 | b36e34..51fa3b:0 timelock unknown 9 24 0.00905248 BTC 164 | 2b2e2d..c6db9e:1 timelock unknown 9 24 0.00065280 BTC 165 | outgoing balance = 0.01000000 BTC 166 | hashvalue = a4c2fe816bf18afb8b1861138e57a51bd70e29d4 167 | ``` 168 | 169 | * In this example there is an incomplete coinswap involving three funding transactions, we must take the hashvalue `a4c2fe816bf18afb8b1861138e57a51bd70e29d4` and pass it to the main subroutine: `cargo run -- --wallet-file-name=taker.teleport recover-from-incomplete-coinswap a4c2fe816bf18afb8b1861138e57a51bd70e29d4`. 170 | 171 | * Displaying the wallet balance again (`cargo run -- --wallet-file-name=taker.teleport wallet-balance`) after the transactions are broadcast will show the coins in the timelocked contracts section: 172 | 173 | ``` 174 | = spendable wallet balance = 175 | coin address type conf value 176 | 9cd867..f80d57:1 bcrt1qgscq....xkxg68mq02 seed 212 0.11103591 BTC 177 | 13a0f4..947ab8:1 bcrt1qwfyl....wf0eyf5kuf seed 212 0.07666832 BTC 178 | 901514..10713b:0 bcrt1qghs3....qsg8al2ch4 seed 95 0.04371040 BTC 179 | 2fe664..db1a59:0 bcrt1ql83h....hht5vc97dl seed 94 0.50990000 BTC 180 | coin count = 4 181 | total balance = 0.74131463 BTC 182 | = live timelocked contracts = 183 | coin hashvalue timelock conf locked? value 184 | 452a99..95f364:0 a4c2fe81.. 9 0 locked 0.00904248 BTC 185 | dcfd27..56108a:0 a4c2fe81.. 9 0 locked 0.00064280 BTC 186 | 6a8328..f2f5ae:0 a4c2fe81.. 9 0 locked 0.00028472 BTC 187 | ``` 188 | 189 | * Right now these coins are protected by timelocked contracts which are not yet spendable, but after a number of blocks they will be added to the spendable wallet balance, where they can be spent either in a coinswap or with `direct-send`. 190 | 191 | 192 | ## Developer resources 193 | 194 | ### How CoinSwap works 195 | 196 | In a two-party coinswap, Alice and Bob can swap a coin in a non-custodial way, where neither party can steal from each other. At worst, they can waste time and miner fees. 197 | 198 | To start a coinswap, Alice will obtain one of Bob's public keys and use that to create a 2-of-2 multisignature address (known as Alice's coinswap address) made from Alice's and Bob's public keys. Alice will create a transaction (known as Alice's funding transaction) sending some of her coins (known as the coinswap amount) into this 2-of-2 multisig, but before she actually broadcasts this transaction she will ask Bob to use his corresponding private key to sign a transaction (known as Alice contract transaction) which sends the coins back to Alice after a timeout. Even though Alice's coins would be in a 2-of-2 multisig not controlled by her, she knows that if she broadcasts her contract transaction she will be able to get her coins back even if Bob disappears. 199 | 200 | Soon after all this has happened, Bob will do a similar thing but mirrored. Bob will obtain one of Alice's public keys and from it Bob's coinswap address. Bob creates a funding transaction paying to it the same coinswap amount, but before he broadcasts it he gets Alice to sign a contract transaction which sends Bob's coins back to him after a timeout. 201 | 202 | At this point both Alice and Bob are able to broadcast their funding transactions paying coins into multisig addresses, and if they want they can get those coins back by broadcasting their contract transactions and waiting for the timeout. The trick with coinswap is that the contract transaction script contains a second clause: it is also possible for the other party to get the coins by providing a hash preimage (e.g. HX = sha256(X)) without waiting for a timeout. The effect of this is that if the hash preimage is revealed to both parties then the coins in the multisig addresses have transferred possession off-chain to the other party who originally didn't own those coins. 203 | 204 | When the preimage is not known, Alice can use her contract transaction to get coins from Alice's multisig address after a timeout, and Bob can use his contract transaction to get coins from the Bob multisig address after a timeout. After the preimage is known, Alice can use Bob's contract transaction and the preimage to get coins from Bob's multisig address, and also Bob can use Alice's contract transaction and the preimage to get the coins from Alice's multisig address. 205 | 206 | Here is a diagram of Alice and Bob's coins and how they swap possession after a coinswap: 207 | ``` 208 | Alice after a timeout 209 | / 210 | / 211 | Alice's coins ------> Alice coinswap address 212 | \ 213 | \ 214 | Bob with knowledge of the hash preimage 215 | 216 | 217 | Bob after a timeout 218 | / 219 | / 220 | Bob's coins ------> Bob coinswap address 221 | \ 222 | \ 223 | Alice with knowledge of the hash preimage 224 | ``` 225 | 226 | If Alice attempts to take the coins from Bob's coinswap address using her knowledge of the hash preimage and Bob's contract transaction, then Bob will be able to read the value of the hash preimage from the blockchain, and use it to take the coins from Alice's coinswap address. This happens in the worst case, but in virtually all real-life situations it will never get to that point. The contracts usually always stay unbroadcasted. 227 | 228 | So at this point we've reached a situation where if Alice gets paid then Bob cannot fail to get paid, and vice versa. Now to save time and miner fees, the party which started with knowledge of the hash preimage will reveal it, and both parties will send each other their private keys corresponding to their public keys in the 2-of-2 multisigs. After this private key handover Alice will know both private keys in the relevant multisig address, and so those coins are in her sole possession. The same is true for Bob. 229 | 230 | ``` 231 | Alice's coins ----> Bob's address 232 | 233 | Bob's coins ----> Alice's address 234 | ``` 235 | 236 | In a successful coinswap, Alice's and Bob's coinswap addresses transform off-chain to be possessed by the other party 237 | 238 | 239 | [Bitcoin's script](https://en.bitcoin.it/wiki/Script) is used to code these timelock and hashlock conditions. Diagrams of the transactions: 240 | ``` 241 | = Alice's funding transaction = 242 | Alice's inputs -----> multisig (Alice pubkey + Bob pubkey) 243 | 244 | = Bob's funding transaction = 245 | Bob's inputs -----> multisig (Bob pubkey + Alice pubkey) 246 | 247 | = Alice's contract transaction= 248 | multisig (Alice pubkey + Bob pubkey) -----> contract script (Alice pubkey + timelock OR Bob pubkey + hashlock) 249 | 250 | = Bob's contract transaction= 251 | multisig (Bob pubkey + Alice pubkey) -----> contract script (Bob pubkey + timelock OR Alice pubkey + hashlock) 252 | ``` 253 | 254 | The contract transactions are only ever used if a dispute occurs. If all goes well the contract transactions never hit the blockchain and so the hashlock is never revealed, and therefore the coinswap improves privacy by delinking the transaction graph. 255 | 256 | The party which starts with knowledge of the hash preimage must have a longer timeout, this means there is always enough time for the party without knowledge of the preimage to read the preimage from the blockchain and get their own transaction confirmed. 257 | 258 | This explanation describes the simplest form of coinswap. On its own it isn't enough to build a really great private system. For more building blocks read the [design document of this project](https://gist.github.com/chris-belcher/9144bd57a91c194e332fb5ca371d0964). 259 | 260 | ### Notes on architecture 261 | 262 | Makers are servers which run Tor hidden services (or possibly other hosting solutions in case Tor ever stops working). Takers connect to them. Makers never connect to each other. 263 | 264 | Diagram of connections for a 4-hop coinswap: 265 | ``` 266 | ---- Bob 267 | / 268 | / 269 | Alice ------ Charlie 270 | \ 271 | \ 272 | ---- Dennis 273 | ``` 274 | 275 | The coinswap itself is multi-hop: 276 | 277 | ``` 278 | Alice ===> Bob ===> Charlie ===> Dennis ===> Alice 279 | ``` 280 | 281 | Makers are not even meant to know how many other makers there are in the route. They just offer their services, offer their fees, protect themselves from DOS, complete the coinswaps and make sure they get paid those fees. We aim to have makers have as little state as possible, which should help with DOS-resistance. 282 | 283 | All the big decisions are made by takers (which makes sense because takers are paying, and the customer is always right.) 284 | Decisions like: 285 | * How many makers in the route 286 | * How many transactions in the multi-transaction coinswap 287 | * How long to wait between funding txes 288 | * The bitcoin amount in the coinswap 289 | 290 | In this protocol it's always important to as much as possible avoid DOS attack opportunities, especially against makers. 291 | 292 | 293 | ### Protocol between takers and makers 294 | 295 | Alice is the taker, Bob, Charlie and Dennis are makers. For a detailed explanation including definitions see the mailing list email [here](https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2020-October/018221.html). That email should be read first and then you can jump back to the diagram below when needed while reading the code. 296 | 297 | Protocol messages are defined by the structs found in `src/messages.rs` and serialized into json with rust's serde crate. 298 | 299 | ``` 300 | | Alice | Bob | Charlie | Dennis | message, or (step) if repeat 301 | |=================|=================|=================|=================| 302 | 0. AB/A htlc ----> | | | sign senders contract 303 | 1. <---- AB/A htlc B/2 | | | senders contract sig 304 | 2. ************** BROADCAST AND MINE ALICE FUNDING TX *************** | 305 | 3. A fund ----> | | | proof of funding 306 | 4. <----AB/B+BC/B htlc | | | sign senders and receivers contract 307 | 5. BC/B htlc ----------------------> | | (0) 308 | 6. <---------------------- BC/B htlc C/2 | | (1) 309 | 7. AB/B+BC/B A+C/2---> | | | senders and receivers contract sig 310 | 8. ************** BROADCAST AND MINE BOB FUNDING TX *************** | 311 | A. B fund ----------------------> | | (3) 312 | B. <----------------------BC/C+CD/C htlc | | (4) 313 | C. CD/C htcl ----------------------------------------> | (0) 314 | D. <---------------------------------------- CD/C htlc D/2 | (1) 315 | E. BC/C htlc ----> | | | sign receiver contract 316 | F. <---- BC/C htlc B/2 | | | receiver contract sig 317 | G.BC/C+CD/C B+D/2-----------------------> | | (7) 318 | H. ************** BROADCAST AND MINE CHARLIE FUNDING TX ************** | 319 | I. C fund ----------------------------------------> | (3) 320 | J. <----------------------------------------CD/D+DA/D htlc | (4) 321 | K. CD/D htlc ----------------------> | | (E) 322 | L. <---------------------- CD/D htlc C/2 | | (F) 323 | M.CD/D+DA/D C+D/2----------------------------------------> | (7) 324 | N. ************** BROADCAST AND MINE DENNIS FUNDING TX *************** | 325 | O. DA/A htlc ----------------------------------------> | (E) 326 | P. <---------------------------------------- DA/A htlc D/2 | (F) 327 | Q. hash preimage ----> | | | hash preimage 328 | R. <---- privB(B+C) | | | privkey handover 329 | S. privA(A+B) ----> | | | (R) 330 | T. hash preimage ----------------------> | | (Q) 331 | U. <---------------------- privC(C+D) | | (R) 332 | V. privB(B+C) ----------------------> | | (R) 333 | W. hash preimage ----------------------------------------> | (Q) 334 | X <---------------------------------------- privD(D+A) | (R) 335 | Y. privC(C+D) ----------------------------------------> | (R) 336 | ``` 337 | 338 | #### Note on terminology: Sender and Receiver 339 | 340 | In the codebase and protocol documentation the words "Sender" and "Receiver" are used. These refer 341 | to either side of a coinswap address. The entity which created a transaction paying into a coinswap 342 | address is called the sender, because they sent the coins into the coinswap address. The other 343 | entity is called the receiver, because they will receive the coins after the coinswap is complete. 344 | 345 | ### Further reading 346 | 347 | * [Waxwing's blog post from 2017 about CoinSwap](https://web.archive.org/web/20200524041008/https://joinmarket.me/blog/blog/coinswaps/) 348 | 349 | * [gmaxwell's original coinswap writeup from 2013](https://bitcointalk.org/index.php?topic=321228.0). It explains how CoinSwap actually works. If you already understand how Lightning payment channels work then CoinSwap is similar. 350 | 351 | * [Design for improving JoinMarket's resistance to sybil attacks using fidelity bonds](https://gist.github.com/chris-belcher/18ea0e6acdb885a2bfbdee43dcd6b5af/). Document explaining the concept of fidelity bonds and how they provide resistance against sybil attacks. 352 | 353 | 354 | 355 | ## Chris Belcher's personal roadmap for the project 356 | 357 | * ☑ learn rust 358 | * ☑ learn rust-bitcoin 359 | * ☑ design a protocol where all the features (vanilla coinswap, multi-tx coinswap, routed coinswap, branching routed coinswap, privkey handover) can be done, and publish to mailing list 360 | * ☑ code the simplest possible wallet, seed phrases "generate" and "recover", no fidelity bonds, everything is sybil attackable or DOS attackable for now, no RBF 361 | * ☑ implement creation and signing of traditional multisig 362 | * ☑ code makers and takers to support simple coinswap 363 | * ☑ code makers and takers to support multi-transaction coinswaps without any security (e.g. no broadcasting of contract transactions) 364 | * ☑ code makers and takers to support multi-hop coinswaps without security 365 | * ☑ write more developer documentation 366 | * ☐ set up a solution to mirror this repository somewhere else in case github rm's it like they did youtube-dl 367 | * ☑ implement and deploy fidelity bonds in joinmarket, to experiment and gain experience with the concept 368 | * ☑ add proper error handling to this project, as right now most of the time it will exit on anything unexpected 369 | * ☑ code security, recover from aborts and deveations 370 | * ☑ implement coinswap fees and taker paying for miner fees 371 | * ☑ add support for connecting to makers that arent on localhost, and tor support 372 | * ☑ code federated message board seeder servers 373 | * ☑ ALPHA RELEASE FOR TESTNET, REGTEST, SIGNET AND MAINNET (FOR THE BRAVE ONES) 374 | * ☑ have watchtower store data in a file, not in RAM 375 | * ☐ study ecdsa-2p and implement ecdsa-2p multisig so the coinswaps can look identical to regular txes 376 | * ☐ have taker store the progress of a coinswap to file, so that the whole process can be easily paused and started 377 | * ☐ add automated incremental backups for wallet files, because seed phrases aren't enough to back up these wallets 378 | * ☐ code fidelity bonds 379 | * ☐ add support precomputed RBF fee-bumps, so that txes can always be confirmed regardless of the block space market 380 | * ☐ automated tests (might be earlier in case its useful in test driven development) 381 | * ☐ move wallet files and config to its own data directory ~/.teleport/ 382 | * ☐ add collateral inputs to receiver contract txes 383 | * ☐ implement encrypted contract txes for watchtowers, so that watchtowers can do their job without needing to know the addresses involved 384 | * ☐ implement branching and merging coinswaps for takers, so that they can create coinswaps even if they just have one UTXO 385 | * ☐ add encrypted wallet files 386 | * ☐ reproducible builds + pin dependencies to a hash 387 | * ☐ break as many blockchain analysis heuristics as possible, e.g. change address detection 388 | * ☐ create a GUI for taker 389 | * ☐ find coins landing on already-used addresses and freeze them, to resist the [forced address reuse attack](https://en.bitcoin.it/wiki/Privacy#Forced_address_reuse) 390 | * ☐ payjoin-with-coinswap with decoy UTXOs 391 | * ☐ convert contracts which currently use script to instead use adaptor signatures, aiming to not reveal contracts in the backout case 392 | * ☐ create a [web API](https://github.com/JoinMarket-Org/joinmarket-clientserver/blob/master/docs/JSON-RPC-API-using-jmwalletd.md) similar to the [one in joinmarket](https://github.com/JoinMarket-Org/joinmarket-clientserver/issues/978) 393 | * ☐ randomized locktimes, study with bayesian inference the best way to randomize them so that an individual maker learns as little information as possible from the locktime value 394 | * ☐ anti-DOS protocol additions for maker (not using json but some kind of binary format that is harder to DOS) 395 | * ☐ abstract away the Core RPC so that its functions can be done in another way, for example for the taker being supported as a plugin for electrum 396 | * ☐ make the project into a plugin which can be used by other wallets to do the taker role, try to implement it for electrum wallet 397 | 398 | ## Community 399 | 400 | * IRC channel: `#coinswap`. Logs available [here](http://gnusha.org/coinswap/). Accessible on the [libera IRC network](https://libera.chat/) at `irc.libera.chat:6697 (TLS)` and on the [webchat client](https://web.libera.chat/#coinswap). Accessible anonymously to Tor users on the [Hackint network](https://www.hackint.org/transport/tor) at `ncwkrwxpq2ikcngxq3dy2xctuheniggtqeibvgofixpzvrwpa77tozqd.onion:6667`. 401 | 402 | * Chris Belcher's work diary: https://gist.github.com/chris-belcher/ca5051285c6f8d38693fd127575be44d 403 | 404 | -------------------------------------------------------------------------------- /codecov.yaml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | target: auto 6 | threshold: 1% 7 | base: auto 8 | informational: false 9 | patch: 10 | default: 11 | target: auto 12 | threshold: 100% 13 | base: auto -------------------------------------------------------------------------------- /src/direct_send.rs: -------------------------------------------------------------------------------- 1 | use std::num::ParseIntError; 2 | use std::str::FromStr; 3 | 4 | use bitcoin::{Address, Amount, Network, OutPoint, Script, Transaction, TxIn, TxOut}; 5 | 6 | use bitcoincore_rpc::json::ListUnspentResultEntry; 7 | use bitcoincore_rpc::Client; 8 | 9 | use crate::contracts::SwapCoin; 10 | use crate::error::Error; 11 | use crate::fidelity_bonds::get_locktime_from_index; 12 | use crate::wallet_sync::{UTXOSpendInfo, Wallet}; 13 | 14 | #[derive(Debug)] 15 | pub enum SendAmount { 16 | Max, 17 | Amount(Amount), 18 | } 19 | 20 | impl FromStr for SendAmount { 21 | type Err = ParseIntError; 22 | 23 | fn from_str(s: &str) -> Result { 24 | Ok(if s == "max" { 25 | SendAmount::Max 26 | } else { 27 | SendAmount::Amount(Amount::from_sat(String::from(s).parse::()?)) 28 | }) 29 | } 30 | } 31 | 32 | #[derive(Debug)] 33 | pub enum Destination { 34 | Wallet, 35 | Address(Address), 36 | } 37 | 38 | impl FromStr for Destination { 39 | type Err = bitcoin::util::address::Error; 40 | 41 | fn from_str(s: &str) -> Result { 42 | Ok(if s == "wallet" { 43 | Destination::Wallet 44 | } else { 45 | Destination::Address(Address::from_str(s)?) 46 | }) 47 | } 48 | } 49 | 50 | #[derive(Debug)] 51 | pub enum CoinToSpend { 52 | LongForm(OutPoint), 53 | ShortForm { 54 | prefix: String, 55 | suffix: String, 56 | vout: u32, 57 | }, 58 | } 59 | 60 | fn parse_short_form_coin(s: &str) -> Option { 61 | //example short form: 568a4e..83a2e8:0 62 | if s.len() < 15 { 63 | return None; 64 | } 65 | let dots = &s[6..8]; 66 | if dots != ".." { 67 | return None; 68 | } 69 | let colon = s.chars().nth(14).unwrap(); 70 | if colon != ':' { 71 | return None; 72 | } 73 | let prefix = String::from(&s[0..6]); 74 | let suffix = String::from(&s[8..14]); 75 | let vout = *(&s[15..].parse::().ok()?); 76 | Some(CoinToSpend::ShortForm { 77 | prefix, 78 | suffix, 79 | vout, 80 | }) 81 | } 82 | 83 | impl FromStr for CoinToSpend { 84 | type Err = bitcoin::blockdata::transaction::ParseOutPointError; 85 | 86 | fn from_str(s: &str) -> Result { 87 | let parsed_outpoint = OutPoint::from_str(s); 88 | if parsed_outpoint.is_ok() { 89 | Ok(CoinToSpend::LongForm(parsed_outpoint.unwrap())) 90 | } else { 91 | let short_form = parse_short_form_coin(s); 92 | if short_form.is_some() { 93 | Ok(short_form.unwrap()) 94 | } else { 95 | Err(parsed_outpoint.err().unwrap()) 96 | } 97 | } 98 | } 99 | } 100 | 101 | impl Wallet { 102 | pub fn create_direct_send( 103 | &mut self, 104 | rpc: &Client, 105 | fee_rate: u64, 106 | send_amount: SendAmount, 107 | destination: Destination, 108 | coins_to_spend: &[CoinToSpend], 109 | ) -> Result { 110 | let mut tx_inputs = Vec::::new(); 111 | let mut unspent_inputs = Vec::<(ListUnspentResultEntry, UTXOSpendInfo)>::new(); 112 | //TODO this search within a search could get very slow 113 | let list_unspent_result = self.list_unspent_from_wallet(rpc, true, true)?; 114 | for (list_unspent_entry, spend_info) in list_unspent_result { 115 | for cts in coins_to_spend { 116 | let previous_output = match cts { 117 | CoinToSpend::LongForm(outpoint) => { 118 | if list_unspent_entry.txid == outpoint.txid 119 | && list_unspent_entry.vout == outpoint.vout 120 | { 121 | *outpoint 122 | } else { 123 | continue; 124 | } 125 | } 126 | CoinToSpend::ShortForm { 127 | prefix, 128 | suffix, 129 | vout, 130 | } => { 131 | let txid_hex = list_unspent_entry.txid.to_string(); 132 | if txid_hex.starts_with(prefix) 133 | && txid_hex.ends_with(suffix) 134 | && list_unspent_entry.vout == *vout 135 | { 136 | OutPoint { 137 | txid: list_unspent_entry.txid, 138 | vout: list_unspent_entry.vout, 139 | } 140 | } else { 141 | continue; 142 | } 143 | } 144 | }; 145 | log::debug!("found coin to spend = {:?}", previous_output); 146 | 147 | let sequence = match spend_info { 148 | UTXOSpendInfo::TimelockContract { 149 | ref swapcoin_multisig_redeemscript, 150 | input_value: _, 151 | } => self 152 | .find_outgoing_swapcoin(swapcoin_multisig_redeemscript) 153 | .unwrap() 154 | .get_timelock() as u32, 155 | UTXOSpendInfo::HashlockContract { 156 | swapcoin_multisig_redeemscript: _, 157 | input_value: _, 158 | } => 1, //hashlock spends must have 1 because of the `OP_CSV 1` 159 | _ => 0, 160 | }; 161 | tx_inputs.push(TxIn { 162 | previous_output, 163 | sequence, 164 | witness: Vec::new(), 165 | script_sig: Script::new(), 166 | }); 167 | unspent_inputs.push((list_unspent_entry.clone(), spend_info.clone())); 168 | } 169 | } 170 | if tx_inputs.len() != coins_to_spend.len() { 171 | panic!( 172 | "unable to find all given inputs, only found = {:?}", 173 | tx_inputs 174 | ); 175 | } 176 | 177 | let dest_addr = match destination { 178 | Destination::Wallet => self.get_next_external_address(rpc)?, 179 | Destination::Address(a) => { 180 | //testnet and signet addresses have the same vbyte 181 | //so a.network is always testnet even if the address is signet 182 | let testnet_signet_type = (a.network == Network::Testnet 183 | || a.network == Network::Signet) 184 | && (self.network == Network::Testnet || self.network == Network::Signet); 185 | if a.network != self.network && !testnet_signet_type { 186 | panic!("wrong address network type (e.g. mainnet, testnet, regtest, signet)"); 187 | } 188 | a 189 | } 190 | }; 191 | let miner_fee = 500 * fee_rate / 1000; //TODO this is just a rough estimate now 192 | 193 | let mut output = Vec::::new(); 194 | let total_input_value = unspent_inputs 195 | .iter() 196 | .fold(Amount::ZERO, |acc, u| acc + u.0.amount) 197 | .as_sat(); 198 | output.push(TxOut { 199 | script_pubkey: dest_addr.script_pubkey(), 200 | value: match send_amount { 201 | SendAmount::Max => total_input_value - miner_fee, 202 | SendAmount::Amount(a) => a.as_sat(), 203 | }, 204 | }); 205 | if let SendAmount::Amount(amount) = send_amount { 206 | output.push(TxOut { 207 | script_pubkey: self.get_next_internal_addresses(rpc, 1)?[0].script_pubkey(), 208 | value: total_input_value - amount.as_sat() - miner_fee, 209 | }); 210 | } 211 | 212 | let lock_time = unspent_inputs 213 | .iter() 214 | .map(|(_, spend_info)| { 215 | if let UTXOSpendInfo::FidelityBondCoin { 216 | index, 217 | input_value: _, 218 | } = spend_info 219 | { 220 | get_locktime_from_index(*index) as u32 + 1 221 | } else { 222 | 0 //TODO add anti-fee-sniping here 223 | } 224 | }) 225 | .max() 226 | .unwrap(); 227 | 228 | let mut tx = Transaction { 229 | input: tx_inputs, 230 | output, 231 | lock_time, 232 | version: 2, 233 | }; 234 | log::debug!("unsigned transaction = {:#?}", tx); 235 | self.sign_transaction( 236 | &mut tx, 237 | &mut unspent_inputs.iter().map(|(_u, usi)| usi.clone()), 238 | ); 239 | Ok(tx) 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /src/directory_servers.rs: -------------------------------------------------------------------------------- 1 | //configure this with your own tor port 2 | pub const TOR_ADDR: &str = "127.0.0.1:9050"; 3 | 4 | use bitcoin::Network; 5 | 6 | use crate::offerbook_sync::MakerAddress; 7 | 8 | //for now just one of these, but later we'll need multiple for good decentralization 9 | const DIRECTORY_SERVER_ADDR: &str = 10 | "zfwo4t5yfuf6epu7rhjbmkr6kiysi6v7kibta4i55zlp4y6xirpcr7qd.onion:8080"; 11 | 12 | #[derive(Debug)] 13 | pub enum DirectoryServerError { 14 | Reqwest(reqwest::Error), 15 | Other(&'static str), 16 | } 17 | 18 | impl From for DirectoryServerError { 19 | fn from(e: reqwest::Error) -> DirectoryServerError { 20 | DirectoryServerError::Reqwest(e) 21 | } 22 | } 23 | 24 | fn network_enum_to_string(network: Network) -> &'static str { 25 | match network { 26 | Network::Bitcoin => "mainnet", 27 | Network::Testnet => "testnet", 28 | Network::Signet => "signet", 29 | Network::Regtest => panic!("dont use directory servers if using regtest"), 30 | } 31 | } 32 | 33 | pub async fn sync_maker_addresses_from_directory_servers( 34 | network: Network, 35 | ) -> Result, DirectoryServerError> { 36 | // https://github.com/seanmonstar/reqwest/blob/master/examples/tor_socks.rs 37 | let proxy = 38 | reqwest::Proxy::all(format!("socks5h://{}", TOR_ADDR)).expect("tor proxy should be there"); 39 | let client = reqwest::Client::builder() 40 | .proxy(proxy) 41 | .build() 42 | .expect("should be able to build reqwest client"); 43 | let res = client 44 | .get(format!( 45 | "http://{}/makers-{}.txt", 46 | DIRECTORY_SERVER_ADDR, 47 | network_enum_to_string(network) 48 | )) 49 | .send() 50 | .await?; 51 | if res.status().as_u16() != 200 { 52 | return Err(DirectoryServerError::Other("status code not success")); 53 | } 54 | let mut maker_addresses = Vec::::new(); 55 | for makers in res.text().await?.split("\n") { 56 | let csv_chunks = makers.split(",").collect::>(); 57 | if csv_chunks.len() < 2 { 58 | continue; 59 | } 60 | maker_addresses.push(MakerAddress::Tor { 61 | address: String::from(csv_chunks[1]), 62 | }); 63 | log::debug!(target:"directory_servers", "expiry timestamp = {} address = {}", 64 | csv_chunks[0], csv_chunks[1]); 65 | } 66 | Ok(maker_addresses) 67 | } 68 | 69 | pub async fn post_maker_address_to_directory_servers( 70 | network: Network, 71 | address: &str, 72 | ) -> Result { 73 | let proxy = 74 | reqwest::Proxy::all(format!("socks5h://{}", TOR_ADDR)).expect("tor proxy should be there"); 75 | let client = reqwest::Client::builder() 76 | .proxy(proxy) 77 | .build() 78 | .expect("should be able to build reqwest client"); 79 | let params = [ 80 | ("address", address), 81 | ("net", network_enum_to_string(network)), 82 | ]; 83 | let res = client 84 | .post(format!("http://{}/directoryserver", DIRECTORY_SERVER_ADDR)) 85 | .form(¶ms) 86 | .send() 87 | .await?; 88 | if res.status().as_u16() != 200 { 89 | return Err(DirectoryServerError::Other("status code not success")); 90 | } 91 | let body = res.text().await?; 92 | let start_bytes = body 93 | .find("") 94 | .ok_or(DirectoryServerError::Other("expiry time not parsable1"))? 95 | + 3; 96 | let end_bytes = body 97 | .find("") 98 | .ok_or(DirectoryServerError::Other("expiry time not parsable2"))?; 99 | let expiry_time_str = &body[start_bytes..end_bytes]; 100 | let expiry_time = expiry_time_str 101 | .parse::() 102 | .map_err(|_| DirectoryServerError::Other("expiry time not parsable3"))?; 103 | Ok(expiry_time) 104 | } 105 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::error; 2 | use std::io; 3 | 4 | // error enum for the whole project 5 | // try to make functions return this 6 | #[derive(Debug)] 7 | pub enum Error { 8 | Network(Box), 9 | Disk(io::Error), 10 | Protocol(&'static str), 11 | Rpc(bitcoincore_rpc::Error), 12 | Socks(tokio_socks::Error), 13 | } 14 | 15 | impl From> for Error { 16 | fn from(e: Box) -> Error { 17 | Error::Network(e) 18 | } 19 | } 20 | 21 | impl From for Error { 22 | fn from(e: io::Error) -> Error { 23 | Error::Disk(e) 24 | } 25 | } 26 | 27 | impl From for Error { 28 | fn from(e: bitcoincore_rpc::Error) -> Error { 29 | Error::Rpc(e) 30 | } 31 | } 32 | 33 | impl From for Error { 34 | fn from(e: tokio_socks::Error) -> Error { 35 | Error::Socks(e) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/fidelity_bonds.rs: -------------------------------------------------------------------------------- 1 | // To (strongly) disincentivize Sybil behaviour, the value assessment of the bond 2 | // is based on the (time value of the bond)^x where x is the bond_value_exponent here, 3 | // where x > 1. 4 | const BOND_VALUE_EXPONENT: f64 = 1.3; 5 | 6 | // Interest rate used when calculating the value of fidelity bonds created 7 | // by locking bitcoins in timelocked addresses 8 | // See also: 9 | // https://gist.github.com/chris-belcher/87ebbcbb639686057a389acb9ab3e25b#determining-interest-rate-r 10 | // Set as a real number, i.e. 1 = 100% and 0.01 = 1% 11 | const BOND_VALUE_INTEREST_RATE: f64 = 0.015; 12 | 13 | use std::collections::HashMap; 14 | use std::fmt::Display; 15 | use std::num::ParseIntError; 16 | use std::str::FromStr; 17 | 18 | use chrono::NaiveDate; 19 | 20 | use bitcoin::blockdata::opcodes; 21 | use bitcoin::blockdata::script::{Builder, Instruction, Script}; 22 | use bitcoin::hashes::sha256d; 23 | use bitcoin::hashes::Hash; 24 | use bitcoin::secp256k1::{Context, Message, Secp256k1, SecretKey, Signing}; 25 | use bitcoin::util::bip32::{ChildNumber, DerivationPath, ExtendedPrivKey}; 26 | use bitcoin::util::key::{PrivateKey, PublicKey}; 27 | use bitcoin::{Address, OutPoint}; 28 | 29 | use bitcoincore_rpc::json::{GetTxOutResult, ListUnspentResultEntry}; 30 | use bitcoincore_rpc::{Client, RpcApi}; 31 | 32 | use crate::contracts::redeemscript_to_scriptpubkey; 33 | use crate::error::Error; 34 | use crate::messages::FidelityBondProof; 35 | use crate::wallet_sync::{generate_keypair, UTXOSpendInfo, Wallet}; 36 | 37 | pub const TIMELOCKED_MPK_PATH: &str = "m/84'/0'/0'/2"; 38 | pub const TIMELOCKED_ADDRESS_COUNT: u32 = 960; 39 | 40 | pub const REGTEST_DUMMY_ONION_HOSTNAME: &str = "regtest-dummy-onion-hostname.onion"; 41 | 42 | #[derive(Debug)] 43 | pub struct YearAndMonth { 44 | year: u32, 45 | month: u32, 46 | } 47 | 48 | impl YearAndMonth { 49 | pub fn new(year: u32, month: u32) -> YearAndMonth { 50 | YearAndMonth { year, month } 51 | } 52 | 53 | pub fn to_index(&self) -> u32 { 54 | (self.year - 2020) * 12 + (self.month - 1) 55 | } 56 | } 57 | 58 | impl FromStr for YearAndMonth { 59 | type Err = YearAndMonthError; 60 | 61 | // yyyy-mm 62 | fn from_str(s: &str) -> Result { 63 | if s.len() != 7 { 64 | return Err(YearAndMonthError::WrongLength); 65 | } 66 | let year = String::from(&s[..4]).parse::()?; 67 | let month = String::from(&s[5..]).parse::()?; 68 | if 2020 <= year && year <= 2079 && 1 <= month && month <= 12 { 69 | Ok(YearAndMonth { year, month }) 70 | } else { 71 | Err(YearAndMonthError::OutOfRange) 72 | } 73 | } 74 | } 75 | 76 | #[derive(Debug)] 77 | pub enum YearAndMonthError { 78 | WrongLength, 79 | ParseIntError(ParseIntError), 80 | OutOfRange, 81 | } 82 | 83 | impl From for YearAndMonthError { 84 | fn from(p: ParseIntError) -> YearAndMonthError { 85 | YearAndMonthError::ParseIntError(p) 86 | } 87 | } 88 | 89 | impl Display for YearAndMonthError { 90 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 91 | match self { 92 | YearAndMonthError::WrongLength => write!(f, "WrongLength, should be yyyy-mm"), 93 | YearAndMonthError::ParseIntError(p) => p.fmt(f), 94 | YearAndMonthError::OutOfRange => { 95 | write!(f, "Out of range, must be between 2020-01 and 2079-12") 96 | } 97 | } 98 | } 99 | } 100 | 101 | fn create_cert_msg_hash(cert_pubkey: &PublicKey, cert_expiry: u16) -> Message { 102 | let cert_msg_str = format!("fidelity-bond-cert|{}|{}", cert_pubkey, cert_expiry); 103 | let cert_msg = cert_msg_str.as_bytes(); 104 | let mut btc_signed_msg = Vec::::new(); 105 | btc_signed_msg.extend("\x18Bitcoin Signed Message:\n".as_bytes()); 106 | btc_signed_msg.push(cert_msg.len() as u8); 107 | btc_signed_msg.extend(cert_msg); 108 | Message::from_slice(&sha256d::Hash::hash(&btc_signed_msg)).unwrap() 109 | } 110 | 111 | pub struct HotWalletFidelityBond { 112 | pub utxo: OutPoint, 113 | utxo_key: PublicKey, 114 | locktime: i64, 115 | utxo_privkey: SecretKey, 116 | } 117 | 118 | impl HotWalletFidelityBond { 119 | pub fn new(wallet: &Wallet, utxo: &ListUnspentResultEntry, spend_info: &UTXOSpendInfo) -> Self { 120 | let index = if let UTXOSpendInfo::FidelityBondCoin { 121 | index, 122 | input_value: _, 123 | } = spend_info 124 | { 125 | *index 126 | } else { 127 | panic!("bug, should be fidelity bond coin") 128 | }; 129 | let redeemscript = wallet.get_timelocked_redeemscript_from_index(index); 130 | Self { 131 | utxo: OutPoint { 132 | txid: utxo.txid, 133 | vout: utxo.vout, 134 | }, 135 | utxo_key: read_pubkey_from_timelocked_redeemscript(&redeemscript).unwrap(), 136 | locktime: read_locktime_from_timelocked_redeemscript(&redeemscript).unwrap(), 137 | utxo_privkey: wallet.get_timelocked_privkey_from_index(index).key, 138 | } 139 | } 140 | 141 | pub fn create_proof( 142 | &self, 143 | rpc: &Client, 144 | onion_hostname: &str, 145 | ) -> Result { 146 | const BLOCK_COUNT_SAFETY: u64 = 2; 147 | const RETARGET_INTERVAL: u64 = 2016; 148 | const CERT_MAX_VALIDITY_TIME: u64 = 1; 149 | 150 | let blocks = rpc.get_block_count()?; 151 | let cert_expiry = 152 | ((blocks + BLOCK_COUNT_SAFETY) / RETARGET_INTERVAL) + CERT_MAX_VALIDITY_TIME; 153 | let cert_expiry = cert_expiry as u16; 154 | 155 | let (cert_pubkey, cert_privkey) = generate_keypair(); 156 | let secp = Secp256k1::new(); 157 | 158 | let cert_msg_hash = create_cert_msg_hash(&cert_pubkey, cert_expiry); 159 | let cert_sig = secp.sign(&cert_msg_hash, &self.utxo_privkey); 160 | 161 | let onion_msg_hash = 162 | Message::from_slice(&sha256d::Hash::hash(onion_hostname.as_bytes())).unwrap(); 163 | let onion_sig = secp.sign(&onion_msg_hash, &cert_privkey); 164 | 165 | Ok(FidelityBondProof { 166 | utxo: self.utxo, 167 | utxo_key: self.utxo_key, 168 | locktime: self.locktime, 169 | cert_sig, 170 | cert_expiry, 171 | cert_pubkey, 172 | onion_sig, 173 | }) 174 | } 175 | } 176 | 177 | impl FidelityBondProof { 178 | pub fn verify_and_get_txo( 179 | &self, 180 | rpc: &Client, 181 | block_count: u64, 182 | onion_hostname: &str, 183 | ) -> Result { 184 | let secp = Secp256k1::new(); 185 | 186 | let onion_msg_hash = 187 | Message::from_slice(&sha256d::Hash::hash(onion_hostname.as_bytes())).unwrap(); 188 | secp.verify(&onion_msg_hash, &self.onion_sig, &self.cert_pubkey.key) 189 | .map_err(|_| Error::Protocol("onion sig does not verify"))?; 190 | 191 | let cert_msg_hash = create_cert_msg_hash(&self.cert_pubkey, self.cert_expiry); 192 | secp.verify(&cert_msg_hash, &self.cert_sig, &self.utxo_key.key) 193 | .map_err(|_| Error::Protocol("cert sig does not verify"))?; 194 | 195 | let txo_data = rpc 196 | .get_tx_out(&self.utxo.txid, self.utxo.vout, None)? 197 | .ok_or(Error::Protocol("fidelity bond UTXO doesnt exist"))?; 198 | 199 | const RETARGET_INTERVAL: u64 = 2016; 200 | if block_count > self.cert_expiry as u64 * RETARGET_INTERVAL { 201 | return Err(Error::Protocol("cert has expired")); 202 | } 203 | 204 | let implied_spk = redeemscript_to_scriptpubkey(&create_timelocked_redeemscript( 205 | self.locktime, 206 | &self.utxo_key, 207 | )); 208 | if txo_data.script_pub_key.hex != implied_spk.into_bytes() { 209 | return Err(Error::Protocol("UTXO script doesnt match given script")); 210 | } 211 | 212 | //an important thing we cant verify in this function 213 | //is that a given fidelity bond UTXO was only used once in the offer book 214 | //that has to be checked elsewhere 215 | 216 | Ok(txo_data) 217 | } 218 | 219 | pub fn calculate_fidelity_bond_value( 220 | &self, 221 | rpc: &Client, 222 | block_count: u64, 223 | txo_data: &GetTxOutResult, 224 | mediantime: u64, 225 | ) -> Result { 226 | let blockhash = rpc.get_block_hash(block_count - txo_data.confirmations as u64 + 1)?; 227 | Ok(calculate_timelocked_fidelity_bond_value( 228 | txo_data.value.as_sat(), 229 | self.locktime, 230 | rpc.get_block_header_info(&blockhash)?.time as i64, 231 | mediantime, 232 | )) 233 | } 234 | } 235 | 236 | #[allow(non_snake_case)] 237 | fn calculate_timelocked_fidelity_bond_value( 238 | value_sats: u64, 239 | locktime: i64, 240 | confirmation_time: i64, 241 | current_time: u64, 242 | ) -> f64 { 243 | const YEAR: f64 = 60.0 * 60.0 * 24.0 * 365.2425; //gregorian calender year length 244 | 245 | let r = BOND_VALUE_INTEREST_RATE; 246 | let T = (locktime - confirmation_time) as f64 / YEAR; 247 | let L = locktime as f64 / YEAR; 248 | let t = current_time as f64 / YEAR; 249 | 250 | let exp_rT_m1 = f64::exp_m1(r * T); 251 | let exp_rtL_m1 = f64::exp_m1(r * f64::max(0.0, t - L)); 252 | 253 | let timevalue = f64::max(0.0, f64::min(1.0, exp_rT_m1) - f64::min(1.0, exp_rtL_m1)); 254 | 255 | (value_sats as f64 * timevalue).powf(BOND_VALUE_EXPONENT) 256 | } 257 | 258 | fn calculate_timelocked_fidelity_bond_value_from_utxo( 259 | utxo: &ListUnspentResultEntry, 260 | usi: &UTXOSpendInfo, 261 | rpc: &Client, 262 | ) -> Result { 263 | Ok(calculate_timelocked_fidelity_bond_value( 264 | utxo.amount.as_sat(), 265 | get_locktime_from_index( 266 | if let UTXOSpendInfo::FidelityBondCoin { 267 | index, 268 | input_value: _, 269 | } = usi 270 | { 271 | *index 272 | } else { 273 | panic!("bug, should be fidelity bond coin") 274 | }, 275 | ), 276 | rpc.get_transaction(&utxo.txid, Some(true))? 277 | .info 278 | .blocktime 279 | .unwrap() as i64, 280 | rpc.get_blockchain_info()?.median_time, 281 | )) 282 | } 283 | 284 | fn create_timelocked_redeemscript(locktime: i64, pubkey: &PublicKey) -> Script { 285 | Builder::new() 286 | .push_int(locktime) 287 | .push_opcode(opcodes::all::OP_CLTV) 288 | .push_opcode(opcodes::all::OP_DROP) 289 | .push_key(&pubkey) 290 | .push_opcode(opcodes::all::OP_CHECKSIG) 291 | .into_script() 292 | } 293 | 294 | pub fn read_locktime_from_timelocked_redeemscript(redeemscript: &Script) -> Option { 295 | if let Instruction::PushBytes(locktime_bytes) = redeemscript.instructions().nth(0)?.ok()? { 296 | let mut u8slice: [u8; 8] = [0; 8]; 297 | u8slice[..locktime_bytes.len()].copy_from_slice(&locktime_bytes); 298 | Some(i64::from_le_bytes(u8slice)) 299 | } else { 300 | None 301 | } 302 | } 303 | 304 | fn read_pubkey_from_timelocked_redeemscript(redeemscript: &Script) -> Option { 305 | if let Instruction::PushBytes(pubkey_bytes) = redeemscript.instructions().nth(3)?.ok()? { 306 | PublicKey::from_slice(pubkey_bytes).ok() 307 | } else { 308 | None 309 | } 310 | } 311 | 312 | fn get_timelocked_master_key_from_root_master_key(master_key: &ExtendedPrivKey) -> ExtendedPrivKey { 313 | let secp = Secp256k1::new(); 314 | 315 | master_key 316 | .derive_priv( 317 | &secp, 318 | &DerivationPath::from_str(TIMELOCKED_MPK_PATH).unwrap(), 319 | ) 320 | .unwrap() 321 | } 322 | 323 | pub fn get_locktime_from_index(index: u32) -> i64 { 324 | let year_off = index as i32 / 12; 325 | let month = index % 12; 326 | NaiveDate::from_ymd(2020 + year_off, 1 + month, 1) 327 | .and_hms(0, 0, 0) 328 | .timestamp() 329 | } 330 | 331 | fn get_timelocked_redeemscript_from_index( 332 | secp: &Secp256k1, 333 | timelocked_master_private_key: &ExtendedPrivKey, 334 | index: u32, 335 | ) -> Script { 336 | let privkey = timelocked_master_private_key 337 | .ckd_priv(secp, ChildNumber::Normal { index }) 338 | .unwrap() 339 | .private_key; 340 | let pubkey = privkey.public_key(&secp); 341 | let locktime = get_locktime_from_index(index); 342 | create_timelocked_redeemscript(locktime, &pubkey) 343 | } 344 | 345 | pub fn generate_all_timelocked_addresses(master_key: &ExtendedPrivKey) -> HashMap { 346 | let timelocked_master_private_key = get_timelocked_master_key_from_root_master_key(master_key); 347 | let mut timelocked_script_index_map = HashMap::::new(); 348 | 349 | let secp = Secp256k1::new(); 350 | //all these magic numbers and constants are explained in the fidelity bonds bip 351 | // https://gist.github.com/chris-belcher/7257763cedcc014de2cd4239857cd36e 352 | for index in 0..TIMELOCKED_ADDRESS_COUNT { 353 | let redeemscript = 354 | get_timelocked_redeemscript_from_index(&secp, &timelocked_master_private_key, index); 355 | let spk = redeemscript_to_scriptpubkey(&redeemscript); 356 | timelocked_script_index_map.insert(spk, index); 357 | } 358 | timelocked_script_index_map 359 | } 360 | 361 | impl Wallet { 362 | pub fn get_timelocked_redeemscript_from_index(&self, index: u32) -> Script { 363 | get_timelocked_redeemscript_from_index( 364 | &Secp256k1::new(), 365 | &get_timelocked_master_key_from_root_master_key(&self.master_key), 366 | index, 367 | ) 368 | } 369 | 370 | pub fn get_timelocked_privkey_from_index(&self, index: u32) -> PrivateKey { 371 | get_timelocked_master_key_from_root_master_key(&self.master_key) 372 | .ckd_priv(&Secp256k1::new(), ChildNumber::Normal { index }) 373 | .unwrap() 374 | .private_key 375 | } 376 | 377 | pub fn get_timelocked_address(&self, locktime: &YearAndMonth) -> (Address, i64) { 378 | let redeemscript = self.get_timelocked_redeemscript_from_index(locktime.to_index()); 379 | let addr = Address::p2wsh(&redeemscript, self.network); 380 | let unix_locktime = read_locktime_from_timelocked_redeemscript(&redeemscript) 381 | .expect("bug: unable to read locktime"); 382 | (addr, unix_locktime) 383 | } 384 | 385 | //returns Ok(None) if no fidelity bonds in wallet 386 | pub fn find_most_valuable_fidelity_bond( 387 | &self, 388 | rpc: &Client, 389 | ) -> Result, Error> { 390 | let list_unspent_result = self.list_unspent_from_wallet(&rpc, false, true)?; 391 | let fidelity_bond_utxos = list_unspent_result 392 | .iter() 393 | .filter(|(utxo, _)| utxo.confirmations > 0) 394 | .filter(|(_, usi)| match usi { 395 | UTXOSpendInfo::FidelityBondCoin { 396 | index: _, 397 | input_value: _, 398 | } => true, 399 | _ => false, 400 | }) 401 | .collect::>(); 402 | let fidelity_bond_values = fidelity_bond_utxos 403 | .iter() 404 | .map(|(utxo, usi)| calculate_timelocked_fidelity_bond_value_from_utxo(utxo, usi, rpc)) 405 | .collect::, Error>>()?; 406 | Ok(fidelity_bond_utxos 407 | .iter() 408 | .zip(fidelity_bond_values.iter()) 409 | //partial_cmp fails if NaN value involved, which wont happen, so unwrap() is acceptable 410 | .max_by(|(_, x), (_, y)| x.partial_cmp(y).unwrap()) 411 | .map(|most_valuable_fidelity_bond| { 412 | HotWalletFidelityBond::new( 413 | self, 414 | &most_valuable_fidelity_bond.0 .0, 415 | &most_valuable_fidelity_bond.0 .1, 416 | ) 417 | })) 418 | } 419 | } 420 | 421 | #[cfg(test)] 422 | mod test { 423 | use super::*; 424 | 425 | #[test] 426 | fn test_fidelity_bond_value_function_behavior() { 427 | const EPSILON: f64 = 0.000001; 428 | const YEAR: f64 = 60.0 * 60.0 * 24.0 * 365.2425; 429 | 430 | //the function should be flat anywhere before the locktime ends 431 | let values = (0..4) 432 | .map(|y| { 433 | calculate_timelocked_fidelity_bond_value( 434 | 100000000, 435 | (6.0 * YEAR) as i64, 436 | 0, 437 | y * YEAR as u64, 438 | ) 439 | }) 440 | .collect::>(); 441 | let value_diff = (0..values.len() - 1) 442 | .map(|i| values[i + 1] - values[i]) 443 | .collect::>(); 444 | for v in &value_diff { 445 | assert!(v.abs() < EPSILON); 446 | } 447 | 448 | //after locktime, the value should go down 449 | let values = (0..5) 450 | .map(|y| { 451 | calculate_timelocked_fidelity_bond_value( 452 | 100000000, 453 | (6.0 * YEAR) as i64, 454 | 0, 455 | (6 + y) * YEAR as u64, 456 | ) 457 | }) 458 | .collect::>(); 459 | let value_diff = (0..values.len() - 1) 460 | .map(|i| values[i + 1] - values[i]) 461 | .collect::>(); 462 | for v in &value_diff { 463 | assert!(*v < 0.0); 464 | } 465 | 466 | //value of a bond goes up as the locktime goes up 467 | let values = (0..5) 468 | .map(|y| { 469 | calculate_timelocked_fidelity_bond_value(100000000, (y as f64 * YEAR) as i64, 0, 0) 470 | }) 471 | .collect::>(); 472 | let value_ratio = (0..values.len() - 1) 473 | .map(|i| values[i] / values[i + 1]) 474 | .collect::>(); 475 | let value_ratio_diff = (0..value_ratio.len() - 1) 476 | .map(|i| value_ratio[i] - value_ratio[i + 1]) 477 | .collect::>(); 478 | for v in &value_ratio_diff { 479 | assert!(*v < 0.0); 480 | } 481 | 482 | //value of a bond locked into the far future is constant, clamped at the value of burned coins 483 | let values = (0..5) 484 | .map(|y| { 485 | calculate_timelocked_fidelity_bond_value( 486 | 100000000, 487 | ((200 + y) as f64 * YEAR) as i64, 488 | 0, 489 | 0, 490 | ) 491 | }) 492 | .collect::>(); 493 | let value_diff = (0..values.len() - 1) 494 | .map(|i| values[i] - values[i + 1]) 495 | .collect::>(); 496 | for v in &value_diff { 497 | assert!(v.abs() < EPSILON); 498 | } 499 | } 500 | } 501 | -------------------------------------------------------------------------------- /src/funding_tx.rs: -------------------------------------------------------------------------------- 1 | //this file contains routines for creating funding transactions 2 | 3 | use std::collections::HashMap; 4 | 5 | use itertools::izip; 6 | 7 | use bitcoin::{hashes::hex::FromHex, Address, Amount, OutPoint, Transaction, Txid}; 8 | 9 | use bitcoincore_rpc::json::{CreateRawTransactionInput, WalletCreateFundedPsbtOptions}; 10 | use bitcoincore_rpc::{Client, RpcApi}; 11 | 12 | use serde_json::Value; 13 | 14 | use rand::rngs::OsRng; 15 | use rand::RngCore; 16 | 17 | use crate::error::Error; 18 | use crate::wallet_sync::{convert_json_rpc_bitcoin_to_satoshis, Wallet}; 19 | 20 | pub struct CreateFundingTxesResult { 21 | pub funding_txes: Vec, 22 | pub payment_output_positions: Vec, 23 | pub total_miner_fee: u64, 24 | } 25 | 26 | impl Wallet { 27 | pub fn create_funding_txes( 28 | &self, 29 | rpc: &Client, 30 | coinswap_amount: u64, 31 | destinations: &[Address], 32 | fee_rate: u64, 33 | ) -> Result, Error> { 34 | //returns Ok(None) if there was no error but the wallet was unable to create funding txes 35 | 36 | log::debug!(target: "wallet", "coinswap_amount = {} destinations = {:?}", 37 | coinswap_amount, destinations); 38 | 39 | let ret = 40 | self.create_funding_txes_random_amounts(rpc, coinswap_amount, destinations, fee_rate); 41 | if ret.is_ok() { 42 | log::debug!(target: "wallet", "created funding txes with random amounts"); 43 | return ret; 44 | } 45 | 46 | let ret = 47 | self.create_funding_txes_utxo_max_sends(rpc, coinswap_amount, destinations, fee_rate); 48 | if ret.is_ok() { 49 | log::debug!(target: "wallet", "created funding txes with fully-spending utxos"); 50 | return ret; 51 | } 52 | 53 | let ret = self.create_funding_txes_use_biggest_utxos( 54 | rpc, 55 | coinswap_amount, 56 | destinations, 57 | fee_rate, 58 | ); 59 | if ret.is_ok() { 60 | log::debug!(target: "wallet", "created funding txes with using the biggest utxos"); 61 | return ret; 62 | } 63 | 64 | log::debug!(target: "wallet", "failed to create funding txes with any method"); 65 | ret 66 | } 67 | 68 | fn generate_amount_fractions_without_correction( 69 | count: usize, 70 | total_amount: u64, 71 | lower_limit: u64, 72 | ) -> Result, Error> { 73 | for _ in 0..100000 { 74 | let mut knives = (1..count) 75 | .map(|_| OsRng.next_u32() as f32 / u32::MAX as f32) 76 | .collect::>(); 77 | knives.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)); 78 | 79 | let mut fractions = Vec::::new(); 80 | let mut last: f32 = 1.0; 81 | for k in knives { 82 | fractions.push(last - k); 83 | last = k; 84 | } 85 | fractions.push(last); 86 | 87 | if fractions 88 | .iter() 89 | .all(|f| *f * (total_amount as f32) > lower_limit as f32) 90 | { 91 | return Ok(fractions); 92 | } 93 | } 94 | Err(Error::Protocol( 95 | "unable to generate amount fractions, probably amount too small", 96 | )) 97 | } 98 | 99 | fn generate_amount_fractions(count: usize, total_amount: u64) -> Result, Error> { 100 | let mut output_values = Wallet::generate_amount_fractions_without_correction( 101 | count, 102 | total_amount, 103 | 5000, //use 5000 satoshi as the lower limit for now 104 | //there should always be enough to pay miner fees 105 | )? 106 | .iter() 107 | .map(|f| (*f * total_amount as f32) as u64) 108 | .collect::>(); 109 | 110 | //rounding errors mean usually 1 or 2 satoshis are lost, add them back 111 | 112 | //this calculation works like this: 113 | //o = [a, b, c, ...] | list of output values 114 | //t = coinswap amount | total desired value 115 | //a' <-- a + (t - (a+b+c+...)) | assign new first output value 116 | //a' <-- a + (t -a-b-c-...) | rearrange 117 | //a' <-- t - b - c -... | 118 | *output_values.first_mut().unwrap() = 119 | total_amount - output_values.iter().skip(1).sum::(); 120 | assert_eq!(output_values.iter().sum::(), total_amount); 121 | log::debug!(target: "wallet", "output values = {:?}", output_values); 122 | 123 | Ok(output_values) 124 | } 125 | 126 | fn create_funding_txes_random_amounts( 127 | &self, 128 | rpc: &Client, 129 | coinswap_amount: u64, 130 | destinations: &[Address], 131 | fee_rate: u64, 132 | ) -> Result, Error> { 133 | //this function creates funding txes by 134 | //randomly generating some satoshi amounts and send them into 135 | //walletcreatefundedpsbt to create txes that create change 136 | 137 | let change_addresses = self.get_next_internal_addresses(rpc, destinations.len() as u32)?; 138 | log::debug!(target: "wallet", "change addrs = {:?}", change_addresses); 139 | 140 | let output_values = Wallet::generate_amount_fractions(destinations.len(), coinswap_amount)?; 141 | 142 | self.lock_all_nonwallet_unspents(rpc)?; 143 | 144 | let mut funding_txes = Vec::::new(); 145 | let mut payment_output_positions = Vec::::new(); 146 | let mut total_miner_fee = 0; 147 | for (address, &output_value, change_address) in izip!( 148 | destinations.iter(), 149 | output_values.iter(), 150 | change_addresses.iter() 151 | ) { 152 | log::debug!(target: "wallet", "output_value = {} to addr={}", output_value, address); 153 | 154 | let mut outputs = HashMap::::new(); 155 | outputs.insert(address.to_string(), Amount::from_sat(output_value)); 156 | 157 | let wcfp_result = rpc.wallet_create_funded_psbt( 158 | &[], 159 | &outputs, 160 | None, 161 | Some(WalletCreateFundedPsbtOptions { 162 | include_watching: Some(true), 163 | change_address: Some(change_address.clone()), 164 | fee_rate: Some(Amount::from_sat(fee_rate)), 165 | ..Default::default() 166 | }), 167 | None, 168 | )?; 169 | total_miner_fee += wcfp_result.fee.as_sat(); 170 | log::debug!(target: "wallet", "created funding tx, miner fee={}", wcfp_result.fee); 171 | 172 | let funding_tx = self.from_walletcreatefundedpsbt_to_tx(rpc, &wcfp_result.psbt)?; 173 | 174 | rpc.lock_unspent( 175 | &funding_tx 176 | .input 177 | .iter() 178 | .map(|vin| vin.previous_output) 179 | .collect::>(), 180 | )?; 181 | 182 | let payment_pos = if wcfp_result.change_position == 0 { 183 | 1 184 | } else { 185 | 0 186 | }; 187 | log::debug!(target: "wallet", "payment_pos = {}", payment_pos); 188 | 189 | funding_txes.push(funding_tx); 190 | payment_output_positions.push(payment_pos); 191 | } 192 | 193 | Ok(Some(CreateFundingTxesResult { 194 | funding_txes, 195 | payment_output_positions, 196 | total_miner_fee, 197 | })) 198 | } 199 | 200 | fn create_mostly_sweep_txes_with_one_tx_having_change( 201 | &self, 202 | rpc: &Client, 203 | coinswap_amount: u64, 204 | destinations: &[Address], 205 | fee_rate: u64, 206 | change_address: &Address, 207 | utxos: &mut dyn Iterator, 208 | //utxos item is (txid, vout, value) 209 | //utxos should be sorted by size, largest first 210 | ) -> Result, Error> { 211 | let mut funding_txes = Vec::::new(); 212 | let mut payment_output_positions = Vec::::new(); 213 | let mut total_miner_fee = 0; 214 | 215 | let mut leftover_coinswap_amount = coinswap_amount; 216 | let mut destinations_iter = destinations.iter(); 217 | let first_tx_input = utxos.next().unwrap(); 218 | 219 | for _ in 0..(destinations.len() - 2) { 220 | let (txid, vout, value) = utxos.next().unwrap(); 221 | 222 | let mut outputs = HashMap::::new(); 223 | outputs.insert( 224 | destinations_iter.next().unwrap().to_string(), 225 | Amount::from_sat(value), 226 | ); 227 | let wcfp_result = rpc.wallet_create_funded_psbt( 228 | &[CreateRawTransactionInput { 229 | txid, 230 | vout, 231 | sequence: None, 232 | }], 233 | &outputs, 234 | None, 235 | Some(WalletCreateFundedPsbtOptions { 236 | add_inputs: Some(false), 237 | subtract_fee_from_outputs: vec![0], 238 | fee_rate: Some(Amount::from_sat(fee_rate)), 239 | ..Default::default() 240 | }), 241 | None, 242 | )?; 243 | let funding_tx = self.from_walletcreatefundedpsbt_to_tx(rpc, &wcfp_result.psbt)?; 244 | leftover_coinswap_amount -= funding_tx.output[0].value; 245 | 246 | total_miner_fee += wcfp_result.fee.as_sat(); 247 | log::debug!(target: "wallet", "created funding tx, miner fee={}", wcfp_result.fee); 248 | 249 | funding_txes.push(funding_tx); 250 | payment_output_positions.push(0); 251 | } 252 | 253 | let (leftover_inputs, leftover_inputs_values): (Vec<_>, Vec<_>) = utxos 254 | .map(|(txid, vout, value)| { 255 | ( 256 | CreateRawTransactionInput { 257 | txid, 258 | vout, 259 | sequence: None, 260 | }, 261 | value, 262 | ) 263 | }) 264 | .unzip(); 265 | let mut outputs = HashMap::::new(); 266 | outputs.insert( 267 | destinations_iter.next().unwrap().to_string(), 268 | Amount::from_sat(leftover_inputs_values.iter().sum::()), 269 | ); 270 | let wcfp_result = rpc.wallet_create_funded_psbt( 271 | &leftover_inputs, 272 | &outputs, 273 | None, 274 | Some(WalletCreateFundedPsbtOptions { 275 | add_inputs: Some(false), 276 | subtract_fee_from_outputs: vec![0], 277 | fee_rate: Some(Amount::from_sat(fee_rate)), 278 | ..Default::default() 279 | }), 280 | None, 281 | )?; 282 | let funding_tx = self.from_walletcreatefundedpsbt_to_tx(rpc, &wcfp_result.psbt)?; 283 | leftover_coinswap_amount -= funding_tx.output[0].value; 284 | 285 | total_miner_fee += wcfp_result.fee.as_sat(); 286 | log::debug!(target: "wallet", "created funding tx, miner fee={}", wcfp_result.fee); 287 | 288 | funding_txes.push(funding_tx); 289 | payment_output_positions.push(0); 290 | 291 | let (first_txid, first_vout, _first_value) = first_tx_input; 292 | let mut outputs = HashMap::::new(); 293 | outputs.insert( 294 | destinations_iter.next().unwrap().to_string(), 295 | Amount::from_sat(leftover_coinswap_amount), 296 | ); 297 | let wcfp_result = rpc.wallet_create_funded_psbt( 298 | &[CreateRawTransactionInput { 299 | txid: first_txid, 300 | vout: first_vout, 301 | sequence: None, 302 | }], 303 | &outputs, 304 | None, 305 | Some(WalletCreateFundedPsbtOptions { 306 | add_inputs: Some(false), 307 | change_address: Some(change_address.clone()), 308 | fee_rate: Some(Amount::from_sat(fee_rate)), 309 | ..Default::default() 310 | }), 311 | None, 312 | )?; 313 | let funding_tx = self.from_walletcreatefundedpsbt_to_tx(rpc, &wcfp_result.psbt)?; 314 | 315 | total_miner_fee += wcfp_result.fee.as_sat(); 316 | log::debug!(target: "wallet", "created funding tx, miner fee={}", wcfp_result.fee); 317 | 318 | funding_txes.push(funding_tx); 319 | payment_output_positions.push(if wcfp_result.change_position == 0 { 320 | 1 321 | } else { 322 | 0 323 | }); 324 | 325 | Ok(Some(CreateFundingTxesResult { 326 | funding_txes, 327 | payment_output_positions, 328 | total_miner_fee, 329 | })) 330 | } 331 | 332 | fn create_funding_txes_utxo_max_sends( 333 | &self, 334 | rpc: &Client, 335 | coinswap_amount: u64, 336 | destinations: &[Address], 337 | fee_rate: u64, 338 | ) -> Result, Error> { 339 | //this function creates funding txes by 340 | //using walletcreatefundedpsbt for the total amount, and if 341 | //the number if inputs UTXOs is >number_of_txes then split those inputs into groups 342 | //across multiple transactions 343 | 344 | let mut outputs = HashMap::::new(); 345 | outputs.insert( 346 | destinations[0].to_string(), 347 | Amount::from_sat(coinswap_amount), 348 | ); 349 | let change_address = self.get_next_internal_addresses(rpc, 1)?[0].clone(); 350 | 351 | self.lock_all_nonwallet_unspents(rpc)?; 352 | let wcfp_result = rpc.wallet_create_funded_psbt( 353 | &[], 354 | &outputs, 355 | None, 356 | Some(WalletCreateFundedPsbtOptions { 357 | include_watching: Some(true), 358 | change_address: Some(change_address.clone()), 359 | fee_rate: Some(Amount::from_sat(fee_rate)), 360 | ..Default::default() 361 | }), 362 | None, 363 | )?; 364 | //TODO rust-bitcoin handles psbt, use those functions instead 365 | let decoded_psbt = rpc.call::("decodepsbt", &[Value::String(wcfp_result.psbt)])?; 366 | log::debug!(target: "wallet", "total tx decoded_psbt = {:?}", decoded_psbt); 367 | 368 | let total_tx_inputs_len = decoded_psbt["inputs"].as_array().unwrap().len(); 369 | log::debug!(target: "wallet", "total tx inputs.len = {}", total_tx_inputs_len); 370 | if total_tx_inputs_len < destinations.len() { 371 | return Err(Error::Protocol( 372 | "not enough UTXOs found, cant use this method", 373 | )); 374 | } 375 | 376 | let mut total_tx_inputs = decoded_psbt["tx"]["vin"] 377 | .as_array() 378 | .unwrap() 379 | .iter() 380 | .zip(decoded_psbt["inputs"].as_array().unwrap().iter()) 381 | .collect::>(); 382 | 383 | total_tx_inputs.sort_by(|(_, a), (_, b)| { 384 | b["witness_utxo"]["amount"] 385 | .as_f64() 386 | .unwrap() 387 | .partial_cmp(&a["witness_utxo"]["amount"].as_f64().unwrap()) 388 | .unwrap_or(std::cmp::Ordering::Equal) 389 | }); 390 | 391 | self.create_mostly_sweep_txes_with_one_tx_having_change( 392 | rpc, 393 | coinswap_amount, 394 | destinations, 395 | fee_rate, 396 | &change_address, 397 | &mut total_tx_inputs.iter().map(|(vin, input_info)| { 398 | ( 399 | Txid::from_hex(vin["txid"].as_str().unwrap()).unwrap(), 400 | vin["vout"].as_u64().unwrap() as u32, 401 | convert_json_rpc_bitcoin_to_satoshis(&input_info["witness_utxo"]["amount"]), 402 | ) 403 | }), 404 | ) 405 | } 406 | 407 | fn create_funding_txes_use_biggest_utxos( 408 | &self, 409 | rpc: &Client, 410 | coinswap_amount: u64, 411 | destinations: &[Address], 412 | fee_rate: u64, 413 | ) -> Result, Error> { 414 | //this function will pick the top most valuable UTXOs and use them 415 | //to create funding transactions 416 | 417 | let mut list_unspent_result = self.list_unspent_from_wallet(rpc, false, false)?; 418 | if list_unspent_result.len() < destinations.len() { 419 | return Err(Error::Protocol( 420 | "Not enough UTXOs to create this many funding txes", 421 | )); 422 | } 423 | list_unspent_result.sort_by(|(a, _), (b, _)| { 424 | b.amount 425 | .as_sat() 426 | .partial_cmp(&a.amount.as_sat()) 427 | .unwrap_or(std::cmp::Ordering::Equal) 428 | }); 429 | let mut list_unspent_count: Option = None; 430 | for ii in destinations.len()..list_unspent_result.len() + 1 { 431 | let sum = list_unspent_result[..ii] 432 | .iter() 433 | .map(|(l, _)| l.amount.as_sat()) 434 | .sum::(); 435 | if sum > coinswap_amount { 436 | list_unspent_count = Some(ii); 437 | break; 438 | } 439 | } 440 | if list_unspent_count.is_none() { 441 | return Err(Error::Protocol( 442 | "Not enough UTXOs/value to create funding txes", 443 | )); 444 | } 445 | 446 | let inputs = &list_unspent_result[..list_unspent_count.unwrap()]; 447 | log::debug!(target: "wallet", "inputs sizes = {:?}", 448 | inputs.iter().map(|(l, _)| l.amount.as_sat()).collect::>()); 449 | 450 | if inputs[1..] 451 | .iter() 452 | .map(|(l, _)| l.amount.as_sat()) 453 | .any(|utxo_value| utxo_value > coinswap_amount) 454 | { 455 | //at least two utxos bigger than the coinswap amount 456 | 457 | //not implemented yet! 458 | log::debug!(target: "wallet", 459 | concat!("failed to create funding txes with the biggest-utxos method, this ", 460 | "branch not implemented")); 461 | Ok(None) 462 | } else { 463 | //at most one utxo bigger than the coinswap amount 464 | 465 | let change_address = &self.get_next_internal_addresses(rpc, 1)?[0]; 466 | self.create_mostly_sweep_txes_with_one_tx_having_change( 467 | rpc, 468 | coinswap_amount, 469 | destinations, 470 | fee_rate, 471 | change_address, 472 | &mut inputs.iter().map(|(list_unspent_entry, _spend_info)| { 473 | ( 474 | list_unspent_entry.txid, 475 | list_unspent_entry.vout, 476 | list_unspent_entry.amount.as_sat(), 477 | ) 478 | }), 479 | ) 480 | } 481 | } 482 | } 483 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | const RPC_CREDENTIALS: Option<(&str, &str)> = Some(("regtestrpcuser", "regtestrpcpass")); 2 | //None; // use Bitcoin Core cookie-based authentication 3 | 4 | const RPC_WALLET: &str = "teleport"; 5 | const RPC_HOSTPORT: &str = "localhost:18443"; 6 | //default ports: mainnet=8332, testnet=18332, regtest=18443, signet=38332 7 | 8 | extern crate bitcoin; 9 | extern crate bitcoin_wallet; 10 | extern crate bitcoincore_rpc; 11 | 12 | use dirs::home_dir; 13 | use std::collections::HashMap; 14 | use std::convert::TryInto; 15 | use std::io; 16 | use std::iter::repeat; 17 | use std::path::PathBuf; 18 | use std::sync::{Arc, Once, RwLock}; 19 | 20 | use bitcoin::hashes::{hash160::Hash as Hash160, hex::ToHex}; 21 | use bitcoin::{Amount, Network}; 22 | use bitcoin_wallet::mnemonic; 23 | use bitcoincore_rpc::{Auth, Client, Error, RpcApi}; 24 | 25 | use chrono::NaiveDateTime; 26 | 27 | pub mod wallet_sync; 28 | use wallet_sync::{ 29 | DisplayAddressType, UTXOSpendInfo, Wallet, WalletSwapCoin, WalletSyncAddressAmount, 30 | }; 31 | 32 | pub mod direct_send; 33 | use direct_send::{CoinToSpend, Destination, SendAmount}; 34 | 35 | pub mod contracts; 36 | use contracts::{read_locktime_from_contract, SwapCoin}; 37 | 38 | pub mod maker_protocol; 39 | use maker_protocol::MakerBehavior; 40 | 41 | pub mod taker_protocol; 42 | use taker_protocol::TakerConfig; 43 | 44 | pub mod offerbook_sync; 45 | use offerbook_sync::{get_advertised_maker_addresses, sync_offerbook_with_addresses, MakerAddress}; 46 | 47 | pub mod fidelity_bonds; 48 | use fidelity_bonds::{get_locktime_from_index, YearAndMonth}; 49 | 50 | pub mod directory_servers; 51 | pub mod error; 52 | pub mod funding_tx; 53 | pub mod messages; 54 | pub mod watchtower_client; 55 | pub mod watchtower_protocol; 56 | 57 | static INIT: Once = Once::new(); 58 | 59 | fn str_to_bitcoin_network(net_str: &str) -> Network { 60 | match net_str { 61 | "main" => Network::Bitcoin, 62 | "test" => Network::Testnet, 63 | "signet" => Network::Signet, 64 | "regtest" => Network::Regtest, 65 | _ => panic!("unknown network: {}", net_str), 66 | } 67 | } 68 | 69 | pub fn get_bitcoin_rpc() -> Result<(Client, Network), Error> { 70 | let auth = match RPC_CREDENTIALS { 71 | Some((user, pass)) => Auth::UserPass(user.to_string(), pass.to_string()), 72 | None => { 73 | //TODO this currently only works for Linux and regtest, 74 | // also support other OSes (Windows, MacOS...) and networks 75 | let data_dir = home_dir().unwrap().join(".bitcoin"); 76 | Auth::CookieFile(data_dir.join("regtest").join(".cookie")) 77 | } 78 | }; 79 | let rpc = Client::new( 80 | format!("http://{}/wallet/{}", RPC_HOSTPORT, RPC_WALLET), 81 | auth, 82 | )?; 83 | let network = str_to_bitcoin_network(rpc.get_blockchain_info()?.chain.as_str()); 84 | Ok((rpc, network)) 85 | } 86 | 87 | /// Setup function that will only run once, even if called multiple times. 88 | pub fn setup_logger() { 89 | INIT.call_once(|| { 90 | env_logger::Builder::from_env( 91 | env_logger::Env::default() 92 | .default_filter_or("teleport=info,main=info,wallet=info") 93 | .default_write_style_or("always"), 94 | ) 95 | .init(); 96 | }); 97 | } 98 | 99 | pub fn generate_wallet(wallet_file_name: &PathBuf) -> std::io::Result<()> { 100 | let (rpc, network) = match get_bitcoin_rpc() { 101 | Ok(rpc) => rpc, 102 | Err(error) => { 103 | log::error!(target: "main", "error connecting to bitcoin node: {:?}", error); 104 | return Ok(()); 105 | } 106 | }; 107 | println!("input seed phrase extension (or leave blank for none): "); 108 | let mut extension = String::new(); 109 | io::stdin().read_line(&mut extension)?; 110 | extension = extension.trim().to_string(); 111 | let mnemonic = 112 | mnemonic::Mnemonic::new_random(bitcoin_wallet::account::MasterKeyEntropy::Sufficient) 113 | .unwrap(); 114 | 115 | Wallet::save_new_wallet_file(&wallet_file_name, mnemonic.to_string(), extension.clone()) 116 | .unwrap(); 117 | 118 | let w = match Wallet::load_wallet_from_file( 119 | &wallet_file_name, 120 | network, 121 | WalletSyncAddressAmount::Normal, 122 | ) { 123 | Ok(w) => w, 124 | Err(error) => panic!("error loading wallet file: {:?}", error), 125 | }; 126 | 127 | println!("Importing addresses into Core. . ."); 128 | if let Err(e) = w.import_initial_addresses( 129 | &rpc, 130 | &w.get_hd_wallet_descriptors(&rpc) 131 | .unwrap() 132 | .iter() 133 | .collect::>(), 134 | &Vec::<_>::new(), 135 | &Vec::<_>::new(), 136 | ) { 137 | w.delete_wallet_file().unwrap(); 138 | panic!("error importing addresses: {:?}", e); 139 | } 140 | 141 | println!("Write down this seed phrase =\n{}", mnemonic.to_string()); 142 | if !extension.trim().is_empty() { 143 | println!("And this extension =\n\"{}\"", extension); 144 | } 145 | println!( 146 | "\nThis seed phrase is NOT enough to backup all coins in your wallet\n\ 147 | The teleport wallet file is needed to backup swapcoins" 148 | ); 149 | println!("\nSaved to file `{}`", wallet_file_name.to_string_lossy()); 150 | 151 | Ok(()) 152 | } 153 | 154 | pub fn recover_wallet(wallet_file_name: &PathBuf) -> std::io::Result<()> { 155 | println!("input seed phrase: "); 156 | let mut seed_phrase = String::new(); 157 | io::stdin().read_line(&mut seed_phrase)?; 158 | seed_phrase = seed_phrase.trim().to_string(); 159 | 160 | if let Err(e) = mnemonic::Mnemonic::from_str(&seed_phrase) { 161 | println!("invalid seed phrase: {:?}", e); 162 | return Ok(()); 163 | } 164 | 165 | println!("input seed phrase extension (or leave blank for none): "); 166 | let mut extension = String::new(); 167 | io::stdin().read_line(&mut extension)?; 168 | extension = extension.trim().to_string(); 169 | 170 | Wallet::save_new_wallet_file(&wallet_file_name, seed_phrase, extension).unwrap(); 171 | println!("\nSaved to file `{}`", wallet_file_name.to_string_lossy()); 172 | Ok(()) 173 | } 174 | 175 | pub fn display_wallet_balance(wallet_file_name: &PathBuf, long_form: Option) { 176 | let (rpc, network) = match get_bitcoin_rpc() { 177 | Ok(rpc) => rpc, 178 | Err(error) => { 179 | log::error!(target: "main", "error connecting to bitcoin node: {:?}", error); 180 | return; 181 | } 182 | }; 183 | let mut wallet = match Wallet::load_wallet_from_file( 184 | wallet_file_name, 185 | network, 186 | WalletSyncAddressAmount::Normal, 187 | ) { 188 | Ok(w) => w, 189 | Err(error) => { 190 | log::error!(target: "main", "error loading wallet file: {:?}", error); 191 | return; 192 | } 193 | }; 194 | wallet.startup_sync(&rpc).unwrap(); 195 | 196 | let long_form = long_form.unwrap_or(false); 197 | 198 | let utxos_incl_fbonds = wallet.list_unspent_from_wallet(&rpc, false, true).unwrap(); 199 | let (mut utxos, mut fidelity_bond_utxos): (Vec<_>, Vec<_>) = 200 | utxos_incl_fbonds.iter().partition(|(_, usi)| { 201 | if let UTXOSpendInfo::FidelityBondCoin { 202 | index: _, 203 | input_value: _, 204 | } = usi 205 | { 206 | false 207 | } else { 208 | true 209 | } 210 | }); 211 | utxos.sort_by(|(a, _), (b, _)| b.confirmations.cmp(&a.confirmations)); 212 | let utxo_count = utxos.len(); 213 | let balance: Amount = utxos 214 | .iter() 215 | .fold(Amount::ZERO, |acc, (u, _)| acc + u.amount); 216 | println!("= spendable wallet balance ="); 217 | println!( 218 | "{:16} {:24} {:^8} {:<7} value", 219 | "coin", "address", "type", "conf", 220 | ); 221 | for (utxo, _) in utxos { 222 | let txid = utxo.txid.to_hex(); 223 | let addr = utxo.address.as_ref().unwrap().to_string(); 224 | #[rustfmt::skip] 225 | println!( 226 | "{}{}{}:{} {}{}{} {:^8} {:<7} {}", 227 | if long_form { &txid } else {&txid[0..6] }, 228 | if long_form { "" } else { ".." }, 229 | if long_form { &"" } else { &txid[58..64] }, 230 | utxo.vout, 231 | if long_form { &addr } else { &addr[0..10] }, 232 | if long_form { "" } else { "...." }, 233 | if long_form { &"" } else { &addr[addr.len() - 10..addr.len()] }, 234 | if utxo.witness_script.is_some() { 235 | "swapcoin" 236 | } else { 237 | if utxo.descriptor.is_some() { "seed" } else { "timelock" } 238 | }, 239 | utxo.confirmations, 240 | utxo.amount 241 | ); 242 | } 243 | println!("coin count = {}", utxo_count); 244 | println!("total balance = {}", balance); 245 | 246 | let incomplete_coinswaps = wallet.find_incomplete_coinswaps(&rpc).unwrap(); 247 | if !incomplete_coinswaps.is_empty() { 248 | println!("= incomplete coinswaps ="); 249 | for (hashvalue, (utxo_incoming_swapcoins, utxo_outgoing_swapcoins)) in incomplete_coinswaps 250 | { 251 | let incoming_swapcoins_balance: Amount = utxo_incoming_swapcoins 252 | .iter() 253 | .fold(Amount::ZERO, |acc, us| acc + us.0.amount); 254 | let outgoing_swapcoins_balance: Amount = utxo_outgoing_swapcoins 255 | .iter() 256 | .fold(Amount::ZERO, |acc, us| acc + us.0.amount); 257 | 258 | println!( 259 | "{:16} {:8} {:8} {:<15} {:<7} value", 260 | "coin", "type", "preimage", "locktime/blocks", "conf", 261 | ); 262 | for ((utxo, swapcoin), contract_type) in utxo_incoming_swapcoins 263 | .iter() 264 | .map(|(l, i)| (l, (*i as &dyn SwapCoin))) 265 | .zip(repeat("hashlock")) 266 | .chain( 267 | utxo_outgoing_swapcoins 268 | .iter() 269 | .map(|(l, o)| (l, (*o as &dyn SwapCoin))) 270 | .zip(repeat("timelock")), 271 | ) 272 | { 273 | let txid = utxo.txid.to_hex(); 274 | 275 | #[rustfmt::skip] 276 | println!("{}{}{}:{} {:8} {:8} {:^15} {:<7} {}", 277 | if long_form { &txid } else {&txid[0..6] }, 278 | if long_form { "" } else { ".." }, 279 | if long_form { &"" } else { &txid[58..64] }, 280 | utxo.vout, 281 | contract_type, 282 | if swapcoin.is_hash_preimage_known() { "known" } else { "unknown" }, 283 | read_locktime_from_contract(&swapcoin.get_contract_redeemscript()) 284 | .expect("unable to read locktime from contract"), 285 | utxo.confirmations, 286 | utxo.amount 287 | ); 288 | } 289 | if incoming_swapcoins_balance != Amount::ZERO { 290 | println!( 291 | "amount earned if coinswap successful = {}", 292 | (incoming_swapcoins_balance.to_signed().unwrap() 293 | - outgoing_swapcoins_balance.to_signed().unwrap()), 294 | ); 295 | } 296 | println!( 297 | "outgoing balance = {}\nhashvalue = {}", 298 | outgoing_swapcoins_balance, 299 | &hashvalue.to_hex()[..] 300 | ); 301 | } 302 | } 303 | 304 | let (mut incoming_contract_utxos, mut outgoing_contract_utxos) = 305 | wallet.find_live_contract_unspents(&rpc).unwrap(); 306 | if !outgoing_contract_utxos.is_empty() { 307 | outgoing_contract_utxos.sort_by(|a, b| b.1.confirmations.cmp(&a.1.confirmations)); 308 | println!("= live timelocked contracts ="); 309 | println!( 310 | "{:16} {:10} {:8} {:<7} {:<8} {:6}", 311 | "coin", "hashvalue", "timelock", "conf", "locked?", "value" 312 | ); 313 | for (outgoing_swapcoin, utxo) in outgoing_contract_utxos { 314 | let txid = utxo.txid.to_hex(); 315 | let timelock = 316 | read_locktime_from_contract(&outgoing_swapcoin.contract_redeemscript).unwrap(); 317 | let hashvalue = outgoing_swapcoin.get_hashvalue().to_hex(); 318 | #[rustfmt::skip] 319 | println!("{}{}{}:{} {}{} {:<8} {:<7} {:<8} {}", 320 | if long_form { &txid } else {&txid[0..6] }, 321 | if long_form { "" } else { ".." }, 322 | if long_form { &"" } else { &txid[58..64] }, 323 | utxo.vout, 324 | if long_form { &hashvalue } else { &hashvalue[..8] }, 325 | if long_form { "" } else { ".." }, 326 | timelock, 327 | utxo.confirmations, 328 | if utxo.confirmations >= timelock.into() { "unlocked" } else { "locked" }, 329 | utxo.amount 330 | ); 331 | } 332 | } 333 | 334 | //ordinary users shouldnt be spending via the hashlock branch 335 | //maybe makers since they're a bit more expertly, and they dont start with the hash preimage 336 | //but takers should basically never use the hash preimage 337 | let expert_mode = true; 338 | if expert_mode && !incoming_contract_utxos.is_empty() { 339 | incoming_contract_utxos.sort_by(|a, b| b.1.confirmations.cmp(&a.1.confirmations)); 340 | println!("= live hashlocked contracts ="); 341 | println!( 342 | "{:16} {:10} {:8} {:<7} {:8} {:6}", 343 | "coin", "hashvalue", "timelock", "conf", "preimage", "value" 344 | ); 345 | for (incoming_swapcoin, utxo) in incoming_contract_utxos { 346 | let txid = utxo.txid.to_hex(); 347 | let timelock = 348 | read_locktime_from_contract(&incoming_swapcoin.contract_redeemscript).unwrap(); 349 | let hashvalue = incoming_swapcoin.get_hashvalue().to_hex(); 350 | #[rustfmt::skip] 351 | println!("{}{}{}:{} {}{} {:<8} {:<7} {:8} {}", 352 | if long_form { &txid } else {&txid[0..6] }, 353 | if long_form { "" } else { ".." }, 354 | if long_form { &"" } else { &txid[58..64] }, 355 | utxo.vout, 356 | if long_form { &hashvalue } else { &hashvalue[..8] }, 357 | if long_form { "" } else { ".." }, 358 | timelock, 359 | utxo.confirmations, 360 | if incoming_swapcoin.is_hash_preimage_known() { "known" } else { "unknown" }, 361 | utxo.amount 362 | ); 363 | } 364 | } 365 | 366 | if fidelity_bond_utxos.len() > 0 { 367 | println!("= fidelity bond coins ="); 368 | println!( 369 | "{:16} {:24} {:<7} {:<11} {:<8} {:6}", 370 | "coin", "address", "conf", "locktime", "locked?", "value" 371 | ); 372 | 373 | let mediantime = rpc.get_blockchain_info().unwrap().median_time; 374 | fidelity_bond_utxos.sort_by(|(a, _), (b, _)| b.confirmations.cmp(&a.confirmations)); 375 | for (utxo, utxo_spend_info) in fidelity_bond_utxos { 376 | let index = if let UTXOSpendInfo::FidelityBondCoin { 377 | index, 378 | input_value: _, 379 | } = utxo_spend_info 380 | { 381 | index 382 | } else { 383 | panic!("logic error, all these utxos should be fidelity bonds"); 384 | }; 385 | let unix_locktime = get_locktime_from_index(*index); 386 | let txid = utxo.txid.to_hex(); 387 | let addr = utxo.address.as_ref().unwrap().to_string(); 388 | #[rustfmt::skip] 389 | println!( 390 | "{}{}{}:{} {}{}{} {:<7} {:<11} {:<8} {:6}", 391 | if long_form { &txid } else {&txid[0..6] }, 392 | if long_form { "" } else { ".." }, 393 | if long_form { &"" } else { &txid[58..64] }, 394 | utxo.vout, 395 | if long_form { &addr } else { &addr[0..10] }, 396 | if long_form { "" } else { "...." }, 397 | if long_form { &"" } else { &addr[addr.len() - 10..addr.len()] }, 398 | utxo.confirmations, 399 | NaiveDateTime::from_timestamp(unix_locktime, 0) 400 | .format("%Y-%m-%d") 401 | .to_string(), 402 | if mediantime >= unix_locktime.try_into().unwrap() { "unlocked" } else { "locked" }, 403 | utxo.amount 404 | ); 405 | } 406 | } 407 | } 408 | 409 | pub fn display_wallet_addresses( 410 | wallet_file_name: &PathBuf, 411 | types: DisplayAddressType, 412 | network: Option, 413 | ) { 414 | let network = match get_bitcoin_rpc() { 415 | Ok((_rpc, network)) => network, 416 | Err(error) => { 417 | if let Some(net_str) = network { 418 | str_to_bitcoin_network(net_str.as_str()) 419 | } else { 420 | panic!( 421 | "network string not provided, and error connecting to bitcoin node: {:?}", 422 | error 423 | ); 424 | } 425 | } 426 | }; 427 | let wallet = match Wallet::load_wallet_from_file( 428 | wallet_file_name, 429 | network, 430 | WalletSyncAddressAmount::Normal, 431 | ) { 432 | Ok(w) => w, 433 | Err(error) => { 434 | log::error!(target: "main", "error loading wallet file: {:?}", error); 435 | return; 436 | } 437 | }; 438 | wallet.display_addresses(types); 439 | } 440 | 441 | pub fn print_receive_invoice(wallet_file_name: &PathBuf) { 442 | let (rpc, network) = match get_bitcoin_rpc() { 443 | Ok(rpc) => rpc, 444 | Err(error) => { 445 | log::error!(target: "main", "error connecting to bitcoin node: {:?}", error); 446 | return; 447 | } 448 | }; 449 | let mut wallet = match Wallet::load_wallet_from_file( 450 | wallet_file_name, 451 | network, 452 | WalletSyncAddressAmount::Normal, 453 | ) { 454 | Ok(w) => w, 455 | Err(error) => { 456 | log::error!(target: "main", "error loading wallet file: {:?}", error); 457 | return; 458 | } 459 | }; 460 | wallet.startup_sync(&rpc).unwrap(); 461 | 462 | let addr = match wallet.get_next_external_address(&rpc) { 463 | Ok(a) => a, 464 | Err(error) => { 465 | println!("error: {:?}", error); 466 | return; 467 | } 468 | }; 469 | println!("{}", addr); 470 | } 471 | 472 | pub fn print_fidelity_bond_address(wallet_file_name: &PathBuf, locktime: &YearAndMonth) { 473 | let (rpc, network) = match get_bitcoin_rpc() { 474 | Ok(rpc) => rpc, 475 | Err(error) => { 476 | log::error!(target: "main", "error connecting to bitcoin node: {:?}", error); 477 | return; 478 | } 479 | }; 480 | let mut wallet = match Wallet::load_wallet_from_file( 481 | wallet_file_name, 482 | network, 483 | WalletSyncAddressAmount::Normal, 484 | ) { 485 | Ok(w) => w, 486 | Err(error) => { 487 | log::error!(target: "main", "error loading wallet file: {:?}", error); 488 | return; 489 | } 490 | }; 491 | wallet.startup_sync(&rpc).unwrap(); 492 | 493 | let (addr, unix_locktime) = wallet.get_timelocked_address(locktime); 494 | println!(concat!( 495 | "WARNING: You should send coins to this address only once.", 496 | " Only single biggest value UTXO will be announced as a fidelity bond.", 497 | " Sending coins to this address multiple times will not increase", 498 | " fidelity bond value." 499 | )); 500 | println!(concat!( 501 | "WARNING: Only send coins here which are from coinjoins, coinswaps or", 502 | " otherwise not linked to your identity. Also, use a sweep transaction when funding the", 503 | " timelocked address, i.e. Don't create a change address." 504 | )); 505 | println!( 506 | "Coins sent to this address will not be spendable until {}", 507 | NaiveDateTime::from_timestamp(unix_locktime, 0) 508 | .format("%Y-%m-%d") 509 | .to_string() 510 | ); 511 | println!("{}", addr); 512 | } 513 | 514 | pub fn run_maker( 515 | wallet_file_name: &PathBuf, 516 | sync_amount: WalletSyncAddressAmount, 517 | port: u16, 518 | maker_behavior: MakerBehavior, 519 | kill_flag: Option>>, 520 | ) { 521 | let (rpc, network) = match get_bitcoin_rpc() { 522 | Ok(rpc) => rpc, 523 | Err(error) => { 524 | log::error!(target: "main", "error connecting to bitcoin node: {:?}", error); 525 | return; 526 | } 527 | }; 528 | let mut wallet = match Wallet::load_wallet_from_file(wallet_file_name, network, sync_amount) { 529 | Ok(w) => w, 530 | Err(error) => { 531 | log::error!(target: "main", "error loading wallet file: {:?}", error); 532 | return; 533 | } 534 | }; 535 | wallet.startup_sync(&rpc).unwrap(); 536 | 537 | let rpc_ptr = Arc::new(rpc); 538 | let wallet_ptr = Arc::new(RwLock::new(wallet)); 539 | let config = maker_protocol::MakerConfig { 540 | port, 541 | rpc_ping_interval_secs: 60, 542 | watchtower_ping_interval_secs: 300, 543 | directory_servers_refresh_interval_secs: 60 * 60 * 12, //12 hours 544 | maker_behavior, 545 | kill_flag: if kill_flag.is_none() { 546 | Arc::new(RwLock::new(false)) 547 | } else { 548 | kill_flag.unwrap().clone() 549 | }, 550 | idle_connection_timeout: 300, 551 | }; 552 | maker_protocol::start_maker(rpc_ptr, wallet_ptr, config); 553 | } 554 | 555 | pub fn run_taker( 556 | wallet_file_name: &PathBuf, 557 | sync_amount: WalletSyncAddressAmount, 558 | fee_rate: u64, 559 | send_amount: u64, 560 | maker_count: u16, 561 | tx_count: u32, 562 | ) { 563 | let (rpc, network) = match get_bitcoin_rpc() { 564 | Ok(rpc) => rpc, 565 | Err(error) => { 566 | log::error!(target: "main", "error connecting to bitcoin node: {:?}", error); 567 | return; 568 | } 569 | }; 570 | let mut wallet = match Wallet::load_wallet_from_file(wallet_file_name, network, sync_amount) { 571 | Ok(w) => w, 572 | Err(error) => { 573 | log::error!(target: "main", "error loading wallet file: {:?}", error); 574 | return; 575 | } 576 | }; 577 | wallet.startup_sync(&rpc).unwrap(); 578 | taker_protocol::start_taker( 579 | &rpc, 580 | &mut wallet, 581 | TakerConfig { 582 | send_amount, 583 | maker_count, 584 | tx_count, 585 | required_confirms: 1, 586 | fee_rate, 587 | }, 588 | ); 589 | } 590 | 591 | pub fn recover_from_incomplete_coinswap( 592 | wallet_file_name: &PathBuf, 593 | hashvalue: Hash160, 594 | dont_broadcast: bool, 595 | ) { 596 | let (rpc, network) = match get_bitcoin_rpc() { 597 | Ok(rpc) => rpc, 598 | Err(error) => { 599 | log::error!(target: "main", "error connecting to bitcoin node: {:?}", error); 600 | return; 601 | } 602 | }; 603 | let mut wallet = match Wallet::load_wallet_from_file( 604 | wallet_file_name, 605 | network, 606 | WalletSyncAddressAmount::Normal, 607 | ) { 608 | Ok(w) => w, 609 | Err(error) => { 610 | log::error!(target: "main", "error loading wallet file: {:?}", error); 611 | return; 612 | } 613 | }; 614 | wallet.startup_sync(&rpc).unwrap(); 615 | 616 | let incomplete_coinswaps = wallet.find_incomplete_coinswaps(&rpc).unwrap(); 617 | let incomplete_coinswap = incomplete_coinswaps.get(&hashvalue); 618 | if incomplete_coinswap.is_none() { 619 | log::error!(target: "main", "hashvalue not refering to incomplete coinswap, run \ 620 | `wallet-balance` to see list of incomplete coinswaps"); 621 | return; 622 | } 623 | let incomplete_coinswap = incomplete_coinswap.unwrap(); 624 | for (ii, swapcoin) in incomplete_coinswap 625 | .0 626 | .iter() 627 | .map(|(l, i)| (l, (*i as &dyn WalletSwapCoin))) 628 | .chain( 629 | incomplete_coinswap 630 | .1 631 | .iter() 632 | .map(|(l, o)| (l, (*o as &dyn WalletSwapCoin))), 633 | ) 634 | .enumerate() 635 | { 636 | wallet 637 | .import_wallet_contract_redeemscript(&rpc, &swapcoin.1.get_contract_redeemscript()) 638 | .unwrap(); 639 | 640 | let signed_contract_tx = swapcoin.1.get_fully_signed_contract_tx(); 641 | if dont_broadcast { 642 | let txhex = bitcoin::consensus::encode::serialize_hex(&signed_contract_tx); 643 | println!( 644 | "contract_tx_{} (txid = {}) = \n{}", 645 | ii, 646 | signed_contract_tx.txid(), 647 | txhex 648 | ); 649 | let accepted = rpc 650 | .test_mempool_accept(&[txhex.clone()]) 651 | .unwrap() 652 | .iter() 653 | .any(|tma| tma.allowed); 654 | assert!(accepted); 655 | } else { 656 | let txid = rpc.send_raw_transaction(&signed_contract_tx).unwrap(); 657 | println!("broadcasted {}", txid); 658 | } 659 | } 660 | } 661 | 662 | #[tokio::main] 663 | pub async fn download_and_display_offers( 664 | network_str: Option, 665 | maker_address: Option, 666 | ) { 667 | let maker_addresses = if let Some(maker_addr) = maker_address { 668 | vec![MakerAddress::Tor { 669 | address: maker_addr, 670 | }] 671 | } else { 672 | let network = match get_bitcoin_rpc() { 673 | Ok((_rpc, network)) => network, 674 | Err(error) => { 675 | if let Some(net_str) = network_str { 676 | str_to_bitcoin_network(net_str.as_str()) 677 | } else { 678 | panic!( 679 | "network string not provided, and error connecting to bitcoin node: {:?}", 680 | error 681 | ); 682 | } 683 | } 684 | }; 685 | get_advertised_maker_addresses(network) 686 | .await 687 | .expect("unable to sync maker addresses from directory servers") 688 | }; 689 | let offers_addresses = sync_offerbook_with_addresses(maker_addresses.clone()).await; 690 | let mut addresses_offers_map = HashMap::new(); 691 | for offer_address in offers_addresses.iter() { 692 | let address_str = match &offer_address.address { 693 | MakerAddress::Clearnet { address } => address, 694 | MakerAddress::Tor { address } => address, 695 | }; 696 | addresses_offers_map.insert(address_str, offer_address); 697 | } 698 | 699 | println!( 700 | "{:<3} {:<70} {:<12} {:<12} {:<12} {:<12} {:<12} {:<12} {:<19}", 701 | "n", 702 | "maker address", 703 | "max size", 704 | "min size", 705 | "abs fee", 706 | "amt rel fee", 707 | "time rel fee", 708 | "minlocktime", 709 | "fidelity bond value", 710 | ); 711 | 712 | for (ii, address) in maker_addresses.iter().enumerate() { 713 | let address_str = match &address { 714 | MakerAddress::Clearnet { address } => address, 715 | MakerAddress::Tor { address } => address, 716 | }; 717 | if let Some(offer_address) = addresses_offers_map.get(&address_str) { 718 | let o = &offer_address.offer; 719 | 720 | println!( 721 | "{:<3} {:<70} {:<12} {:<12} {:<12} {:<12} {:<12} {:<12}", 722 | ii, 723 | address_str, 724 | o.max_size, 725 | o.min_size, 726 | o.absolute_fee_sat, 727 | o.amount_relative_fee_ppb, 728 | o.time_relative_fee_ppb, 729 | o.minimum_locktime, 730 | ); 731 | } else { 732 | println!("{:<3} {:<70} UNREACHABLE", ii, address_str); 733 | } 734 | } 735 | } 736 | 737 | pub fn direct_send( 738 | wallet_file_name: &PathBuf, 739 | fee_rate: u64, 740 | send_amount: SendAmount, 741 | destination: Destination, 742 | coins_to_spend: &[CoinToSpend], 743 | dont_broadcast: bool, 744 | ) { 745 | let (rpc, network) = match get_bitcoin_rpc() { 746 | Ok(rpc) => rpc, 747 | Err(error) => { 748 | log::error!(target: "main", "error connecting to bitcoin node: {:?}", error); 749 | return; 750 | } 751 | }; 752 | let mut wallet = match Wallet::load_wallet_from_file( 753 | wallet_file_name, 754 | network, 755 | WalletSyncAddressAmount::Normal, 756 | ) { 757 | Ok(w) => w, 758 | Err(error) => { 759 | log::error!(target: "main", "error loading wallet file: {:?}", error); 760 | return; 761 | } 762 | }; 763 | wallet.startup_sync(&rpc).unwrap(); 764 | let tx = wallet 765 | .create_direct_send(&rpc, fee_rate, send_amount, destination, coins_to_spend) 766 | .unwrap(); 767 | let txhex = bitcoin::consensus::encode::serialize_hex(&tx); 768 | log::debug!("fully signed tx hex = {}", txhex); 769 | let test_mempool_accept_result = &rpc.test_mempool_accept(&[txhex.clone()]).unwrap()[0]; 770 | if !test_mempool_accept_result.allowed { 771 | panic!( 772 | "created invalid transaction, reason = {:#?}", 773 | test_mempool_accept_result 774 | ); 775 | } 776 | println!( 777 | "actual fee rate = {:.3} sat/vb", 778 | test_mempool_accept_result 779 | .fees 780 | .as_ref() 781 | .unwrap() 782 | .base 783 | .as_sat() as f64 784 | / test_mempool_accept_result.vsize.unwrap() as f64 785 | ); 786 | if dont_broadcast { 787 | println!("tx = \n{}", txhex); 788 | } else { 789 | let txid = rpc.send_raw_transaction(&tx).unwrap(); 790 | println!("broadcasted {}", txid); 791 | } 792 | } 793 | 794 | pub fn run_watchtower(data_file_path: &PathBuf, kill_flag: Option>>) { 795 | let (rpc, network) = match get_bitcoin_rpc() { 796 | Ok(rpc) => rpc, 797 | Err(error) => { 798 | log::error!(target: "main", "error connecting to bitcoin node: {:?}", error); 799 | return; 800 | } 801 | }; 802 | 803 | watchtower_protocol::start_watchtower( 804 | &rpc, 805 | data_file_path, 806 | network, 807 | if kill_flag.is_none() { 808 | Arc::new(RwLock::new(false)) 809 | } else { 810 | kill_flag.unwrap().clone() 811 | }, 812 | ); 813 | } 814 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use bitcoin::consensus::encode::deserialize; 2 | use bitcoin::hashes::{hash160::Hash as Hash160, hex::FromHex}; 3 | use bitcoin::{Script, Transaction}; 4 | 5 | use std::path::{Path, PathBuf}; 6 | use structopt::StructOpt; 7 | 8 | use teleport; 9 | use teleport::direct_send::{CoinToSpend, Destination, SendAmount}; 10 | use teleport::fidelity_bonds::YearAndMonth; 11 | use teleport::maker_protocol::MakerBehavior; 12 | use teleport::wallet_sync::{DisplayAddressType, WalletSyncAddressAmount}; 13 | use teleport::watchtower_protocol::{ContractTransaction, ContractsInfo}; 14 | 15 | #[derive(Debug, StructOpt)] 16 | #[structopt(name = "teleport", about = "A tool for CoinSwap")] 17 | struct ArgsWithWalletFile { 18 | /// Wallet file 19 | #[structopt(default_value = "wallet.teleport", parse(from_os_str), long)] 20 | wallet_file_name: PathBuf, 21 | 22 | /// Dont broadcast transactions, only output their transaction hex string 23 | /// Only for commands which involve sending transactions e.g. recover-from-incomplete-coinswap 24 | #[structopt(short, long)] 25 | dont_broadcast: bool, 26 | 27 | /// Miner fee rate, in satoshis per thousand vbytes, i.e. 1000 = 1 sat/vb 28 | #[structopt(default_value = "1000", short = "f", long)] 29 | fee_rate: u64, 30 | 31 | /// Subcommand 32 | #[structopt(flatten)] 33 | subcommand: Subcommand, 34 | } 35 | 36 | #[derive(Debug, StructOpt)] 37 | #[structopt(name = "teleport", about = "A tool for CoinSwap")] 38 | enum Subcommand { 39 | /// Generates a new seed phrase and wallet file 40 | GenerateWallet, 41 | 42 | /// Recovers a wallet file from an existing seed phrase 43 | RecoverWallet, 44 | 45 | /// Prints current wallet balance. 46 | WalletBalance { 47 | /// Whether to print entire TXIDs and addresses 48 | long_form: Option, 49 | }, 50 | 51 | /// Dumps all addresses in wallet file, only useful for debugging 52 | DisplayWalletAddresses { 53 | /// Address types: "all", "masterkey", "seed", "incomingswap", "outgoingswap", 54 | /// "swap", "incomingcontract", "outgoingcontract", "contract", "fidelitybond". 55 | /// Default is "all" 56 | types: Option, 57 | /// Network in question, options are "main", "test", "signet", "regtest". Only used 58 | /// if configured bitcoin node RPC is unreachable 59 | network: Option, 60 | }, 61 | 62 | /// Prints receive invoice. 63 | GetReceiveInvoice, 64 | 65 | /// Runs yield generator aiming to produce an income 66 | RunYieldGenerator { 67 | /// Port to listen on, default is 6102 68 | port: Option, 69 | /// Special behavior used for testing e.g. "closeonsignsenderscontracttx" 70 | special_behavior: Option, 71 | }, 72 | 73 | /// Prints a fidelity bond timelocked address 74 | GetFidelityBondAddress { 75 | /// Locktime value of timelocked address as yyyy-mm year and month, for example "2025-03" 76 | year_and_month: YearAndMonth, 77 | }, 78 | 79 | /// Runs Taker. 80 | DoCoinswap { 81 | /// Amount to send (in sats) 82 | send_amount: u64, //TODO convert this to SendAmount 83 | /// How many makers to route through, default 2 84 | maker_count: Option, 85 | /// How many transactions per hop, default 3 86 | tx_count: Option, 87 | }, 88 | 89 | /// Broadcast contract transactions for incomplete coinswap. Locked up bitcoins are 90 | /// returned to your wallet after the timeout 91 | RecoverFromIncompleteCoinswap { 92 | /// Hashvalue as hex string which uniquely identifies the coinswap 93 | hashvalue: Hash160, 94 | }, 95 | 96 | /// Download all offers from all makers out there. If bitcoin node not configured then 97 | /// provide the network as an argument, can also optionally download from one given maker 98 | DownloadOffers { 99 | /// Network in question, options are "main", "test", "signet". Only used if configured 100 | /// bitcoin node RPC is unreachable 101 | network: Option, 102 | /// Optional single maker address to only download from. Useful if testing if your own 103 | /// maker is reachable 104 | maker_address: Option, 105 | }, 106 | 107 | /// Send a transaction from the wallet 108 | DirectSend { 109 | /// Amount to send (in sats), or "max" for fully-spending with no change 110 | send_amount: SendAmount, 111 | /// Address to send coins to, or "wallet" to send back to own wallet 112 | destination: Destination, 113 | /// Coins to spend as inputs, either in long form ":vout" or short 114 | /// form "txid-prefix..txid-suffix:vout" 115 | coins_to_spend: Vec, 116 | }, 117 | 118 | /// Run watchtower 119 | RunWatchtower { 120 | /// File path used for the watchtower data file, default "watchtower.dat" 121 | data_file_path: Option, 122 | }, 123 | 124 | /// Test watchtower client 125 | TestWatchtowerClient { 126 | contract_transactions_hex: Vec, 127 | }, 128 | } 129 | 130 | fn main() -> Result<(), Box> { 131 | teleport::setup_logger(); 132 | let args = ArgsWithWalletFile::from_args(); 133 | 134 | match args.subcommand { 135 | Subcommand::GenerateWallet => { 136 | teleport::generate_wallet(&args.wallet_file_name)?; 137 | } 138 | Subcommand::RecoverWallet => { 139 | teleport::recover_wallet(&args.wallet_file_name)?; 140 | } 141 | Subcommand::WalletBalance { long_form } => { 142 | teleport::display_wallet_balance(&args.wallet_file_name, long_form); 143 | } 144 | Subcommand::DisplayWalletAddresses { types, network } => { 145 | teleport::display_wallet_addresses( 146 | &args.wallet_file_name, 147 | types.unwrap_or(DisplayAddressType::All), 148 | network, 149 | ); 150 | } 151 | Subcommand::GetReceiveInvoice => { 152 | teleport::print_receive_invoice(&args.wallet_file_name); 153 | } 154 | Subcommand::RunYieldGenerator { 155 | port, 156 | special_behavior, 157 | } => { 158 | let maker_special_behavior = match special_behavior.unwrap_or(String::new()).as_str() { 159 | "closeonsignsenderscontracttx" => MakerBehavior::CloseOnSignSendersContractTx, 160 | _ => MakerBehavior::Normal, 161 | }; 162 | teleport::run_maker( 163 | &args.wallet_file_name, 164 | WalletSyncAddressAmount::Normal, 165 | port.unwrap_or(6102), 166 | maker_special_behavior, 167 | None, 168 | ); 169 | } 170 | Subcommand::GetFidelityBondAddress { year_and_month } => { 171 | teleport::print_fidelity_bond_address(&args.wallet_file_name, &year_and_month); 172 | } 173 | Subcommand::DoCoinswap { 174 | send_amount, 175 | maker_count, 176 | tx_count, 177 | } => { 178 | teleport::run_taker( 179 | &args.wallet_file_name, 180 | WalletSyncAddressAmount::Normal, 181 | args.fee_rate, 182 | send_amount, 183 | maker_count.unwrap_or(2), 184 | tx_count.unwrap_or(3), 185 | ); 186 | } 187 | Subcommand::RecoverFromIncompleteCoinswap { hashvalue } => { 188 | teleport::recover_from_incomplete_coinswap( 189 | &args.wallet_file_name, 190 | hashvalue, 191 | args.dont_broadcast, 192 | ); 193 | } 194 | Subcommand::DownloadOffers { 195 | network, 196 | maker_address, 197 | } => { 198 | teleport::download_and_display_offers(network, maker_address); 199 | } 200 | Subcommand::DirectSend { 201 | send_amount, 202 | destination, 203 | coins_to_spend, 204 | } => { 205 | teleport::direct_send( 206 | &args.wallet_file_name, 207 | args.fee_rate, 208 | send_amount, 209 | destination, 210 | &coins_to_spend, 211 | args.dont_broadcast, 212 | ); 213 | } 214 | Subcommand::RunWatchtower { data_file_path } => { 215 | teleport::run_watchtower( 216 | &data_file_path.unwrap_or(Path::new("watchtower.dat").to_path_buf()), 217 | None, 218 | ); 219 | } 220 | Subcommand::TestWatchtowerClient { 221 | mut contract_transactions_hex, 222 | } => { 223 | if contract_transactions_hex.is_empty() { 224 | // https://bitcoin.stackexchange.com/questions/68811/what-is-the-absolute-smallest-size-of-the-data-bytes-that-a-blockchain-transac 225 | contract_transactions_hex = 226 | vec![String::from(concat!("020000000001010000000000000", 227 | "0000000000000000000000000000000000000000000000000000000000000fdffffff010100000000", 228 | "000000160014ffffffffffffffffffffffffffffffffffffffff02210200000000000000000000000", 229 | "000000000000000000000000000000000000000014730440220777777777777777777777777777777", 230 | "777777777777777777777777777777777702205555555555555555555555555555555555555555555", 231 | "5555555555555555555550100000000"))]; 232 | } 233 | let contract_txes = contract_transactions_hex 234 | .iter() 235 | .map(|cth| ContractTransaction { 236 | tx: deserialize::( 237 | &Vec::from_hex(&cth).expect("Invalid transaction hex string"), 238 | ) 239 | .expect("Unable to deserialize transaction hex"), 240 | redeemscript: Script::new(), 241 | hashlock_spend_without_preimage: None, 242 | timelock_spend: None, 243 | timelock_spend_broadcasted: false, 244 | }) 245 | .collect::>(); 246 | teleport::watchtower_client::test_watchtower_client(ContractsInfo { 247 | contract_txes, 248 | wallet_label: String::new(), 249 | }); 250 | } 251 | } 252 | 253 | Ok(()) 254 | } 255 | -------------------------------------------------------------------------------- /src/messages.rs: -------------------------------------------------------------------------------- 1 | //we make heavy use of serde_json's de/serialization for the benefits of 2 | //having the compiler check for us, extra type checking and readability 3 | 4 | //this works because of enum representations in serde 5 | //see https://serde.rs/enum-representations.html 6 | 7 | use serde::{Deserialize, Serialize}; 8 | 9 | use bitcoin::hashes::hash160::Hash as Hash160; 10 | use bitcoin::secp256k1::{SecretKey, Signature}; 11 | use bitcoin::util::ecdsa::PublicKey; 12 | use bitcoin::{OutPoint, Script, Transaction}; 13 | 14 | pub const PREIMAGE_LEN: usize = 32; 15 | pub type Preimage = [u8; PREIMAGE_LEN]; 16 | 17 | //TODO the structs here which are actual messages should have the word Message 18 | //added to their name e.g. SignSendersContractTx 19 | //to distinguish them from structs which just collect together 20 | //data e.g. SenderContractTxInfo 21 | 22 | #[derive(Debug, Serialize, Deserialize)] 23 | pub struct TakerHello { 24 | pub protocol_version_min: u32, 25 | pub protocol_version_max: u32, 26 | } 27 | 28 | #[derive(Debug, Serialize, Deserialize)] 29 | pub struct GiveOffer; 30 | 31 | #[derive(Debug, Serialize, Deserialize)] 32 | pub struct SenderContractTxNoncesInfo { 33 | pub multisig_key_nonce: SecretKey, 34 | pub hashlock_key_nonce: SecretKey, 35 | pub timelock_pubkey: PublicKey, 36 | pub senders_contract_tx: Transaction, 37 | pub multisig_redeemscript: Script, 38 | pub funding_input_value: u64, 39 | } 40 | 41 | #[derive(Debug, Serialize, Deserialize)] 42 | pub struct SignSendersContractTx { 43 | pub txes_info: Vec, 44 | pub hashvalue: Hash160, 45 | pub locktime: u16, 46 | } 47 | 48 | #[derive(Debug, Serialize, Deserialize)] 49 | pub struct ConfirmedCoinSwapTxInfo { 50 | pub funding_tx: Transaction, 51 | pub funding_tx_merkleproof: String, 52 | pub multisig_redeemscript: Script, 53 | pub multisig_key_nonce: SecretKey, 54 | pub contract_redeemscript: Script, 55 | pub hashlock_key_nonce: SecretKey, 56 | } 57 | 58 | #[derive(Debug, Serialize, Deserialize)] 59 | pub struct NextCoinSwapTxInfo { 60 | pub next_coinswap_multisig_pubkey: PublicKey, 61 | pub next_hashlock_pubkey: PublicKey, 62 | } 63 | 64 | #[derive(Debug, Serialize, Deserialize)] 65 | pub struct ProofOfFunding { 66 | pub confirmed_funding_txes: Vec, 67 | pub next_coinswap_info: Vec, 68 | pub next_locktime: u16, 69 | pub next_fee_rate: u64, 70 | } 71 | 72 | #[derive(Debug, Serialize, Deserialize)] 73 | pub struct SendersAndReceiversContractSigs { 74 | pub receivers_sigs: Vec, 75 | pub senders_sigs: Vec, 76 | } 77 | 78 | #[derive(Debug, Serialize, Deserialize)] 79 | pub struct ReceiversContractTxInfo { 80 | pub multisig_redeemscript: Script, 81 | pub contract_tx: Transaction, 82 | } 83 | 84 | #[derive(Debug, Serialize, Deserialize)] 85 | pub struct SignReceiversContractTx { 86 | pub txes: Vec, 87 | } 88 | 89 | #[derive(Debug, Serialize, Deserialize)] 90 | pub struct HashPreimage { 91 | pub senders_multisig_redeemscripts: Vec