From: LLFourn Date: Tue, 21 Feb 2023 05:23:08 +0000 (+1100) Subject: Convert to workspace X-Git-Tag: v1.0.0-alpha.0~6^2~46 X-Git-Url: http://internal-gitweb-vhost/script/%22https:/struct.CodeLengthError.html?a=commitdiff_plain;h=8a6de3aa2dc9bac479f059630d0788ec1e62301d;p=bdk Convert to workspace --- diff --git a/Cargo.toml b/Cargo.toml index c4820cbb..9549e423 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,76 +1,9 @@ -[package] -name = "bdk" -version = "0.27.1" -edition = "2018" -authors = ["Alekos Filini ", "Riccardo Casatta "] -homepage = "https://bitcoindevkit.org" -repository = "https://github.com/bitcoindevkit/bdk" -documentation = "https://docs.rs/bdk" -description = "A modern, lightweight, descriptor-based wallet library" -keywords = ["bitcoin", "wallet", "descriptor", "psbt"] -readme = "README.md" -license = "MIT OR Apache-2.0" -# TODO: remove this when examples all work -autoexamples = false - -[dependencies] -log = "^0.4" -miniscript = { version = "9", features = ["serde"] } -bitcoin = { version = "0.29", features = ["serde", "base64", "rand"] } -serde = { version = "^1.0", features = ["derive"] } -serde_json = { version = "^1.0" } -bdk_chain = { version = "0.1", features = ["miniscript", "serde"] } -rand = "^0.8" - -# Optional dependencies -hwi = { version = "0.5", optional = true, features = [ "use-miniscript"] } -bip39 = { version = "1.0.1", optional = true } - -[target.'cfg(target_arch = "wasm32")'.dependencies] -getrandom = "0.2" -js-sys = "0.3" - -[features] -default = ["std"] -std = [] -file-store = [ "std", "bdk_chain/file_store"] -compiler = ["miniscript/compiler"] -all-keys = ["keys-bip39"] -keys-bip39 = ["bip39"] -hardware-signer = ["hwi"] - -# Debug/Test features -test-md-docs = [] -test-hardware-signer = ["hardware-signer"] - -# This feature is used to run `cargo check` in our CI targeting wasm. It's not recommended -# for libraries to explicitly include the "getrandom/js" feature, so we only do it when -# necessary for running our CI. See: https://docs.rs/getrandom/0.2.8/getrandom/#webassembly-support -dev-getrandom-wasm = ["getrandom/js"] - -[dev-dependencies] -lazy_static = "1.4" -env_logger = "0.7" -# Move back to importing from rust-bitcoin once https://github.com/rust-bitcoin/rust-bitcoin/pull/1342 is released -base64 = "^0.13" -assert_matches = "1.5.0" - -[[example]] -name = "miniscriptc" -path = "examples/compiler.rs" -required-features = ["compiler"] - -[[example]] -name = "policy" -path = "examples/policy.rs" -required-features = ["std"] - -[[example]] -name = "mnemonic_to_descriptors" -path = "examples/mnemonic_to_descriptors.rs" -required-features = ["all-keys"] - -[package.metadata.docs.rs] -all-features = true -# defines the configuration attribute `docsrs` -rustdoc-args = ["--cfg", "docsrs"] +[workspace] +members = [ + "crates/bdk", + "example-crates/esplora-wallet", + "example-crates/electrum-wallet", +] + +[workspace.package] +authors = ["Bitcoin Dev Kit Developers"] diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 9c61848f..00000000 --- a/LICENSE +++ /dev/null @@ -1,14 +0,0 @@ -This software is licensed under [Apache 2.0](LICENSE-APACHE) or -[MIT](LICENSE-MIT), at your option. - -Some files retain their own copyright notice, however, for full authorship -information, see version control history. - -Except as otherwise noted in individual files, all files in this repository are -licensed under the Apache License, Version 2.0 or the MIT license , at your option. - -You may not use, copy, modify, merge, publish, distribute, sublicense, and/or -sell copies of this software or any files in this repository except in -accordance with one or both of these licenses. \ No newline at end of file diff --git a/LICENSE-APACHE b/LICENSE-APACHE deleted file mode 100644 index 261eeb9e..00000000 --- a/LICENSE-APACHE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/LICENSE-MIT b/LICENSE-MIT deleted file mode 100644 index 9d982a4d..00000000 --- a/LICENSE-MIT +++ /dev/null @@ -1,16 +0,0 @@ -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index 95ce4315..b34a26fe 100644 --- a/README.md +++ b/README.md @@ -1,197 +1,18 @@ -
-

BDK

+# The Bitcoin Dev Kit - +The `bdk` libraries aims to be the core building block for Bitcoin wallets of any kind. -

- A modern, lightweight, descriptor-based wallet library written in Rust! -

+The Bitcoin Dev Kit developers are in the process of releasing `v1.0` which is a fundamental +re-write of how the library works. -

- Crate Info - MIT or Apache-2.0 Licensed - CI Status - - API Docs - Rustc Version 1.57.0+ - Chat on Discord -

+See for some background on this project: https://bitcoindevkit.org/blog/road-to-bdk-1/ (ignore the timeline 😁) -

- Project Homepage - | - Documentation -

-
+For a release timeline see the [`bdk_core_staging`] repo where a lot of the component work is being done. The plan is that everything in the `bdk_core_staging` repo will be moved into the `crates` directory here. -## About -The `bdk` library aims to be the core building block for Bitcoin wallets of any kind. +[`bdk_core_staging`]: https://github.com/LLFourn/bdk_core_staging -* It uses [Miniscript](https://github.com/rust-bitcoin/rust-miniscript) to support descriptors with generalized conditions. This exact same library can be used to build - single-sig wallets, multisigs, timelocked contracts and more. -* It supports multiple blockchain backends and databases, allowing developers to choose exactly what's right for their projects. -* It's built to be cross-platform: the core logic works on desktop, mobile, and even WebAssembly. -* It's very easy to extend: developers can implement customized logic for blockchain backends, databases, signers, coin selection, and more, without having to fork and modify this library. -## Examples -### Sync the balance of a descriptor -```rust,no_run -use bdk::Wallet; -use bdk::blockchain::ElectrumBlockchain; -use bdk::SyncOptions; -use bdk::electrum_client::Client; -use bdk::bitcoin::Network; -fn main() -> Result<(), bdk::Error> { - let blockchain = ElectrumBlockchain::from(Client::new("ssl://electrum.blockstream.info:60002")?); - let wallet = Wallet::new( - "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)", - Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"), - Network::Testnet, - )?; - - wallet.sync(&blockchain, SyncOptions::default())?; - - println!("Descriptor balance: {} SAT", wallet.get_balance()?); - - Ok(()) -} -``` - -### Generate a few addresses - -```rust -use bdk::Wallet; -use bdk::wallet::AddressIndex::New; -use bdk::bitcoin::Network; - -fn main() -> Result<(), bdk::Error> { - let wallet = Wallet::new_no_persist( - "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)", - Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"), - Network::Testnet, - )?; - - println!("Address #0: {}", wallet.get_address(New)); - println!("Address #1: {}", wallet.get_address(New)); - println!("Address #2: {}", wallet.get_address(New)); - - Ok(()) -} -``` - -### Create a transaction - -```rust,no_run -use bdk::{FeeRate, Wallet, SyncOptions}; -use bdk::blockchain::ElectrumBlockchain; - -use bdk::electrum_client::Client; -use bdk::wallet::AddressIndex::New; - -use base64; -use bdk::bitcoin::consensus::serialize; -use bdk::bitcoin::Network; - -fn main() -> Result<(), bdk::Error> { - let blockchain = ElectrumBlockchain::from(Client::new("ssl://electrum.blockstream.info:60002")?); - let wallet = Wallet::new_no_persist( - "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)", - Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"), - Network::Testnet, - )?; - - wallet.sync(&blockchain, SyncOptions::default())?; - - let send_to = wallet.get_address(New); - let (psbt, details) = { - let mut builder = wallet.build_tx(); - builder - .add_recipient(send_to.script_pubkey(), 50_000) - .enable_rbf() - .do_not_spend_change() - .fee_rate(FeeRate::from_sat_per_vb(5.0)); - builder.finish()? - }; - - println!("Transaction details: {:#?}", details); - println!("Unsigned PSBT: {}", base64::encode(&serialize(&psbt))); - - Ok(()) -} -``` - -### Sign a transaction - -```rust,no_run -use bdk::{Wallet, SignOptions}; - -use base64; -use bdk::bitcoin::consensus::deserialize; -use bdk::bitcoin::Network; - -fn main() -> Result<(), bdk::Error> { - let wallet = Wallet::new_no_persist( - "wpkh([c258d2e4/84h/1h/0h]tprv8griRPhA7342zfRyB6CqeKF8CJDXYu5pgnj1cjL1u2ngKcJha5jjTRimG82ABzJQ4MQe71CV54xfn25BbhCNfEGGJZnxvCDQCd6JkbvxW6h/0/*)", - Some("wpkh([c258d2e4/84h/1h/0h]tprv8griRPhA7342zfRyB6CqeKF8CJDXYu5pgnj1cjL1u2ngKcJha5jjTRimG82ABzJQ4MQe71CV54xfn25BbhCNfEGGJZnxvCDQCd6JkbvxW6h/1/*)"), - Network::Testnet, - )?; - - let psbt = "..."; - let mut psbt = deserialize(&base64::decode(psbt).unwrap())?; - - let _finalized = wallet.sign(&mut psbt, SignOptions::default())?; - - Ok(()) -} -``` - -## Testing - -### Unit testing - -```bash -cargo test -``` - -### Integration testing - -Integration testing require testing features, for example: - -```bash -cargo test --features test-electrum -``` - -The other options are `test-esplora`, `test-rpc` or `test-rpc-legacy` which runs against an older version of Bitcoin Core. -Note that `electrs` and `bitcoind` binaries are automatically downloaded (on mac and linux), to specify you already have installed binaries you must use `--no-default-features` and provide `BITCOIND_EXE` and `ELECTRS_EXE` as environment variables. - -## Running under WASM - -If you want to run this library under WASM you will probably have to add the following lines to you `Cargo.toml`: - -```toml -[dependencies] -getrandom = { version = "0.2", features = ["js"] } -``` - -This enables the `rand` crate to work in environments where JavaScript is available. See [this link](https://docs.rs/getrandom/0.2.8/getrandom/#webassembly-support) to learn more. - -## License - -Licensed under either of - - * Apache License, Version 2.0 - ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) - * MIT license - ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) - -at your option. - -## Contribution - -Unless you explicitly state otherwise, any contribution intentionally submitted -for inclusion in the work by you, as defined in the Apache-2.0 license, shall be -dual licensed as above, without any additional terms or conditions. diff --git a/crates/bdk/Cargo.toml b/crates/bdk/Cargo.toml new file mode 100644 index 00000000..9031ff85 --- /dev/null +++ b/crates/bdk/Cargo.toml @@ -0,0 +1,63 @@ +[package] +name = "bdk" +homepage = "https://bitcoindevkit.org" +version = "1.0.0-alpha.0" +repository = "https://github.com/bitcoindevkit/bdk" +documentation = "https://docs.rs/bdk" +description = "A modern, lightweight, descriptor-based wallet library" +keywords = ["bitcoin", "wallet", "descriptor", "psbt"] +readme = "README.md" +license = "MIT OR Apache-2.0" +authors.workspace = true +edition = "2018" + + +[dependencies] +log = "^0.4" +rand = "^0.8" +miniscript = { version = "9", features = ["serde"] } +bitcoin = { version = "0.29", features = ["serde", "base64", "rand"] } +serde = { version = "^1.0", features = ["derive"] } +serde_json = { version = "^1.0" } +bdk_chain = { version = "0.1", features = ["miniscript", "serde"] } + +# Optional dependencies +hwi = { version = "0.5", optional = true, features = [ "use-miniscript"] } +bip39 = { version = "1.0.1", optional = true } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +getrandom = "0.2" +js-sys = "0.3" + + +[features] +default = ["std"] +std = [] +file-store = [ "std", "bdk_chain/file_store"] +compiler = ["miniscript/compiler"] +all-keys = ["keys-bip39"] +keys-bip39 = ["bip39"] +hardware-signer = ["hwi"] + +[dev-dependencies] +lazy_static = "1.4" +env_logger = "0.7" +# Move back to importing from rust-bitcoin once https://github.com/rust-bitcoin/rust-bitcoin/pull/1342 is released +base64 = "^0.13" +assert_matches = "1.5.0" + + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + + +[[example]] +name = "mnemonic_to_descriptors" +path = "examples/mnemonic_to_descriptors.rs" +required-features = ["all-keys"] + +[[example]] +name = "miniscriptc" +path = "examples/compiler.rs" +required-features = ["compiler"] diff --git a/crates/bdk/LICENSE b/crates/bdk/LICENSE new file mode 100644 index 00000000..9c61848f --- /dev/null +++ b/crates/bdk/LICENSE @@ -0,0 +1,14 @@ +This software is licensed under [Apache 2.0](LICENSE-APACHE) or +[MIT](LICENSE-MIT), at your option. + +Some files retain their own copyright notice, however, for full authorship +information, see version control history. + +Except as otherwise noted in individual files, all files in this repository are +licensed under the Apache License, Version 2.0 or the MIT license , at your option. + +You may not use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of this software or any files in this repository except in +accordance with one or both of these licenses. \ No newline at end of file diff --git a/crates/bdk/LICENSE-APACHE b/crates/bdk/LICENSE-APACHE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/crates/bdk/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/crates/bdk/LICENSE-MIT b/crates/bdk/LICENSE-MIT new file mode 100644 index 00000000..9d982a4d --- /dev/null +++ b/crates/bdk/LICENSE-MIT @@ -0,0 +1,16 @@ +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/crates/bdk/README.md b/crates/bdk/README.md new file mode 100644 index 00000000..9531f1cd --- /dev/null +++ b/crates/bdk/README.md @@ -0,0 +1,186 @@ +
+

BDK

+ + + +

+ A modern, lightweight, descriptor-based wallet library written in Rust! +

+ +

+ Crate Info + MIT or Apache-2.0 Licensed + CI Status + + API Docs + Rustc Version 1.57.0+ + Chat on Discord +

+ +

+ Project Homepage + | + Documentation +

+
+ +## About + +The `bdk` library aims to be the core building block for Bitcoin wallets of any kind. + +* It uses [Miniscript](https://github.com/rust-bitcoin/rust-miniscript) to support descriptors with generalized conditions. This exact same library can be used to build + single-sig wallets, multisigs, timelocked contracts and more. +* It supports multiple blockchain backends and databases, allowing developers to choose exactly what's right for their projects. +* It's built to be cross-platform: the core logic works on desktop, mobile, and even WebAssembly. +* It's very easy to extend: developers can implement customized logic for blockchain backends, databases, signers, coin selection, and more, without having to fork and modify this library. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +## Testing + +### Unit testing + +```bash +cargo test +``` + +## Running under WASM + +If you want to run this library under WASM you will probably have to add the following lines to you `Cargo.toml`: + +```toml +[dependencies] +getrandom = { version = "0.2", features = ["js"] } +``` + +This enables the `rand` crate to work in environments where JavaScript is available. See [this link](https://docs.rs/getrandom/0.2.8/getrandom/#webassembly-support) to learn more. + +## License + +Licensed under either of + + * Apache License, Version 2.0 + ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) + * MIT license + ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) + +at your option. + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in the work by you, as defined in the Apache-2.0 license, shall be +dual licensed as above, without any additional terms or conditions. diff --git a/crates/bdk/examples/compiler.rs b/crates/bdk/examples/compiler.rs new file mode 100644 index 00000000..f8918895 --- /dev/null +++ b/crates/bdk/examples/compiler.rs @@ -0,0 +1,73 @@ +// Bitcoin Dev Kit +// Written in 2020 by Alekos Filini +// +// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +extern crate bdk; +extern crate bitcoin; +extern crate log; +extern crate miniscript; +extern crate serde_json; + +use std::error::Error; +use std::str::FromStr; + +use log::info; + +use bitcoin::Network; +use miniscript::policy::Concrete; +use miniscript::Descriptor; + +use bdk::wallet::AddressIndex::New; +use bdk::{KeychainKind, Wallet}; + +/// Miniscript policy is a high level abstraction of spending conditions. Defined in the +/// rust-miniscript library here https://docs.rs/miniscript/7.0.0/miniscript/policy/index.html +/// rust-miniscript provides a `compile()` function that can be used to compile any miniscript policy +/// into a descriptor. This descriptor then in turn can be used in bdk a fully functioning wallet +/// can be derived from the policy. +/// +/// This example demonstrates the interaction between a bdk wallet and miniscript policy. + +fn main() -> Result<(), Box> { + env_logger::init_from_env( + env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"), + ); + + // We start with a generic miniscript policy string + let policy_str = "or(10@thresh(4,pk(029ffbe722b147f3035c87cb1c60b9a5947dd49c774cc31e94773478711a929ac0),pk(025f05815e3a1a8a83bfbb03ce016c9a2ee31066b98f567f6227df1d76ec4bd143),pk(025625f41e4a065efc06d5019cbbd56fe8c07595af1231e7cbc03fafb87ebb71ec),pk(02a27c8b850a00f67da3499b60562673dcf5fdfb82b7e17652a7ac54416812aefd),pk(03e618ec5f384d6e19ca9ebdb8e2119e5bef978285076828ce054e55c4daf473e2)),1@and(older(4209713),thresh(2,pk(03deae92101c790b12653231439f27b8897264125ecb2f46f48278603102573165),pk(033841045a531e1adf9910a6ec279589a90b3b8a904ee64ffd692bd08a8996c1aa),pk(02aebf2d10b040eb936a6f02f44ee82f8b34f5c1ccb20ff3949c2b28206b7c1068))))"; + info!("Compiling policy: \n{}", policy_str); + + // Parse the string as a [`Concrete`] type miniscript policy. + let policy = Concrete::::from_str(policy_str)?; + + // Create a `wsh` type descriptor from the policy. + // `policy.compile()` returns the resulting miniscript from the policy. + let descriptor = Descriptor::new_wsh(policy.compile()?)?; + + info!("Compiled into following Descriptor: \n{}", descriptor); + + // Create a new wallet from this descriptor + let mut wallet = Wallet::new_no_persist(&format!("{}", descriptor), None, Network::Regtest)?; + + info!( + "First derived address from the descriptor: \n{}", + wallet.get_address(New) + ); + + // BDK also has it's own `Policy` structure to represent the spending condition in a more + // human readable json format. + let spending_policy = wallet.policies(KeychainKind::External)?; + info!( + "The BDK spending policy: \n{}", + serde_json::to_string_pretty(&spending_policy)? + ); + + Ok(()) +} diff --git a/crates/bdk/examples/mnemonic_to_descriptors.rs b/crates/bdk/examples/mnemonic_to_descriptors.rs new file mode 100644 index 00000000..0a560a52 --- /dev/null +++ b/crates/bdk/examples/mnemonic_to_descriptors.rs @@ -0,0 +1,60 @@ +// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +use bdk::bitcoin::secp256k1::Secp256k1; +use bdk::bitcoin::util::bip32::DerivationPath; +use bdk::bitcoin::Network; +use bdk::descriptor; +use bdk::descriptor::IntoWalletDescriptor; +use bdk::keys::bip39::{Language, Mnemonic, WordCount}; +use bdk::keys::{GeneratableKey, GeneratedKey}; +use bdk::miniscript::Tap; +use bdk::Error as BDK_Error; +use std::error::Error; +use std::str::FromStr; + +/// This example demonstrates how to generate a mnemonic phrase +/// using BDK and use that to generate a descriptor string. +fn main() -> Result<(), Box> { + let secp = Secp256k1::new(); + + // In this example we are generating a 12 words mnemonic phrase + // but it is also possible generate 15, 18, 21 and 24 words + // using their respective `WordCount` variant. + let mnemonic: GeneratedKey<_, Tap> = + Mnemonic::generate((WordCount::Words12, Language::English)) + .map_err(|_| BDK_Error::Generic("Mnemonic generation error".to_string()))?; + + println!("Mnemonic phrase: {}", *mnemonic); + let mnemonic_with_passphrase = (mnemonic, None); + + // define external and internal derivation key path + let external_path = DerivationPath::from_str("m/86h/0h/0h/0").unwrap(); + let internal_path = DerivationPath::from_str("m/86h/0h/0h/1").unwrap(); + + // generate external and internal descriptor from mnemonic + let (external_descriptor, ext_keymap) = + descriptor!(tr((mnemonic_with_passphrase.clone(), external_path)))? + .into_wallet_descriptor(&secp, Network::Testnet)?; + let (internal_descriptor, int_keymap) = + descriptor!(tr((mnemonic_with_passphrase, internal_path)))? + .into_wallet_descriptor(&secp, Network::Testnet)?; + + println!("tpub external descriptor: {}", external_descriptor); + println!("tpub internal descriptor: {}", internal_descriptor); + println!( + "tprv external descriptor: {}", + external_descriptor.to_string_with_secret(&ext_keymap) + ); + println!( + "tprv internal descriptor: {}", + internal_descriptor.to_string_with_secret(&int_keymap) + ); + + Ok(()) +} diff --git a/crates/bdk/examples/policy.rs b/crates/bdk/examples/policy.rs new file mode 100644 index 00000000..64e17825 --- /dev/null +++ b/crates/bdk/examples/policy.rs @@ -0,0 +1,66 @@ +// Bitcoin Dev Kit +// Written in 2020 by Alekos Filini +// +// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +extern crate bdk; +extern crate env_logger; +extern crate log; +use std::error::Error; + +use bdk::bitcoin::Network; +use bdk::descriptor::{policy::BuildSatisfaction, ExtractPolicy, IntoWalletDescriptor}; +use bdk::wallet::signer::SignersContainer; + +/// This example describes the use of the BDK's [`bdk::descriptor::policy`] module. +/// +/// Policy is higher abstraction representation of the wallet descriptor spending condition. +/// This is useful to express complex miniscript spending conditions into more human readable form. +/// The resulting `Policy` structure can be used to derive spending conditions the wallet is capable +/// to spend from. +/// +/// This example demos a Policy output for a 2of2 multisig between between 2 parties, where the wallet holds +/// one of the Extend Private key. + +fn main() -> Result<(), Box> { + env_logger::init_from_env( + env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"), + ); + + let secp = bitcoin::secp256k1::Secp256k1::new(); + + // The descriptor used in the example + // The form is "wsh(multi(2, , ))" + let desc = "wsh(multi(2,tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/*,tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/*))"; + + // Use the descriptor string to derive the full descriptor and a keymap. + // The wallet descriptor can be used to create a new bdk::wallet. + // While the `keymap` can be used to create a `SignerContainer`. + // + // The `SignerContainer` can sign for `PSBT`s. + // a bdk::wallet internally uses these to handle transaction signing. + // But they can be used as independent tools also. + let (wallet_desc, keymap) = desc.into_wallet_descriptor(&secp, Network::Testnet)?; + + log::info!("Example Descriptor for policy analysis : {}", wallet_desc); + + // Create the signer with the keymap and descriptor. + let signers_container = SignersContainer::build(keymap, &wallet_desc, &secp); + + // Extract the Policy from the given descriptor and signer. + // Note that Policy is a wallet specific structure. It depends on the the descriptor, and + // what the concerned wallet with a given signer can sign for. + let policy = wallet_desc + .extract_policy(&signers_container, BuildSatisfaction::None, &secp)? + .expect("We expect a policy"); + + log::info!("Derived Policy for the descriptor {:#?}", policy); + + Ok(()) +} diff --git a/crates/bdk/src/descriptor/checksum.rs b/crates/bdk/src/descriptor/checksum.rs new file mode 100644 index 00000000..07120ab7 --- /dev/null +++ b/crates/bdk/src/descriptor/checksum.rs @@ -0,0 +1,182 @@ +// Bitcoin Dev Kit +// Written in 2020 by Alekos Filini +// +// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Descriptor checksum +//! +//! This module contains a re-implementation of the function used by Bitcoin Core to calculate the +//! checksum of a descriptor + +use crate::descriptor::DescriptorError; +use alloc::string::String; + +const INPUT_CHARSET: &[u8] = b"0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ "; +const CHECKSUM_CHARSET: &[u8] = b"qpzry9x8gf2tvdw0s3jn54khce6mua7l"; + +fn poly_mod(mut c: u64, val: u64) -> u64 { + let c0 = c >> 35; + c = ((c & 0x7ffffffff) << 5) ^ val; + if c0 & 1 > 0 { + c ^= 0xf5dee51989 + }; + if c0 & 2 > 0 { + c ^= 0xa9fdca3312 + }; + if c0 & 4 > 0 { + c ^= 0x1bab10e32d + }; + if c0 & 8 > 0 { + c ^= 0x3706b1677a + }; + if c0 & 16 > 0 { + c ^= 0x644d626ffd + }; + + c +} + +/// Computes the checksum bytes of a descriptor. +/// `exclude_hash = true` ignores all data after the first '#' (inclusive). +pub(crate) fn calc_checksum_bytes_internal( + mut desc: &str, + exclude_hash: bool, +) -> Result<[u8; 8], DescriptorError> { + let mut c = 1; + let mut cls = 0; + let mut clscount = 0; + + let mut original_checksum = None; + if exclude_hash { + if let Some(split) = desc.split_once('#') { + desc = split.0; + original_checksum = Some(split.1); + } + } + + for ch in desc.as_bytes() { + let pos = INPUT_CHARSET + .iter() + .position(|b| b == ch) + .ok_or(DescriptorError::InvalidDescriptorCharacter(*ch))? as u64; + c = poly_mod(c, pos & 31); + cls = cls * 3 + (pos >> 5); + clscount += 1; + if clscount == 3 { + c = poly_mod(c, cls); + cls = 0; + clscount = 0; + } + } + if clscount > 0 { + c = poly_mod(c, cls); + } + (0..8).for_each(|_| c = poly_mod(c, 0)); + c ^= 1; + + let mut checksum = [0_u8; 8]; + for j in 0..8 { + checksum[j] = CHECKSUM_CHARSET[((c >> (5 * (7 - j))) & 31) as usize]; + } + + // if input data already had a checksum, check calculated checksum against original checksum + if let Some(original_checksum) = original_checksum { + if original_checksum.as_bytes() != checksum { + return Err(DescriptorError::InvalidDescriptorChecksum); + } + } + + Ok(checksum) +} + +/// Compute the checksum bytes of a descriptor, excludes any existing checksum in the descriptor string from the calculation +pub fn calc_checksum_bytes(desc: &str) -> Result<[u8; 8], DescriptorError> { + calc_checksum_bytes_internal(desc, true) +} + +/// Compute the checksum of a descriptor, excludes any existing checksum in the descriptor string from the calculation +pub fn calc_checksum(desc: &str) -> Result { + // unsafe is okay here as the checksum only uses bytes in `CHECKSUM_CHARSET` + calc_checksum_bytes_internal(desc, true) + .map(|b| unsafe { String::from_utf8_unchecked(b.to_vec()) }) +} + +// TODO in release 0.25.0, remove get_checksum_bytes and get_checksum +// TODO in release 0.25.0, consolidate calc_checksum_bytes_internal into calc_checksum_bytes + +/// Compute the checksum bytes of a descriptor +#[deprecated( + since = "0.24.0", + note = "Use new `calc_checksum_bytes` function which excludes any existing checksum in the descriptor string before calculating the checksum hash bytes. See https://github.com/bitcoindevkit/bdk/pull/765." +)] +pub fn get_checksum_bytes(desc: &str) -> Result<[u8; 8], DescriptorError> { + calc_checksum_bytes_internal(desc, false) +} + +/// Compute the checksum of a descriptor +#[deprecated( + since = "0.24.0", + note = "Use new `calc_checksum` function which excludes any existing checksum in the descriptor string before calculating the checksum hash. See https://github.com/bitcoindevkit/bdk/pull/765." +)] +pub fn get_checksum(desc: &str) -> Result { + // unsafe is okay here as the checksum only uses bytes in `CHECKSUM_CHARSET` + calc_checksum_bytes_internal(desc, false) + .map(|b| unsafe { String::from_utf8_unchecked(b.to_vec()) }) +} + +#[cfg(test)] +mod test { + use super::*; + use crate::descriptor::calc_checksum; + use assert_matches::assert_matches; + + // test calc_checksum() function; it should return the same value as Bitcoin Core + #[test] + fn test_calc_checksum() { + let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)"; + assert_eq!(calc_checksum(desc).unwrap(), "tqz0nc62"); + + let desc = "pkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/44'/1'/0'/0/*)"; + assert_eq!(calc_checksum(desc).unwrap(), "lasegmfs"); + } + + // test calc_checksum() function; it should return the same value as Bitcoin Core even if the + // descriptor string includes a checksum hash + #[test] + fn test_calc_checksum_with_checksum_hash() { + let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)#tqz0nc62"; + assert_eq!(calc_checksum(desc).unwrap(), "tqz0nc62"); + + let desc = "pkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/44'/1'/0'/0/*)#lasegmfs"; + assert_eq!(calc_checksum(desc).unwrap(), "lasegmfs"); + + let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)#tqz0nc26"; + assert_matches!( + calc_checksum(desc), + Err(DescriptorError::InvalidDescriptorChecksum) + ); + + let desc = "pkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/44'/1'/0'/0/*)#lasegmsf"; + assert_matches!( + calc_checksum(desc), + Err(DescriptorError::InvalidDescriptorChecksum) + ); + } + + #[test] + fn test_calc_checksum_invalid_character() { + let sparkle_heart = unsafe { core::str::from_utf8_unchecked(&[240, 159, 146, 150]) }; + let invalid_desc = format!("wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcL{}fjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)", sparkle_heart); + + assert_matches!( + calc_checksum(&invalid_desc), + Err(DescriptorError::InvalidDescriptorCharacter(invalid_char)) if invalid_char == sparkle_heart.as_bytes()[0] + ); + } +} diff --git a/crates/bdk/src/descriptor/dsl.rs b/crates/bdk/src/descriptor/dsl.rs new file mode 100644 index 00000000..e689a540 --- /dev/null +++ b/crates/bdk/src/descriptor/dsl.rs @@ -0,0 +1,1220 @@ +// Bitcoin Dev Kit +// Written in 2020 by Alekos Filini +// +// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Descriptors DSL + +#[doc(hidden)] +#[macro_export] +macro_rules! impl_top_level_sh { + // disallow `sortedmulti` in `bare()` + ( Bare, new, new, Legacy, sortedmulti $( $inner:tt )* ) => { + compile_error!("`bare()` descriptors can't contain any `sortedmulti()` operands"); + }; + ( Bare, new, new, Legacy, sortedmulti_vec $( $inner:tt )* ) => { + compile_error!("`bare()` descriptors can't contain any `sortedmulti_vec()` operands"); + }; + + ( $inner_struct:ident, $constructor:ident, $sortedmulti_constructor:ident, $ctx:ident, sortedmulti $( $inner:tt )* ) => {{ + use core::marker::PhantomData; + + use $crate::miniscript::descriptor::{$inner_struct, Descriptor, DescriptorPublicKey}; + use $crate::miniscript::$ctx; + + let build_desc = |k, pks| { + Ok((Descriptor::::$inner_struct($inner_struct::$sortedmulti_constructor(k, pks)?), PhantomData::<$ctx>)) + }; + + $crate::impl_sortedmulti!(build_desc, sortedmulti $( $inner )*) + }}; + ( $inner_struct:ident, $constructor:ident, $sortedmulti_constructor:ident, $ctx:ident, sortedmulti_vec $( $inner:tt )* ) => {{ + use core::marker::PhantomData; + + use $crate::miniscript::descriptor::{$inner_struct, Descriptor, DescriptorPublicKey}; + use $crate::miniscript::$ctx; + + let build_desc = |k, pks| { + Ok((Descriptor::::$inner_struct($inner_struct::$sortedmulti_constructor(k, pks)?), PhantomData::<$ctx>)) + }; + + $crate::impl_sortedmulti!(build_desc, sortedmulti_vec $( $inner )*) + }}; + + ( $inner_struct:ident, $constructor:ident, $sortedmulti_constructor:ident, $ctx:ident, $( $minisc:tt )* ) => {{ + use $crate::miniscript::descriptor::{$inner_struct, Descriptor, DescriptorPublicKey}; + + $crate::fragment!($( $minisc )*) + .and_then(|(minisc, keymap, networks)| Ok(($inner_struct::$constructor(minisc)?, keymap, networks))) + .and_then(|(inner, key_map, valid_networks)| Ok((Descriptor::::$inner_struct(inner), key_map, valid_networks))) + }}; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! impl_top_level_pk { + ( $inner_type:ident, $ctx:ty, $key:expr ) => {{ + use $crate::miniscript::descriptor::$inner_type; + + #[allow(unused_imports)] + use $crate::keys::{DescriptorKey, IntoDescriptorKey}; + let secp = $crate::bitcoin::secp256k1::Secp256k1::new(); + + $key.into_descriptor_key() + .and_then(|key: DescriptorKey<$ctx>| key.extract(&secp)) + .map_err($crate::descriptor::DescriptorError::Key) + .map(|(pk, key_map, valid_networks)| ($inner_type::new(pk), key_map, valid_networks)) + }}; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! impl_top_level_tr { + ( $internal_key:expr, $tap_tree:expr ) => {{ + use $crate::miniscript::descriptor::{ + Descriptor, DescriptorPublicKey, KeyMap, TapTree, Tr, + }; + use $crate::miniscript::Tap; + + #[allow(unused_imports)] + use $crate::keys::{DescriptorKey, IntoDescriptorKey, ValidNetworks}; + + let secp = $crate::bitcoin::secp256k1::Secp256k1::new(); + + $internal_key + .into_descriptor_key() + .and_then(|key: DescriptorKey| key.extract(&secp)) + .map_err($crate::descriptor::DescriptorError::Key) + .and_then(|(pk, mut key_map, mut valid_networks)| { + let tap_tree = $tap_tree.map( + |(tap_tree, tree_keymap, tree_networks): ( + TapTree, + KeyMap, + ValidNetworks, + )| { + key_map.extend(tree_keymap.into_iter()); + valid_networks = + $crate::keys::merge_networks(&valid_networks, &tree_networks); + + tap_tree + }, + ); + + Ok(( + Descriptor::::Tr(Tr::new(pk, tap_tree)?), + key_map, + valid_networks, + )) + }) + }}; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! impl_leaf_opcode { + ( $terminal_variant:ident ) => {{ + use $crate::descriptor::CheckMiniscript; + + $crate::miniscript::Miniscript::from_ast( + $crate::miniscript::miniscript::decode::Terminal::$terminal_variant, + ) + .map_err($crate::descriptor::DescriptorError::Miniscript) + .and_then(|minisc| { + minisc.check_miniscript()?; + Ok(minisc) + }) + .map(|minisc| { + ( + minisc, + $crate::miniscript::descriptor::KeyMap::default(), + $crate::keys::any_network(), + ) + }) + }}; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! impl_leaf_opcode_value { + ( $terminal_variant:ident, $value:expr ) => {{ + use $crate::descriptor::CheckMiniscript; + + $crate::miniscript::Miniscript::from_ast( + $crate::miniscript::miniscript::decode::Terminal::$terminal_variant($value), + ) + .map_err($crate::descriptor::DescriptorError::Miniscript) + .and_then(|minisc| { + minisc.check_miniscript()?; + Ok(minisc) + }) + .map(|minisc| { + ( + minisc, + $crate::miniscript::descriptor::KeyMap::default(), + $crate::keys::any_network(), + ) + }) + }}; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! impl_leaf_opcode_value_two { + ( $terminal_variant:ident, $one:expr, $two:expr ) => {{ + use $crate::descriptor::CheckMiniscript; + + $crate::miniscript::Miniscript::from_ast( + $crate::miniscript::miniscript::decode::Terminal::$terminal_variant($one, $two), + ) + .map_err($crate::descriptor::DescriptorError::Miniscript) + .and_then(|minisc| { + minisc.check_miniscript()?; + Ok(minisc) + }) + .map(|minisc| { + ( + minisc, + $crate::miniscript::descriptor::KeyMap::default(), + $crate::keys::any_network(), + ) + }) + }}; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! impl_node_opcode_two { + ( $terminal_variant:ident, $( $inner:tt )* ) => ({ + use $crate::descriptor::CheckMiniscript; + + let inner = $crate::fragment_internal!( @t $( $inner )* ); + let (a, b) = $crate::descriptor::dsl::TupleTwo::from(inner).flattened(); + + a + .and_then(|a| Ok((a, b?))) + .and_then(|((a_minisc, mut a_keymap, a_networks), (b_minisc, b_keymap, b_networks))| { + // join key_maps + a_keymap.extend(b_keymap.into_iter()); + + let minisc = $crate::miniscript::Miniscript::from_ast($crate::miniscript::miniscript::decode::Terminal::$terminal_variant( + $crate::alloc::sync::Arc::new(a_minisc), + $crate::alloc::sync::Arc::new(b_minisc), + ))?; + + minisc.check_miniscript()?; + + Ok((minisc, a_keymap, $crate::keys::merge_networks(&a_networks, &b_networks))) + }) + }); +} + +#[doc(hidden)] +#[macro_export] +macro_rules! impl_node_opcode_three { + ( $terminal_variant:ident, $( $inner:tt )* ) => ({ + use $crate::descriptor::CheckMiniscript; + + let inner = $crate::fragment_internal!( @t $( $inner )* ); + let (a, b, c) = $crate::descriptor::dsl::TupleThree::from(inner).flattened(); + + a + .and_then(|a| Ok((a, b?, c?))) + .and_then(|((a_minisc, mut a_keymap, a_networks), (b_minisc, b_keymap, b_networks), (c_minisc, c_keymap, c_networks))| { + // join key_maps + a_keymap.extend(b_keymap.into_iter()); + a_keymap.extend(c_keymap.into_iter()); + + let networks = $crate::keys::merge_networks(&a_networks, &b_networks); + let networks = $crate::keys::merge_networks(&networks, &c_networks); + + let minisc = $crate::miniscript::Miniscript::from_ast($crate::miniscript::miniscript::decode::Terminal::$terminal_variant( + $crate::alloc::sync::Arc::new(a_minisc), + $crate::alloc::sync::Arc::new(b_minisc), + $crate::alloc::sync::Arc::new(c_minisc), + ))?; + + minisc.check_miniscript()?; + + Ok((minisc, a_keymap, networks)) + }) + }); +} + +#[doc(hidden)] +#[macro_export] +macro_rules! impl_sortedmulti { + ( $build_desc:expr, sortedmulti_vec ( $thresh:expr, $keys:expr ) ) => ({ + let secp = $crate::bitcoin::secp256k1::Secp256k1::new(); + $crate::keys::make_sortedmulti($thresh, $keys, $build_desc, &secp) + }); + ( $build_desc:expr, sortedmulti ( $thresh:expr $(, $key:expr )+ ) ) => ({ + use $crate::keys::IntoDescriptorKey; + let secp = $crate::bitcoin::secp256k1::Secp256k1::new(); + + let keys = vec![ + $( + $key.into_descriptor_key(), + )* + ]; + + keys.into_iter().collect::, _>>() + .map_err($crate::descriptor::DescriptorError::Key) + .and_then(|keys| $crate::keys::make_sortedmulti($thresh, keys, $build_desc, &secp)) + }); + +} + +#[doc(hidden)] +#[macro_export] +macro_rules! parse_tap_tree { + ( @merge $tree_a:expr, $tree_b:expr) => {{ + use $crate::alloc::sync::Arc; + use $crate::miniscript::descriptor::TapTree; + + $tree_a + .and_then(|tree_a| Ok((tree_a, $tree_b?))) + .and_then(|((a_tree, mut a_keymap, a_networks), (b_tree, b_keymap, b_networks))| { + a_keymap.extend(b_keymap.into_iter()); + Ok((TapTree::Tree(Arc::new(a_tree), Arc::new(b_tree)), a_keymap, $crate::keys::merge_networks(&a_networks, &b_networks))) + }) + + }}; + + // Two sub-trees + ( { { $( $tree_a:tt )* }, { $( $tree_b:tt )* } } ) => {{ + let tree_a = $crate::parse_tap_tree!( { $( $tree_a )* } ); + let tree_b = $crate::parse_tap_tree!( { $( $tree_b )* } ); + + $crate::parse_tap_tree!(@merge tree_a, tree_b) + }}; + + // One leaf and a sub-tree + ( { $op_a:ident ( $( $minisc_a:tt )* ), { $( $tree_b:tt )* } } ) => {{ + let tree_a = $crate::parse_tap_tree!( $op_a ( $( $minisc_a )* ) ); + let tree_b = $crate::parse_tap_tree!( { $( $tree_b )* } ); + + $crate::parse_tap_tree!(@merge tree_a, tree_b) + }}; + ( { { $( $tree_a:tt )* }, $op_b:ident ( $( $minisc_b:tt )* ) } ) => {{ + let tree_a = $crate::parse_tap_tree!( { $( $tree_a )* } ); + let tree_b = $crate::parse_tap_tree!( $op_b ( $( $minisc_b )* ) ); + + $crate::parse_tap_tree!(@merge tree_a, tree_b) + }}; + + // Two leaves + ( { $op_a:ident ( $( $minisc_a:tt )* ), $op_b:ident ( $( $minisc_b:tt )* ) } ) => {{ + let tree_a = $crate::parse_tap_tree!( $op_a ( $( $minisc_a )* ) ); + let tree_b = $crate::parse_tap_tree!( $op_b ( $( $minisc_b )* ) ); + + $crate::parse_tap_tree!(@merge tree_a, tree_b) + }}; + + // Single leaf + ( $op:ident ( $( $minisc:tt )* ) ) => {{ + use $crate::alloc::sync::Arc; + use $crate::miniscript::descriptor::TapTree; + + $crate::fragment!( $op ( $( $minisc )* ) ) + .map(|(a_minisc, a_keymap, a_networks)| (TapTree::Leaf(Arc::new(a_minisc)), a_keymap, a_networks)) + }}; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! apply_modifier { + ( $terminal_variant:ident, $inner:expr ) => {{ + use $crate::descriptor::CheckMiniscript; + + $inner + .map_err(|e| -> $crate::descriptor::DescriptorError { e.into() }) + .and_then(|(minisc, keymap, networks)| { + let minisc = $crate::miniscript::Miniscript::from_ast( + $crate::miniscript::miniscript::decode::Terminal::$terminal_variant( + $crate::alloc::sync::Arc::new(minisc), + ), + )?; + + minisc.check_miniscript()?; + + Ok((minisc, keymap, networks)) + }) + }}; + + ( a: $inner:expr ) => {{ + $crate::apply_modifier!(Alt, $inner) + }}; + ( s: $inner:expr ) => {{ + $crate::apply_modifier!(Swap, $inner) + }}; + ( c: $inner:expr ) => {{ + $crate::apply_modifier!(Check, $inner) + }}; + ( d: $inner:expr ) => {{ + $crate::apply_modifier!(DupIf, $inner) + }}; + ( v: $inner:expr ) => {{ + $crate::apply_modifier!(Verify, $inner) + }}; + ( j: $inner:expr ) => {{ + $crate::apply_modifier!(NonZero, $inner) + }}; + ( n: $inner:expr ) => {{ + $crate::apply_modifier!(ZeroNotEqual, $inner) + }}; + + // Modifiers expanded to other operators + ( t: $inner:expr ) => {{ + $inner.and_then(|(a_minisc, a_keymap, a_networks)| { + $crate::impl_leaf_opcode_value_two!( + AndV, + $crate::alloc::sync::Arc::new(a_minisc), + $crate::alloc::sync::Arc::new($crate::fragment!(true).unwrap().0) + ) + .map(|(minisc, _, _)| (minisc, a_keymap, a_networks)) + }) + }}; + ( l: $inner:expr ) => {{ + $inner.and_then(|(a_minisc, a_keymap, a_networks)| { + $crate::impl_leaf_opcode_value_two!( + OrI, + $crate::alloc::sync::Arc::new($crate::fragment!(false).unwrap().0), + $crate::alloc::sync::Arc::new(a_minisc) + ) + .map(|(minisc, _, _)| (minisc, a_keymap, a_networks)) + }) + }}; + ( u: $inner:expr ) => {{ + $inner.and_then(|(a_minisc, a_keymap, a_networks)| { + $crate::impl_leaf_opcode_value_two!( + OrI, + $crate::alloc::sync::Arc::new(a_minisc), + $crate::alloc::sync::Arc::new($crate::fragment!(false).unwrap().0) + ) + .map(|(minisc, _, _)| (minisc, a_keymap, a_networks)) + }) + }}; +} + +/// Macro to write full descriptors with code +/// +/// This macro expands to a `Result` of +/// [`DescriptorTemplateOut`](super::template::DescriptorTemplateOut) and [`DescriptorError`](crate::descriptor::DescriptorError) +/// +/// The syntax is very similar to the normal descriptor syntax, with the exception that modifiers +/// cannot be grouped together. For instance, a descriptor fragment like `sdv:older(144)` has to be +/// broken up to `s:d:v:older(144)`. +/// +/// The `pk()`, `pk_k()` and `pk_h()` operands can take as argument any type that implements +/// [`IntoDescriptorKey`]. This means that keys can also be written inline as strings, but in that +/// case they must be wrapped in quotes, which is another difference compared to the standard +/// descriptor syntax. +/// +/// [`IntoDescriptorKey`]: crate::keys::IntoDescriptorKey +/// +/// ## Example +/// +/// Signature plus timelock descriptor: +/// +/// ``` +/// # use std::str::FromStr; +/// let (my_descriptor, my_keys_map, networks) = bdk::descriptor!(sh(wsh(and_v(v:pk("cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy"),older(50)))))?; +/// # Ok::<(), Box>(()) +/// ``` +/// +/// ------- +/// +/// 2-of-3 that becomes a 1-of-3 after a timelock has expired. Both `descriptor_a` and `descriptor_b` are equivalent: the first +/// syntax is more suitable for a fixed number of items known at compile time, while the other accepts a +/// [`Vec`] of items, which makes it more suitable for writing dynamic descriptors. +/// +/// They both produce the descriptor: `wsh(thresh(2,pk(...),s:pk(...),sndv:older(...)))` +/// +/// ``` +/// # use std::str::FromStr; +/// let my_key_1 = bitcoin::PublicKey::from_str( +/// "02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c", +/// )?; +/// let my_key_2 = +/// bitcoin::PrivateKey::from_wif("cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy")?; +/// let my_timelock = 50; +/// +/// let (descriptor_a, key_map_a, networks) = bdk::descriptor! { +/// wsh ( +/// thresh(2, pk(my_key_1), s:pk(my_key_2), s:n:d:v:older(my_timelock)) +/// ) +/// }?; +/// +/// #[rustfmt::skip] +/// let b_items = vec![ +/// bdk::fragment!(pk(my_key_1))?, +/// bdk::fragment!(s:pk(my_key_2))?, +/// bdk::fragment!(s:n:d:v:older(my_timelock))?, +/// ]; +/// let (descriptor_b, mut key_map_b, networks) = bdk::descriptor!(wsh(thresh_vec(2, b_items)))?; +/// +/// assert_eq!(descriptor_a, descriptor_b); +/// assert_eq!(key_map_a.len(), key_map_b.len()); +/// # Ok::<(), Box>(()) +/// ``` +/// +/// ------ +/// +/// Simple 2-of-2 multi-signature, equivalent to: `wsh(multi(2, ...))` +/// +/// ``` +/// # use std::str::FromStr; +/// let my_key_1 = bitcoin::PublicKey::from_str( +/// "02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c", +/// )?; +/// let my_key_2 = +/// bitcoin::PrivateKey::from_wif("cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy")?; +/// +/// let (descriptor, key_map, networks) = bdk::descriptor! { +/// wsh ( +/// multi(2, my_key_1, my_key_2) +/// ) +/// }?; +/// # Ok::<(), Box>(()) +/// ``` +/// +/// ------ +/// +/// Native-Segwit single-sig, equivalent to: `wpkh(...)` +/// +/// ``` +/// let my_key = +/// bitcoin::PrivateKey::from_wif("cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy")?; +/// +/// let (descriptor, key_map, networks) = bdk::descriptor!(wpkh(my_key))?; +/// # Ok::<(), Box>(()) +/// ``` +#[macro_export] +macro_rules! descriptor { + ( bare ( $( $minisc:tt )* ) ) => ({ + $crate::impl_top_level_sh!(Bare, new, new, Legacy, $( $minisc )*) + }); + ( sh ( wsh ( $( $minisc:tt )* ) ) ) => ({ + $crate::descriptor!(shwsh ($( $minisc )*)) + }); + ( shwsh ( $( $minisc:tt )* ) ) => ({ + $crate::impl_top_level_sh!(Sh, new_wsh, new_wsh_sortedmulti, Segwitv0, $( $minisc )*) + }); + ( pk ( $key:expr ) ) => ({ + // `pk()` is actually implemented as `bare(pk())` + $crate::descriptor!( bare ( pk ( $key ) ) ) + }); + ( pkh ( $key:expr ) ) => ({ + use $crate::miniscript::descriptor::{Descriptor, DescriptorPublicKey}; + + $crate::impl_top_level_pk!(Pkh, $crate::miniscript::Legacy, $key) + .map(|(a, b, c)| (Descriptor::::Pkh(a), b, c)) + }); + ( wpkh ( $key:expr ) ) => ({ + use $crate::miniscript::descriptor::{Descriptor, DescriptorPublicKey}; + + $crate::impl_top_level_pk!(Wpkh, $crate::miniscript::Segwitv0, $key) + .and_then(|(a, b, c)| Ok((a?, b, c))) + .map(|(a, b, c)| (Descriptor::::Wpkh(a), b, c)) + }); + ( sh ( wpkh ( $key:expr ) ) ) => ({ + $crate::descriptor!(shwpkh ( $key )) + }); + ( shwpkh ( $key:expr ) ) => ({ + use $crate::miniscript::descriptor::{Descriptor, DescriptorPublicKey, Sh}; + + $crate::impl_top_level_pk!(Wpkh, $crate::miniscript::Segwitv0, $key) + .and_then(|(a, b, c)| Ok((a?, b, c))) + .and_then(|(a, b, c)| Ok((Descriptor::::Sh(Sh::new_wpkh(a.into_inner())?), b, c))) + }); + ( sh ( $( $minisc:tt )* ) ) => ({ + $crate::impl_top_level_sh!(Sh, new, new_sortedmulti, Legacy, $( $minisc )*) + }); + ( wsh ( $( $minisc:tt )* ) ) => ({ + $crate::impl_top_level_sh!(Wsh, new, new_sortedmulti, Segwitv0, $( $minisc )*) + }); + + ( tr ( $internal_key:expr ) ) => ({ + $crate::impl_top_level_tr!($internal_key, None) + }); + ( tr ( $internal_key:expr, $( $taptree:tt )* ) ) => ({ + let tap_tree = $crate::parse_tap_tree!( $( $taptree )* ); + tap_tree + .and_then(|tap_tree| $crate::impl_top_level_tr!($internal_key, Some(tap_tree))) + }); +} + +#[doc(hidden)] +pub struct TupleTwo { + pub a: A, + pub b: B, +} + +impl TupleTwo { + pub fn flattened(self) -> (A, B) { + (self.a, self.b) + } +} + +impl From<(A, (B, ()))> for TupleTwo { + fn from((a, (b, _)): (A, (B, ()))) -> Self { + TupleTwo { a, b } + } +} + +#[doc(hidden)] +pub struct TupleThree { + pub a: A, + pub b: B, + pub c: C, +} + +impl TupleThree { + pub fn flattened(self) -> (A, B, C) { + (self.a, self.b, self.c) + } +} + +impl From<(A, (B, (C, ())))> for TupleThree { + fn from((a, (b, (c, _))): (A, (B, (C, ())))) -> Self { + TupleThree { a, b, c } + } +} + +#[doc(hidden)] +#[macro_export] +macro_rules! group_multi_keys { + ( $( $key:expr ),+ ) => {{ + use $crate::keys::IntoDescriptorKey; + + let keys = vec![ + $( + $key.into_descriptor_key(), + )* + ]; + + keys.into_iter().collect::, _>>() + .map_err($crate::descriptor::DescriptorError::Key) + }}; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! fragment_internal { + // The @v prefix is used to parse a sequence of operands and return them in a vector. This is + // used by operands that take a variable number of arguments, like `thresh()` and `multi()`. + ( @v $op:ident ( $( $args:tt )* ) $( $tail:tt )* ) => ({ + let mut v = vec![$crate::fragment!( $op ( $( $args )* ) )]; + v.append(&mut $crate::fragment_internal!( @v $( $tail )* )); + + v + }); + // Match modifiers + ( @v $modif:tt : $( $tail:tt )* ) => ({ + let mut v = $crate::fragment_internal!( @v $( $tail )* ); + let first = v.drain(..1).next().unwrap(); + + let first = $crate::apply_modifier!($modif:first); + + let mut v_final = vec![first]; + v_final.append(&mut v); + + v_final + }); + // Remove commas between operands + ( @v , $( $tail:tt )* ) => ({ + $crate::fragment_internal!( @v $( $tail )* ) + }); + ( @v ) => ({ + vec![] + }); + + // The @t prefix is used to parse a sequence of operands and return them in a tuple. This + // allows checking at compile-time the number of arguments passed to an operand. For this + // reason it's used by `and_*()`, `or_*()`, etc. + // + // Unfortunately, due to the fact that concatenating tuples is pretty hard, the final result + // adds in the first spot the parsed operand and in the second spot the result of parsing + // all the following ones. For two operands the type then corresponds to: (X, (X, ())). For + // three operands it's (X, (X, (X, ()))), etc. + // + // To check that the right number of arguments has been passed we can "cast" those tuples to + // more convenient structures like `TupleTwo`. If the conversion succeeds, the right number of + // args was passed. Otherwise the compilation fails entirely. + ( @t $op:ident ( $( $args:tt )* ) $( $tail:tt )* ) => ({ + ($crate::fragment!( $op ( $( $args )* ) ), $crate::fragment_internal!( @t $( $tail )* )) + }); + // Match modifiers + ( @t $modif:tt : $( $tail:tt )* ) => ({ + let (first, tail) = $crate::fragment_internal!( @t $( $tail )* ); + ($crate::apply_modifier!($modif:first), tail) + }); + // Remove commas between operands + ( @t , $( $tail:tt )* ) => ({ + $crate::fragment_internal!( @t $( $tail )* ) + }); + ( @t ) => ({}); + + // Fallback to calling `fragment!()` + ( $( $tokens:tt )* ) => ({ + $crate::fragment!($( $tokens )*) + }); +} + +/// Macro to write descriptor fragments with code +/// +/// This macro will be expanded to an object of type `Result<(Miniscript, KeyMap, ValidNetworks), DescriptorError>`. It allows writing +/// fragments of larger descriptors that can be pieced together using `fragment!(thresh_vec(m, ...))`. +/// +/// The syntax to write macro fragment is the same as documented for the [`descriptor`] macro. +#[macro_export] +macro_rules! fragment { + // Modifiers + ( $modif:tt : $( $tail:tt )* ) => ({ + let op = $crate::fragment!( $( $tail )* ); + $crate::apply_modifier!($modif:op) + }); + + // Miniscript + ( true ) => ({ + $crate::impl_leaf_opcode!(True) + }); + ( false ) => ({ + $crate::impl_leaf_opcode!(False) + }); + ( pk_k ( $key:expr ) ) => ({ + let secp = $crate::bitcoin::secp256k1::Secp256k1::new(); + $crate::keys::make_pk($key, &secp) + }); + ( pk ( $key:expr ) ) => ({ + $crate::fragment!(c:pk_k ( $key )) + }); + ( pk_h ( $key:expr ) ) => ({ + let secp = $crate::bitcoin::secp256k1::Secp256k1::new(); + $crate::keys::make_pkh($key, &secp) + }); + ( after ( $value:expr ) ) => ({ + $crate::impl_leaf_opcode_value!(After, $crate::bitcoin::PackedLockTime($value)) // TODO!! https://github.com/rust-bitcoin/rust-bitcoin/issues/1302 + }); + ( older ( $value:expr ) ) => ({ + $crate::impl_leaf_opcode_value!(Older, $crate::bitcoin::Sequence($value)) // TODO!! + }); + ( sha256 ( $hash:expr ) ) => ({ + $crate::impl_leaf_opcode_value!(Sha256, $hash) + }); + ( hash256 ( $hash:expr ) ) => ({ + $crate::impl_leaf_opcode_value!(Hash256, $hash) + }); + ( ripemd160 ( $hash:expr ) ) => ({ + $crate::impl_leaf_opcode_value!(Ripemd160, $hash) + }); + ( hash160 ( $hash:expr ) ) => ({ + $crate::impl_leaf_opcode_value!(Hash160, $hash) + }); + ( and_v ( $( $inner:tt )* ) ) => ({ + $crate::impl_node_opcode_two!(AndV, $( $inner )*) + }); + ( and_b ( $( $inner:tt )* ) ) => ({ + $crate::impl_node_opcode_two!(AndB, $( $inner )*) + }); + ( and_or ( $( $inner:tt )* ) ) => ({ + $crate::impl_node_opcode_three!(AndOr, $( $inner )*) + }); + ( andor ( $( $inner:tt )* ) ) => ({ + $crate::impl_node_opcode_three!(AndOr, $( $inner )*) + }); + ( or_b ( $( $inner:tt )* ) ) => ({ + $crate::impl_node_opcode_two!(OrB, $( $inner )*) + }); + ( or_d ( $( $inner:tt )* ) ) => ({ + $crate::impl_node_opcode_two!(OrD, $( $inner )*) + }); + ( or_c ( $( $inner:tt )* ) ) => ({ + $crate::impl_node_opcode_two!(OrC, $( $inner )*) + }); + ( or_i ( $( $inner:tt )* ) ) => ({ + $crate::impl_node_opcode_two!(OrI, $( $inner )*) + }); + ( thresh_vec ( $thresh:expr, $items:expr ) ) => ({ + use $crate::miniscript::descriptor::KeyMap; + + let (items, key_maps_networks): ($crate::alloc::vec::Vec<_>, $crate::alloc::vec::Vec<_>) = $items.into_iter().map(|(a, b, c)| (a, (b, c))).unzip(); + let items = items.into_iter().map($crate::alloc::sync::Arc::new).collect(); + + let (key_maps, valid_networks) = key_maps_networks.into_iter().fold((KeyMap::default(), $crate::keys::any_network()), |(mut keys_acc, net_acc), (key, net)| { + keys_acc.extend(key.into_iter()); + let net_acc = $crate::keys::merge_networks(&net_acc, &net); + + (keys_acc, net_acc) + }); + + $crate::impl_leaf_opcode_value_two!(Thresh, $thresh, items) + .map(|(minisc, _, _)| (minisc, key_maps, valid_networks)) + }); + ( thresh ( $thresh:expr, $( $inner:tt )* ) ) => ({ + let items = $crate::fragment_internal!( @v $( $inner )* ); + + items.into_iter().collect::, _>>() + .and_then(|items| $crate::fragment!(thresh_vec($thresh, items))) + }); + ( multi_vec ( $thresh:expr, $keys:expr ) ) => ({ + let secp = $crate::bitcoin::secp256k1::Secp256k1::new(); + + $crate::keys::make_multi($thresh, $crate::miniscript::Terminal::Multi, $keys, &secp) + }); + ( multi ( $thresh:expr $(, $key:expr )+ ) ) => ({ + $crate::group_multi_keys!( $( $key ),* ) + .and_then(|keys| $crate::fragment!( multi_vec ( $thresh, keys ) )) + }); + ( multi_a_vec ( $thresh:expr, $keys:expr ) ) => ({ + let secp = $crate::bitcoin::secp256k1::Secp256k1::new(); + + $crate::keys::make_multi($thresh, $crate::miniscript::Terminal::MultiA, $keys, &secp) + }); + ( multi_a ( $thresh:expr $(, $key:expr )+ ) ) => ({ + $crate::group_multi_keys!( $( $key ),* ) + .and_then(|keys| $crate::fragment!( multi_a_vec ( $thresh, keys ) )) + }); + + // `sortedmulti()` is handled separately + ( sortedmulti ( $( $inner:tt )* ) ) => ({ + compile_error!("`sortedmulti` can only be used as the root operand of a descriptor"); + }); + ( sortedmulti_vec ( $( $inner:tt )* ) ) => ({ + compile_error!("`sortedmulti_vec` can only be used as the root operand of a descriptor"); + }); +} + +#[cfg(test)] +mod test { + use alloc::string::ToString; + use bitcoin::hashes::hex::ToHex; + use bitcoin::secp256k1::Secp256k1; + use miniscript::descriptor::{DescriptorPublicKey, KeyMap}; + use miniscript::{Descriptor, Legacy, Segwitv0}; + + use core::str::FromStr; + + use crate::descriptor::{DescriptorError, DescriptorMeta}; + use crate::keys::{DescriptorKey, IntoDescriptorKey, ValidNetworks}; + use bitcoin::network::constants::Network::{Bitcoin, Regtest, Signet, Testnet}; + use bitcoin::util::bip32; + use bitcoin::PrivateKey; + + // test the descriptor!() macro + + // verify descriptor generates expected script(s) (if bare or pk) or address(es) + fn check( + desc: Result<(Descriptor, KeyMap, ValidNetworks), DescriptorError>, + is_witness: bool, + is_fixed: bool, + expected: &[&str], + ) { + let (desc, _key_map, _networks) = desc.unwrap(); + assert_eq!(desc.is_witness(), is_witness); + assert_eq!(!desc.has_wildcard(), is_fixed); + for i in 0..expected.len() { + let index = i as u32; + let child_desc = if !desc.has_wildcard() { + desc.at_derivation_index(0) + } else { + desc.at_derivation_index(index) + }; + let address = child_desc.address(Regtest); + if let Ok(address) = address { + assert_eq!(address.to_string(), *expected.get(i).unwrap()); + } else { + let script = child_desc.script_pubkey(); + assert_eq!(script.to_hex().as_str(), *expected.get(i).unwrap()); + } + } + } + + // - at least one of each "type" of operator; i.e. one modifier, one leaf_opcode, one leaf_opcode_value, etc. + // - mixing up key types that implement IntoDescriptorKey in multi() or thresh() + + // expected script for pk and bare manually created + // expected addresses created with `bitcoin-cli getdescriptorinfo` (for hash) and `bitcoin-cli deriveaddresses` + + #[test] + fn test_fixed_legacy_descriptors() { + let pubkey1 = bitcoin::PublicKey::from_str( + "03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd", + ) + .unwrap(); + let pubkey2 = bitcoin::PublicKey::from_str( + "032e58afe51f9ed8ad3cc7897f634d881fdbe49a81564629ded8156bebd2ffd1af", + ) + .unwrap(); + + check( + descriptor!(bare(multi(1,pubkey1,pubkey2))), + false, + true, + &["512103a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd21032e58afe51f9ed8ad3cc7897f634d881fdbe49a81564629ded8156bebd2ffd1af52ae"], + ); + check( + descriptor!(pk(pubkey1)), + false, + true, + &["2103a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bdac"], + ); + check( + descriptor!(pkh(pubkey1)), + false, + true, + &["muZpTpBYhxmRFuCjLc7C6BBDF32C8XVJUi"], + ); + check( + descriptor!(sh(multi(1, pubkey1, pubkey2))), + false, + true, + &["2MymURoV1bzuMnWMGiXzyomDkeuxXY7Suey"], + ); + } + + #[test] + fn test_fixed_segwitv0_descriptors() { + let pubkey1 = bitcoin::PublicKey::from_str( + "03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd", + ) + .unwrap(); + let pubkey2 = bitcoin::PublicKey::from_str( + "032e58afe51f9ed8ad3cc7897f634d881fdbe49a81564629ded8156bebd2ffd1af", + ) + .unwrap(); + + check( + descriptor!(wpkh(pubkey1)), + true, + true, + &["bcrt1qngw83fg8dz0k749cg7k3emc7v98wy0c7azaa6h"], + ); + check( + descriptor!(sh(wpkh(pubkey1))), + true, + true, + &["2N5LiC3CqzxDamRTPG1kiNv1FpNJQ7x28sb"], + ); + check( + descriptor!(wsh(multi(1, pubkey1, pubkey2))), + true, + true, + &["bcrt1qgw8jvv2hsrvjfa6q66rk6har7d32lrqm5unnf5cl63q9phxfvgps5fyfqe"], + ); + check( + descriptor!(sh(wsh(multi(1, pubkey1, pubkey2)))), + true, + true, + &["2NCidRJysy7apkmE6JF5mLLaJFkrN3Ub9iy"], + ); + } + + #[test] + fn test_fixed_threeop_descriptors() { + let redeem_key = bitcoin::PublicKey::from_str( + "03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd", + ) + .unwrap(); + let move_key = bitcoin::PublicKey::from_str( + "032e58afe51f9ed8ad3cc7897f634d881fdbe49a81564629ded8156bebd2ffd1af", + ) + .unwrap(); + + check( + descriptor!(sh(wsh(and_or(pk(redeem_key), older(1000), pk(move_key))))), + true, + true, + &["2MypGwr5eQWAWWJtiJgUEToVxc4zuokjQRe"], + ); + } + + #[test] + fn test_bip32_legacy_descriptors() { + let xprv = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); + + let path = bip32::DerivationPath::from_str("m/0").unwrap(); + let desc_key = (xprv, path.clone()).into_descriptor_key().unwrap(); + check( + descriptor!(pk(desc_key)), + false, + false, + &[ + "2102363ad03c10024e1b597a5b01b9982807fb638e00b06f3b2d4a89707de3b93c37ac", + "2102063a21fd780df370ed2fc8c4b86aa5ea642630609c203009df631feb7b480dd2ac", + "2102ba2685ad1fa5891cb100f1656b2ce3801822ccb9bac0336734a6f8c1b93ebbc0ac", + ], + ); + + let desc_key = (xprv, path.clone()).into_descriptor_key().unwrap(); + check( + descriptor!(pkh(desc_key)), + false, + false, + &[ + "muvBdsVpJxpFuTHMKA47htJPdCvdt4F9DP", + "mxQSHK7DL2t1DN3xFxov1janCoXSSkrSPj", + "mfz43r15GiWo4nizmyzMNubsnkDpByFFAn", + ], + ); + + let path2 = bip32::DerivationPath::from_str("m/2147483647'/0").unwrap(); + let desc_key1 = (xprv, path).into_descriptor_key().unwrap(); + let desc_key2 = (xprv, path2).into_descriptor_key().unwrap(); + + check( + descriptor!(sh(multi(1, desc_key1, desc_key2))), + false, + false, + &[ + "2MtMDXsfwefZkEEhVViEPidvcKRUtJamJJ8", + "2MwAUZ1NYyWjhVvGTethFL6n7nZhS8WE6At", + "2MuT6Bj66HLwZd7s4SoD8XbK4GwriKEA6Gr", + ], + ); + } + + #[test] + fn test_bip32_segwitv0_descriptors() { + let xprv = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); + + let path = bip32::DerivationPath::from_str("m/0").unwrap(); + let desc_key = (xprv, path.clone()).into_descriptor_key().unwrap(); + check( + descriptor!(wpkh(desc_key)), + true, + false, + &[ + "bcrt1qnhm8w9fhc8cxzgqsmqdf9fyjccyvc0gltnymu0", + "bcrt1qhylfd55rn75w9fj06zspctad5w4hz33rf0ttad", + "bcrt1qq5sq3a6k9av9d8cne0k9wcldy4nqey5yt6889r", + ], + ); + + let desc_key = (xprv, path.clone()).into_descriptor_key().unwrap(); + check( + descriptor!(sh(wpkh(desc_key))), + true, + false, + &[ + "2MxvjQCaLqZ5QxZ7XotZDQ63hZw3NPss763", + "2NDUoevN4QMzhvHDMGhKuiT2fN9HXbFRMwn", + "2NF4BEAY2jF1Fu8vqfN3NVKoFtom77pUxrx", + ], + ); + + let path2 = bip32::DerivationPath::from_str("m/2147483647'/0").unwrap(); + let desc_key1 = (xprv, path.clone()).into_descriptor_key().unwrap(); + let desc_key2 = (xprv, path2.clone()).into_descriptor_key().unwrap(); + check( + descriptor!(wsh(multi(1, desc_key1, desc_key2))), + true, + false, + &[ + "bcrt1qfxv8mxmlv5sz8q2mnuyaqdfe9jr4vvmx0csjhn092p6f4qfygfkq2hng49", + "bcrt1qerj85g243e6jlcdxpmn9spk0gefcwvu7nw7ee059d5ydzpdhkm2qwfkf5k", + "bcrt1qxkl2qss3k58q9ktc8e89pwr4gnptfpw4hju4xstxcjc0hkcae3jstluty7", + ], + ); + + let desc_key1 = (xprv, path).into_descriptor_key().unwrap(); + let desc_key2 = (xprv, path2).into_descriptor_key().unwrap(); + check( + descriptor!(sh(wsh(multi(1, desc_key1, desc_key2)))), + true, + false, + &[ + "2NFCtXvx9q4ci2kvKub17iSTgvRXGctCGhz", + "2NB2PrFPv5NxWCpygas8tPrGJG2ZFgeuwJw", + "2N79ZAGo5cMi5Jt7Wo9L5YmF5GkEw7sjWdC", + ], + ); + } + + #[test] + fn test_dsl_sortedmulti() { + let key_1 = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); + let path_1 = bip32::DerivationPath::from_str("m/0").unwrap(); + + let key_2 = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPegBHHnq7YEgM815dG24M2Jk5RVqipgDxF1HJ1tsnT815X5Fd5FRfMVUs8NZs9XCb6y9an8hRPThnhfwfXJ36intaekySHGF").unwrap(); + let path_2 = bip32::DerivationPath::from_str("m/1").unwrap(); + + let desc_key1 = (key_1, path_1); + let desc_key2 = (key_2, path_2); + + check( + descriptor!(sh(sortedmulti(1, desc_key1.clone(), desc_key2.clone()))), + false, + false, + &[ + "2MsxzPEJDBzpGffJXPaDpfXZAUNnZhaMh2N", + "2My3x3DLPK3UbGWGpxrXr1RnbD8MNC4FpgS", + "2NByEuiQT7YLqHCTNxL5KwYjvtuCYcXNBSC", + "2N1TGbP81kj2VUKTSWgrwxoMfuWjvfUdyu7", + "2N3Bomq2fpAcLRNfZnD3bCWK9quan28CxCR", + "2N9nrZaEzEFDqEAU9RPvDnXGT6AVwBDKAQb", + ], + ); + + check( + descriptor!(sh(wsh(sortedmulti( + 1, + desc_key1.clone(), + desc_key2.clone() + )))), + true, + false, + &[ + "2NCogc5YyM4N6ruv1hUa7WLMW1BPeCK7N9B", + "2N6mkSAKi1V2oaBXby7XHdvBMKEDRQcFpNe", + "2NFmTSttm9v6bXeoWaBvpMcgfPQcZhNn3Eh", + "2Mvib87RBPUHXNEpX5S5Kv1qqrhBfgBGsJM", + "2MtMv5mcK2EjcLsH8Txpx2JxLLzHr4ttczL", + "2MsWCB56rb4T6yPv8QudZGHERTwNgesE4f6", + ], + ); + + check( + descriptor!(wsh(sortedmulti_vec(1, vec![desc_key1, desc_key2]))), + true, + false, + &[ + "bcrt1qcvq0lg8q7a47ytrd7zk5y7uls7mulrenjgvflwylpppgwf8029es4vhpnj", + "bcrt1q80yn8sdt6l7pjvkz25lglyaqctlmsq9ugk80rmxt8yu0npdsj97sc7l4de", + "bcrt1qrvf6024v9s50qhffe3t2fr2q9ckdhx2g6jz32chm2pp24ymgtr5qfrdmct", + "bcrt1q6srfmra0ynypym35c7jvsxt2u4yrugeajq95kg2ps7lk6h2gaunsq9lzxn", + "bcrt1qhl8rrzzcdpu7tcup3lcg7tge52sqvwy5fcv4k78v6kxtwmqf3v6qpvyjza", + "bcrt1ql2elz9mhm9ll27ddpewhxs732xyl2fk2kpkqz9gdyh33wgcun4vstrd49k", + ], + ); + } + + // - verify the valid_networks returned is correctly computed based on the keys present in the descriptor + #[test] + fn test_valid_networks() { + let xprv = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); + let path = bip32::DerivationPath::from_str("m/0").unwrap(); + let desc_key = (xprv, path).into_descriptor_key().unwrap(); + + let (_desc, _key_map, valid_networks) = descriptor!(pkh(desc_key)).unwrap(); + assert_eq!( + valid_networks, + [Testnet, Regtest, Signet].iter().cloned().collect() + ); + + let xprv = bip32::ExtendedPrivKey::from_str("xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi").unwrap(); + let path = bip32::DerivationPath::from_str("m/10/20/30/40").unwrap(); + let desc_key = (xprv, path).into_descriptor_key().unwrap(); + + let (_desc, _key_map, valid_networks) = descriptor!(wpkh(desc_key)).unwrap(); + assert_eq!(valid_networks, [Bitcoin].iter().cloned().collect()); + } + + // - verify the key_maps are correctly merged together + #[test] + fn test_key_maps_merged() { + let secp = Secp256k1::new(); + + let xprv1 = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); + let path1 = bip32::DerivationPath::from_str("m/0").unwrap(); + let desc_key1 = (xprv1, path1.clone()).into_descriptor_key().unwrap(); + + let xprv2 = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPegBHHnq7YEgM815dG24M2Jk5RVqipgDxF1HJ1tsnT815X5Fd5FRfMVUs8NZs9XCb6y9an8hRPThnhfwfXJ36intaekySHGF").unwrap(); + let path2 = bip32::DerivationPath::from_str("m/2147483647'/0").unwrap(); + let desc_key2 = (xprv2, path2.clone()).into_descriptor_key().unwrap(); + + let xprv3 = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPdZXrcHNLf5JAJWFAoJ2TrstMRdSKtEggz6PddbuSkvHKM9oKJyFgZV1B7rw8oChspxyYbtmEXYyg1AjfWbL3ho3XHDpHRZf").unwrap(); + let path3 = bip32::DerivationPath::from_str("m/10/20/30/40").unwrap(); + let desc_key3 = (xprv3, path3.clone()).into_descriptor_key().unwrap(); + + let (_desc, key_map, _valid_networks) = + descriptor!(sh(wsh(multi(2, desc_key1, desc_key2, desc_key3)))).unwrap(); + assert_eq!(key_map.len(), 3); + + let desc_key1: DescriptorKey = (xprv1, path1).into_descriptor_key().unwrap(); + let desc_key2: DescriptorKey = (xprv2, path2).into_descriptor_key().unwrap(); + let desc_key3: DescriptorKey = (xprv3, path3).into_descriptor_key().unwrap(); + + let (key1, _key_map, _valid_networks) = desc_key1.extract(&secp).unwrap(); + let (key2, _key_map, _valid_networks) = desc_key2.extract(&secp).unwrap(); + let (key3, _key_map, _valid_networks) = desc_key3.extract(&secp).unwrap(); + assert_eq!(key_map.get(&key1).unwrap().to_string(), "tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy/0/*"); + assert_eq!(key_map.get(&key2).unwrap().to_string(), "tprv8ZgxMBicQKsPegBHHnq7YEgM815dG24M2Jk5RVqipgDxF1HJ1tsnT815X5Fd5FRfMVUs8NZs9XCb6y9an8hRPThnhfwfXJ36intaekySHGF/2147483647'/0/*"); + assert_eq!(key_map.get(&key3).unwrap().to_string(), "tprv8ZgxMBicQKsPdZXrcHNLf5JAJWFAoJ2TrstMRdSKtEggz6PddbuSkvHKM9oKJyFgZV1B7rw8oChspxyYbtmEXYyg1AjfWbL3ho3XHDpHRZf/10/20/30/40/*"); + } + + // - verify the ScriptContext is correctly validated (i.e. passing a type that only impl IntoDescriptorKey to a pkh() descriptor should throw a compilation error + #[test] + fn test_script_context_validation() { + // this compiles + let xprv = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); + let path = bip32::DerivationPath::from_str("m/0").unwrap(); + let desc_key: DescriptorKey = (xprv, path).into_descriptor_key().unwrap(); + + let (desc, _key_map, _valid_networks) = descriptor!(pkh(desc_key)).unwrap(); + assert_eq!(desc.to_string(), "pkh(tpubD6NzVbkrYhZ4WR7a4vY1VT3khMJMeAxVsfq9TBJyJWrNk247zCJtV7AWf6UJP7rAVsn8NNKdJi3gFyKPTmWZS9iukb91xbn2HbFSMQm2igY/0/*)#yrnz9pp2"); + + // as expected this does not compile due to invalid context + //let desc_key:DescriptorKey = (xprv, path.clone()).into_descriptor_key().unwrap(); + //let (desc, _key_map, _valid_networks) = descriptor!(pkh(desc_key)).unwrap(); + } + + #[test] + fn test_dsl_modifiers() { + let private_key = + PrivateKey::from_wif("cSQPHDBwXGjVzWRqAHm6zfvQhaTuj1f2bFH58h55ghbjtFwvmeXR").unwrap(); + let (descriptor, _, _) = + descriptor!(wsh(thresh(2,n:d:v:older(1),s:pk(private_key),s:pk(private_key)))).unwrap(); + + assert_eq!(descriptor.to_string(), "wsh(thresh(2,ndv:older(1),s:pk(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c),s:pk(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c)))#zzk3ux8g") + } + + #[test] + #[should_panic( + expected = "Miniscript(ContextError(CompressedOnly(\"04b4632d08485ff1df2db55b9dafd23347d1c47a457072a1e87be26896549a87378ec38ff91d43e8c2092ebda601780485263da089465619e0358a5c1be7ac91f4\")))" + )] + fn test_dsl_miniscript_checks() { + let mut uncompressed_pk = + PrivateKey::from_wif("L5EZftvrYaSudiozVRzTqLcHLNDoVn7H5HSfM9BAN6tMJX8oTWz6").unwrap(); + uncompressed_pk.compressed = false; + + descriptor!(wsh(v: pk(uncompressed_pk))).unwrap(); + } + + #[test] + fn test_dsl_tr_only_key() { + let private_key = + PrivateKey::from_wif("cSQPHDBwXGjVzWRqAHm6zfvQhaTuj1f2bFH58h55ghbjtFwvmeXR").unwrap(); + let (descriptor, _, _) = descriptor!(tr(private_key)).unwrap(); + + assert_eq!( + descriptor.to_string(), + "tr(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c)#heq9m95v" + ) + } + + #[test] + fn test_dsl_tr_simple_tree() { + let private_key = + PrivateKey::from_wif("cSQPHDBwXGjVzWRqAHm6zfvQhaTuj1f2bFH58h55ghbjtFwvmeXR").unwrap(); + let (descriptor, _, _) = + descriptor!(tr(private_key, { pk(private_key), pk(private_key) })).unwrap(); + + assert_eq!(descriptor.to_string(), "tr(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c,{pk(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c),pk(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c)})#xy5fjw6d") + } + + #[test] + fn test_dsl_tr_single_leaf() { + let private_key = + PrivateKey::from_wif("cSQPHDBwXGjVzWRqAHm6zfvQhaTuj1f2bFH58h55ghbjtFwvmeXR").unwrap(); + let (descriptor, _, _) = descriptor!(tr(private_key, pk(private_key))).unwrap(); + + assert_eq!(descriptor.to_string(), "tr(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c,pk(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c))#lzl2vmc7") + } +} diff --git a/crates/bdk/src/descriptor/error.rs b/crates/bdk/src/descriptor/error.rs new file mode 100644 index 00000000..83efb01a --- /dev/null +++ b/crates/bdk/src/descriptor/error.rs @@ -0,0 +1,87 @@ +// Bitcoin Dev Kit +// Written in 2020 by Alekos Filini +// +// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Descriptor errors + +/// Errors related to the parsing and usage of descriptors +#[derive(Debug)] +pub enum Error { + /// Invalid HD Key path, such as having a wildcard but a length != 1 + InvalidHdKeyPath, + /// The provided descriptor doesn't match its checksum + InvalidDescriptorChecksum, + /// The descriptor contains hardened derivation steps on public extended keys + HardenedDerivationXpub, + + /// Error thrown while working with [`keys`](crate::keys) + Key(crate::keys::KeyError), + /// Error while extracting and manipulating policies + Policy(crate::descriptor::policy::PolicyError), + + /// Invalid byte found in the descriptor checksum + InvalidDescriptorCharacter(u8), + + /// BIP32 error + Bip32(bitcoin::util::bip32::Error), + /// Error during base58 decoding + Base58(bitcoin::util::base58::Error), + /// Key-related error + Pk(bitcoin::util::key::Error), + /// Miniscript error + Miniscript(miniscript::Error), + /// Hex decoding error + Hex(bitcoin::hashes::hex::Error), +} + +impl From for Error { + fn from(key_error: crate::keys::KeyError) -> Error { + match key_error { + crate::keys::KeyError::Miniscript(inner) => Error::Miniscript(inner), + crate::keys::KeyError::Bip32(inner) => Error::Bip32(inner), + e => Error::Key(e), + } + } +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::InvalidHdKeyPath => write!(f, "Invalid HD key path"), + Self::InvalidDescriptorChecksum => { + write!(f, "The provided descriptor doesn't match its checksum") + } + Self::HardenedDerivationXpub => write!( + f, + "The descriptor contains hardened derivation steps on public extended keys" + ), + Self::Key(err) => write!(f, "Key error: {}", err), + Self::Policy(err) => write!(f, "Policy error: {}", err), + Self::InvalidDescriptorCharacter(char) => { + write!(f, "Invalid descriptor character: {}", char) + } + Self::Bip32(err) => write!(f, "BIP32 error: {}", err), + Self::Base58(err) => write!(f, "Base58 error: {}", err), + Self::Pk(err) => write!(f, "Key-related error: {}", err), + Self::Miniscript(err) => write!(f, "Miniscript error: {}", err), + Self::Hex(err) => write!(f, "Hex decoding error: {}", err), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for Error {} + +impl_error!(bitcoin::util::bip32::Error, Bip32); +impl_error!(bitcoin::util::base58::Error, Base58); +impl_error!(bitcoin::util::key::Error, Pk); +impl_error!(miniscript::Error, Miniscript); +impl_error!(bitcoin::hashes::hex::Error, Hex); +impl_error!(crate::descriptor::policy::PolicyError, Policy); diff --git a/crates/bdk/src/descriptor/mod.rs b/crates/bdk/src/descriptor/mod.rs new file mode 100644 index 00000000..0a747466 --- /dev/null +++ b/crates/bdk/src/descriptor/mod.rs @@ -0,0 +1,876 @@ +// Bitcoin Dev Kit +// Written in 2020 by Alekos Filini +// +// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Descriptors +//! +//! This module contains generic utilities to work with descriptors, plus some re-exported types +//! from [`miniscript`]. + +use crate::collections::BTreeMap; +use alloc::string::String; +use alloc::vec::Vec; + +use bitcoin::util::bip32::{ChildNumber, DerivationPath, ExtendedPubKey, Fingerprint, KeySource}; +use bitcoin::util::{psbt, taproot}; +use bitcoin::{secp256k1, PublicKey, XOnlyPublicKey}; +use bitcoin::{Network, TxOut}; + +use miniscript::descriptor::{ + DefiniteDescriptorKey, DescriptorSecretKey, DescriptorType, InnerXKey, SinglePubKey, +}; +pub use miniscript::{ + descriptor::DescriptorXKey, descriptor::KeyMap, descriptor::Wildcard, Descriptor, + DescriptorPublicKey, Legacy, Miniscript, ScriptContext, Segwitv0, +}; +use miniscript::{ForEachKey, MiniscriptKey, TranslatePk}; + +use crate::descriptor::policy::BuildSatisfaction; + +pub mod checksum; +#[doc(hidden)] +pub mod dsl; +pub mod error; +pub mod policy; +pub mod template; + +pub use self::checksum::calc_checksum; +use self::checksum::calc_checksum_bytes; +pub use self::error::Error as DescriptorError; +pub use self::policy::Policy; +use self::template::DescriptorTemplateOut; +use crate::keys::{IntoDescriptorKey, KeyError}; +use crate::wallet::signer::SignersContainer; +use crate::wallet::utils::SecpCtx; + +/// Alias for a [`Descriptor`] that can contain extended keys using [`DescriptorPublicKey`] +pub type ExtendedDescriptor = Descriptor; + +/// Alias for a [`Descriptor`] that contains extended **derived** keys +pub type DerivedDescriptor = Descriptor; + +/// Alias for the type of maps that represent derivation paths in a [`psbt::Input`] or +/// [`psbt::Output`] +/// +/// [`psbt::Input`]: bitcoin::util::psbt::Input +/// [`psbt::Output`]: bitcoin::util::psbt::Output +pub type HdKeyPaths = BTreeMap; + +/// Alias for the type of maps that represent taproot key origins in a [`psbt::Input`] or +/// [`psbt::Output`] +/// +/// [`psbt::Input`]: bitcoin::util::psbt::Input +/// [`psbt::Output`]: bitcoin::util::psbt::Output +pub type TapKeyOrigins = BTreeMap, KeySource)>; + +/// Trait for types which can be converted into an [`ExtendedDescriptor`] and a [`KeyMap`] usable by a wallet in a specific [`Network`] +pub trait IntoWalletDescriptor { + /// Convert to wallet descriptor + fn into_wallet_descriptor( + self, + secp: &SecpCtx, + network: Network, + ) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError>; +} + +impl IntoWalletDescriptor for &str { + fn into_wallet_descriptor( + self, + secp: &SecpCtx, + network: Network, + ) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> { + let descriptor = match self.split_once('#') { + Some((desc, original_checksum)) => { + let checksum = calc_checksum_bytes(desc)?; + if original_checksum.as_bytes() != checksum { + return Err(DescriptorError::InvalidDescriptorChecksum); + } + desc + } + None => self, + }; + + ExtendedDescriptor::parse_descriptor(secp, descriptor)? + .into_wallet_descriptor(secp, network) + } +} + +impl IntoWalletDescriptor for &String { + fn into_wallet_descriptor( + self, + secp: &SecpCtx, + network: Network, + ) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> { + self.as_str().into_wallet_descriptor(secp, network) + } +} + +impl IntoWalletDescriptor for ExtendedDescriptor { + fn into_wallet_descriptor( + self, + secp: &SecpCtx, + network: Network, + ) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> { + (self, KeyMap::default()).into_wallet_descriptor(secp, network) + } +} + +impl IntoWalletDescriptor for (ExtendedDescriptor, KeyMap) { + fn into_wallet_descriptor( + self, + secp: &SecpCtx, + network: Network, + ) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> { + use crate::keys::DescriptorKey; + + struct Translator<'s, 'd> { + secp: &'s SecpCtx, + descriptor: &'d ExtendedDescriptor, + network: Network, + } + + impl<'s, 'd> + miniscript::Translator + for Translator<'s, 'd> + { + fn pk( + &mut self, + pk: &DescriptorPublicKey, + ) -> Result { + let secp = &self.secp; + + let (_, _, networks) = if self.descriptor.is_taproot() { + let descriptor_key: DescriptorKey = + pk.clone().into_descriptor_key()?; + descriptor_key.extract(secp)? + } else if self.descriptor.is_witness() { + let descriptor_key: DescriptorKey = + pk.clone().into_descriptor_key()?; + descriptor_key.extract(secp)? + } else { + let descriptor_key: DescriptorKey = + pk.clone().into_descriptor_key()?; + descriptor_key.extract(secp)? + }; + + if networks.contains(&self.network) { + Ok(miniscript::DummyKey) + } else { + Err(DescriptorError::Key(KeyError::InvalidNetwork)) + } + } + fn sha256( + &mut self, + _sha256: &::Sha256, + ) -> Result { + Ok(Default::default()) + } + fn hash256( + &mut self, + _hash256: &::Hash256, + ) -> Result { + Ok(Default::default()) + } + fn ripemd160( + &mut self, + _ripemd160: &::Ripemd160, + ) -> Result { + Ok(Default::default()) + } + fn hash160( + &mut self, + _hash160: &::Hash160, + ) -> Result { + Ok(Default::default()) + } + } + + // check the network for the keys + self.0.translate_pk(&mut Translator { + secp, + network, + descriptor: &self.0, + })?; + + Ok(self) + } +} + +impl IntoWalletDescriptor for DescriptorTemplateOut { + fn into_wallet_descriptor( + self, + _secp: &SecpCtx, + network: Network, + ) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> { + struct Translator { + network: Network, + } + + impl miniscript::Translator + for Translator + { + fn pk( + &mut self, + pk: &DescriptorPublicKey, + ) -> Result { + // workaround for xpubs generated by other key types, like bip39: since when the + // conversion is made one network has to be chosen, what we generally choose + // "mainnet", but then override the set of valid networks to specify that all of + // them are valid. here we reset the network to make sure the wallet struct gets a + // descriptor with the right network everywhere. + let pk = match pk { + DescriptorPublicKey::XPub(ref xpub) => { + let mut xpub = xpub.clone(); + xpub.xkey.network = self.network; + + DescriptorPublicKey::XPub(xpub) + } + other => other.clone(), + }; + + Ok(pk) + } + miniscript::translate_hash_clone!( + DescriptorPublicKey, + DescriptorPublicKey, + DescriptorError + ); + } + + let (desc, keymap, networks) = self; + + if !networks.contains(&network) { + return Err(DescriptorError::Key(KeyError::InvalidNetwork)); + } + + // fixup the network for keys that need it in the descriptor + let translated = desc.translate_pk(&mut Translator { network })?; + // ...and in the key map + let fixed_keymap = keymap + .into_iter() + .map(|(mut k, mut v)| { + match (&mut k, &mut v) { + (DescriptorPublicKey::XPub(xpub), DescriptorSecretKey::XPrv(xprv)) => { + xpub.xkey.network = network; + xprv.xkey.network = network; + } + (_, DescriptorSecretKey::Single(key)) => { + key.key.network = network; + } + _ => {} + } + + (k, v) + }) + .collect(); + + Ok((translated, fixed_keymap)) + } +} + +/// Wrapper for `IntoWalletDescriptor` that performs additional checks on the keys contained in the +/// descriptor +pub(crate) fn into_wallet_descriptor_checked( + inner: T, + secp: &SecpCtx, + network: Network, +) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> { + let (descriptor, keymap) = inner.into_wallet_descriptor(secp, network)?; + + // Ensure the keys don't contain any hardened derivation steps or hardened wildcards + let descriptor_contains_hardened_steps = descriptor.for_any_key(|k| { + if let DescriptorPublicKey::XPub(DescriptorXKey { + derivation_path, + wildcard, + .. + }) = k + { + return *wildcard == Wildcard::Hardened + || derivation_path.into_iter().any(ChildNumber::is_hardened); + } + + false + }); + if descriptor_contains_hardened_steps { + return Err(DescriptorError::HardenedDerivationXpub); + } + + // Run miniscript's sanity check, which will look for duplicated keys and other potential + // issues + descriptor.sanity_check()?; + + Ok((descriptor, keymap)) +} + +#[doc(hidden)] +/// Used internally mainly by the `descriptor!()` and `fragment!()` macros +pub trait CheckMiniscript { + fn check_miniscript(&self) -> Result<(), miniscript::Error>; +} + +impl CheckMiniscript + for miniscript::Miniscript +{ + fn check_miniscript(&self) -> Result<(), miniscript::Error> { + Ctx::check_global_validity(self)?; + + Ok(()) + } +} + +/// Trait implemented on [`Descriptor`]s to add a method to extract the spending [`policy`] +pub trait ExtractPolicy { + /// Extract the spending [`policy`] + fn extract_policy( + &self, + signers: &SignersContainer, + psbt: BuildSatisfaction, + secp: &SecpCtx, + ) -> Result, DescriptorError>; +} + +pub(crate) trait XKeyUtils { + fn root_fingerprint(&self, secp: &SecpCtx) -> Fingerprint; +} + +impl XKeyUtils for DescriptorXKey +where + T: InnerXKey, +{ + fn root_fingerprint(&self, secp: &SecpCtx) -> Fingerprint { + match self.origin { + Some((fingerprint, _)) => fingerprint, + None => self.xkey.xkey_fingerprint(secp), + } + } +} + +pub(crate) trait DescriptorMeta { + fn is_witness(&self) -> bool; + fn is_taproot(&self) -> bool; + fn get_extended_keys(&self) -> Vec>; + fn derive_from_hd_keypaths<'s>( + &self, + hd_keypaths: &HdKeyPaths, + secp: &'s SecpCtx, + ) -> Option; + fn derive_from_tap_key_origins<'s>( + &self, + tap_key_origins: &TapKeyOrigins, + secp: &'s SecpCtx, + ) -> Option; + fn derive_from_psbt_key_origins<'s>( + &self, + key_origins: BTreeMap, + secp: &'s SecpCtx, + ) -> Option; + fn derive_from_psbt_input<'s>( + &self, + psbt_input: &psbt::Input, + utxo: Option, + secp: &'s SecpCtx, + ) -> Option; +} + +impl DescriptorMeta for ExtendedDescriptor { + fn is_witness(&self) -> bool { + matches!( + self.desc_type(), + DescriptorType::Wpkh + | DescriptorType::ShWpkh + | DescriptorType::Wsh + | DescriptorType::ShWsh + | DescriptorType::ShWshSortedMulti + | DescriptorType::WshSortedMulti + ) + } + + fn is_taproot(&self) -> bool { + self.desc_type() == DescriptorType::Tr + } + + fn get_extended_keys(&self) -> Vec> { + let mut answer = Vec::new(); + + self.for_each_key(|pk| { + if let DescriptorPublicKey::XPub(xpub) = pk { + answer.push(xpub.clone()); + } + + true + }); + + answer + } + + fn derive_from_psbt_key_origins<'s>( + &self, + key_origins: BTreeMap, + secp: &'s SecpCtx, + ) -> Option { + // Ensure that deriving `xpub` with `path` yields `expected` + let verify_key = |xpub: &DescriptorXKey, + path: &DerivationPath, + expected: &SinglePubKey| { + let derived = xpub + .xkey + .derive_pub(secp, path) + .expect("The path should never contain hardened derivation steps") + .public_key; + + match expected { + SinglePubKey::FullKey(pk) if &PublicKey::new(derived) == pk => true, + SinglePubKey::XOnly(pk) if &XOnlyPublicKey::from(derived) == pk => true, + _ => false, + } + }; + + let mut path_found = None; + + // using `for_any_key` should make this stop as soon as we return `true` + self.for_any_key(|key| { + if let DescriptorPublicKey::XPub(xpub) = key { + // Check if the key matches one entry in our `key_origins`. If it does, `matches()` will + // return the "prefix" that matched, so we remove that prefix from the full path + // found in `key_origins` and save it in `derive_path`. We expect this to be a derivation + // path of length 1 if the key is `wildcard` and an empty path otherwise. + let root_fingerprint = xpub.root_fingerprint(secp); + let derive_path = key_origins + .get_key_value(&root_fingerprint) + .and_then(|(fingerprint, (path, expected))| { + xpub.matches(&(*fingerprint, (*path).clone()), secp) + .zip(Some((path, expected))) + }) + .and_then(|(prefix, (full_path, expected))| { + let derive_path = full_path + .into_iter() + .skip(prefix.into_iter().count()) + .cloned() + .collect::(); + + // `derive_path` only contains the replacement index for the wildcard, if present, or + // an empty path for fixed descriptors. To verify the key we also need the normal steps + // that come before the wildcard, so we take them directly from `xpub` and then append + // the final index + if verify_key( + xpub, + &xpub.derivation_path.extend(derive_path.clone()), + expected, + ) { + Some(derive_path) + } else { + log::debug!( + "Key `{}` derived with {} yields an unexpected key", + root_fingerprint, + derive_path + ); + None + } + }); + + match derive_path { + Some(path) if xpub.wildcard != Wildcard::None && path.len() == 1 => { + // Ignore hardened wildcards + if let ChildNumber::Normal { index } = path[0] { + path_found = Some(index); + return true; + } + } + Some(path) if xpub.wildcard == Wildcard::None && path.is_empty() => { + path_found = Some(0); + return true; + } + _ => {} + } + } + + false + }); + + path_found.map(|path| self.at_derivation_index(path)) + } + + fn derive_from_hd_keypaths<'s>( + &self, + hd_keypaths: &HdKeyPaths, + secp: &'s SecpCtx, + ) -> Option { + // "Convert" an hd_keypaths map to the format required by `derive_from_psbt_key_origins` + let key_origins = hd_keypaths + .iter() + .map(|(pk, (fingerprint, path))| { + ( + *fingerprint, + (path, SinglePubKey::FullKey(PublicKey::new(*pk))), + ) + }) + .collect(); + self.derive_from_psbt_key_origins(key_origins, secp) + } + + fn derive_from_tap_key_origins<'s>( + &self, + tap_key_origins: &TapKeyOrigins, + secp: &'s SecpCtx, + ) -> Option { + // "Convert" a tap_key_origins map to the format required by `derive_from_psbt_key_origins` + let key_origins = tap_key_origins + .iter() + .map(|(pk, (_, (fingerprint, path)))| (*fingerprint, (path, SinglePubKey::XOnly(*pk)))) + .collect(); + self.derive_from_psbt_key_origins(key_origins, secp) + } + + fn derive_from_psbt_input<'s>( + &self, + psbt_input: &psbt::Input, + utxo: Option, + secp: &'s SecpCtx, + ) -> Option { + if let Some(derived) = self.derive_from_hd_keypaths(&psbt_input.bip32_derivation, secp) { + return Some(derived); + } + if let Some(derived) = self.derive_from_tap_key_origins(&psbt_input.tap_key_origins, secp) { + return Some(derived); + } + if self.has_wildcard() { + // We can't try to bruteforce the derivation index, exit here + return None; + } + + let descriptor = self.at_derivation_index(0); + match descriptor.desc_type() { + // TODO: add pk() here + DescriptorType::Pkh + | DescriptorType::Wpkh + | DescriptorType::ShWpkh + | DescriptorType::Tr + if utxo.is_some() + && descriptor.script_pubkey() == utxo.as_ref().unwrap().script_pubkey => + { + Some(descriptor) + } + DescriptorType::Bare | DescriptorType::Sh | DescriptorType::ShSortedMulti + if psbt_input.redeem_script.is_some() + && &descriptor.explicit_script().unwrap() + == psbt_input.redeem_script.as_ref().unwrap() => + { + Some(descriptor) + } + DescriptorType::Wsh + | DescriptorType::ShWsh + | DescriptorType::ShWshSortedMulti + | DescriptorType::WshSortedMulti + if psbt_input.witness_script.is_some() + && &descriptor.explicit_script().unwrap() + == psbt_input.witness_script.as_ref().unwrap() => + { + Some(descriptor) + } + _ => None, + } + } +} + +#[cfg(test)] +mod test { + use alloc::string::ToString; + use core::str::FromStr; + + use assert_matches::assert_matches; + use bitcoin::consensus::encode::deserialize; + use bitcoin::hashes::hex::FromHex; + use bitcoin::secp256k1::Secp256k1; + use bitcoin::util::{bip32, psbt}; + use bitcoin::Script; + + use super::*; + use crate::psbt::PsbtUtils; + + #[test] + fn test_derive_from_psbt_input_wpkh_wif() { + let descriptor = Descriptor::::from_str( + "wpkh(02b4632d08485ff1df2db55b9dafd23347d1c47a457072a1e87be26896549a8737)", + ) + .unwrap(); + let psbt: psbt::PartiallySignedTransaction = deserialize( + &Vec::::from_hex( + "70736274ff010052010000000162307be8e431fbaff807cdf9cdc3fde44d7402\ + 11bc8342c31ffd6ec11fe35bcc0100000000ffffffff01328601000000000016\ + 001493ce48570b55c42c2af816aeaba06cfee1224fae000000000001011fa086\ + 01000000000016001493ce48570b55c42c2af816aeaba06cfee1224fae010304\ + 010000000000", + ) + .unwrap(), + ) + .unwrap(); + + assert!(descriptor + .derive_from_psbt_input(&psbt.inputs[0], psbt.get_utxo_for(0), &Secp256k1::new()) + .is_some()); + } + + #[test] + fn test_derive_from_psbt_input_pkh_tpub() { + let descriptor = Descriptor::::from_str( + "pkh([0f056943/44h/0h/0h]tpubDDpWvmUrPZrhSPmUzCMBHffvC3HyMAPnWDSAQNBTnj1iZeJa7BZQEttFiP4DS4GCcXQHezdXhn86Hj6LHX5EDstXPWrMaSneRWM8yUf6NFd/10/*)", + ) + .unwrap(); + let psbt: psbt::PartiallySignedTransaction = deserialize( + &Vec::::from_hex( + "70736274ff010053010000000145843b86be54a3cd8c9e38444e1162676c00df\ + e7964122a70df491ea12fd67090100000000ffffffff01c19598000000000017\ + a91432bb94283282f72b2e034709e348c44d5a4db0ef8700000000000100f902\ + 0000000001010167e99c0eb67640f3a1b6805f2d8be8238c947f8aaf49eb0a9c\ + bee6a42c984200000000171600142b29a22019cca05b9c2b2d283a4c4489e1cf\ + 9f8ffeffffff02a01dced06100000017a914e2abf033cadbd74f0f4c74946201\ + decd20d5c43c8780969800000000001976a9148b0fce5fb1264e599a65387313\ + 3c95478b902eb288ac02473044022015d9211576163fa5b001e84dfa3d44efd9\ + 86b8f3a0d3d2174369288b2b750906022048dacc0e5d73ae42512fd2b97e2071\ + a8d0bce443b390b1fe0b8128fe70ec919e01210232dad1c5a67dcb0116d407e2\ + 52584228ab7ec00e8b9779d0c3ffe8114fc1a7d2c80600000103040100000022\ + 0603433b83583f8c4879b329dd08bbc7da935e4cc02f637ff746e05f0466ffb2\ + a6a2180f0569432c00008000000080000000800a000000000000000000", + ) + .unwrap(), + ) + .unwrap(); + + assert!(descriptor + .derive_from_psbt_input(&psbt.inputs[0], psbt.get_utxo_for(0), &Secp256k1::new()) + .is_some()); + } + + #[test] + fn test_derive_from_psbt_input_wsh() { + let descriptor = Descriptor::::from_str( + "wsh(and_v(v:pk(03b6633fef2397a0a9de9d7b6f23aef8368a6e362b0581f0f0af70d5ecfd254b14),older(6)))", + ) + .unwrap(); + let psbt: psbt::PartiallySignedTransaction = deserialize( + &Vec::::from_hex( + "70736274ff01005302000000011c8116eea34408ab6529223c9a176606742207\ + 67a1ff1d46a6e3c4a88243ea6e01000000000600000001109698000000000017\ + a914ad105f61102e0d01d7af40d06d6a5c3ae2f7fde387000000000001012b80\ + 969800000000002200203ca72f106a72234754890ca7640c43f65d2174e44d33\ + 336030f9059345091044010304010000000105252103b6633fef2397a0a9de9d\ + 7b6f23aef8368a6e362b0581f0f0af70d5ecfd254b14ad56b20000", + ) + .unwrap(), + ) + .unwrap(); + + assert!(descriptor + .derive_from_psbt_input(&psbt.inputs[0], psbt.get_utxo_for(0), &Secp256k1::new()) + .is_some()); + } + + #[test] + fn test_derive_from_psbt_input_sh() { + let descriptor = Descriptor::::from_str( + "sh(and_v(v:pk(021403881a5587297818fcaf17d239cefca22fce84a45b3b1d23e836c4af671dbb),after(630000)))", + ) + .unwrap(); + let psbt: psbt::PartiallySignedTransaction = deserialize( + &Vec::::from_hex( + "70736274ff0100530100000001bc8c13df445dfadcc42afa6dc841f85d22b01d\ + a6270ebf981740f4b7b1d800390000000000feffffff01ba9598000000000017\ + a91457b148ba4d3e5fa8608a8657875124e3d1c9390887f09c0900000100e002\ + 0000000001016ba1bbe05cc93574a0d611ec7d93ad0ab6685b28d0cd80e8a82d\ + debb326643c90100000000feffffff02809698000000000017a914d9a6e8c455\ + 8e16c8253afe53ce37ad61cf4c38c487403504cf6100000017a9144044fb6e0b\ + 757dfc1b34886b6a95aef4d3db137e870247304402202a9b72d939bcde8ba2a1\ + e0980597e47af4f5c152a78499143c3d0a78ac2286a602207a45b1df9e93b8c9\ + 6f09f5c025fe3e413ca4b905fe65ee55d32a3276439a9b8f012102dc1fcc2636\ + 4da1aa718f03d8d9bd6f2ff410ed2cf1245a168aa3bcc995ac18e0a806000001\ + 03040100000001042821021403881a5587297818fcaf17d239cefca22fce84a4\ + 5b3b1d23e836c4af671dbbad03f09c09b10000", + ) + .unwrap(), + ) + .unwrap(); + + assert!(descriptor + .derive_from_psbt_input(&psbt.inputs[0], psbt.get_utxo_for(0), &Secp256k1::new()) + .is_some()); + } + + #[test] + fn test_to_wallet_descriptor_fixup_networks() { + use crate::keys::{any_network, IntoDescriptorKey}; + + let secp = Secp256k1::new(); + + let xprv = bip32::ExtendedPrivKey::from_str("xprv9s21ZrQH143K3c3gF1DUWpWNr2SG2XrG8oYPpqYh7hoWsJy9NjabErnzriJPpnGHyKz5NgdXmq1KVbqS1r4NXdCoKitWg5e86zqXHa8kxyB").unwrap(); + let path = bip32::DerivationPath::from_str("m/0").unwrap(); + + // here `to_descriptor_key` will set the valid networks for the key to only mainnet, since + // we are using an "xpub" + let key = (xprv, path.clone()).into_descriptor_key().unwrap(); + // override it with any. this happens in some key conversions, like bip39 + let key = key.override_valid_networks(any_network()); + + // make a descriptor out of it + let desc = crate::descriptor!(wpkh(key)).unwrap(); + // this should convert the key that supports "any_network" to the right network (testnet) + let (wallet_desc, keymap) = desc + .into_wallet_descriptor(&secp, Network::Testnet) + .unwrap(); + + let mut xprv_testnet = xprv; + xprv_testnet.network = Network::Testnet; + + let xpub_testnet = bip32::ExtendedPubKey::from_priv(&secp, &xprv_testnet); + let desc_pubkey = DescriptorPublicKey::XPub(DescriptorXKey { + xkey: xpub_testnet, + origin: None, + derivation_path: path, + wildcard: Wildcard::Unhardened, + }); + + assert_eq!(wallet_desc.to_string(), "wpkh(tpubD6NzVbkrYhZ4XtJzoDja5snUjBNQRP5B3f4Hyn1T1x6PVPxzzVjvw6nJx2D8RBCxog9GEVjZoyStfepTz7TtKoBVdkCtnc7VCJh9dD4RAU9/0/*)#a3svx0ha"); + assert_eq!( + keymap + .get(&desc_pubkey) + .map(|key| key.to_public(&secp).unwrap()), + Some(desc_pubkey) + ); + } + + // test IntoWalletDescriptor trait from &str with and without checksum appended + #[test] + fn test_descriptor_from_str_with_checksum() { + let secp = Secp256k1::new(); + + let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)#tqz0nc62" + .into_wallet_descriptor(&secp, Network::Testnet); + assert!(desc.is_ok()); + + let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)" + .into_wallet_descriptor(&secp, Network::Testnet); + assert!(desc.is_ok()); + + let desc = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/2/*)#67ju93jw" + .into_wallet_descriptor(&secp, Network::Testnet); + assert!(desc.is_ok()); + + let desc = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/2/*)" + .into_wallet_descriptor(&secp, Network::Testnet); + assert!(desc.is_ok()); + + let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)#67ju93jw" + .into_wallet_descriptor(&secp, Network::Testnet); + assert_matches!(desc, Err(DescriptorError::InvalidDescriptorChecksum)); + + let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)#67ju93jw" + .into_wallet_descriptor(&secp, Network::Testnet); + assert_matches!(desc, Err(DescriptorError::InvalidDescriptorChecksum)); + } + + // test IntoWalletDescriptor trait from &str with keys from right and wrong network + #[test] + fn test_descriptor_from_str_with_keys_network() { + let secp = Secp256k1::new(); + + let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)" + .into_wallet_descriptor(&secp, Network::Testnet); + assert!(desc.is_ok()); + + let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)" + .into_wallet_descriptor(&secp, Network::Regtest); + assert!(desc.is_ok()); + + let desc = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/2/*)" + .into_wallet_descriptor(&secp, Network::Testnet); + assert!(desc.is_ok()); + + let desc = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/2/*)" + .into_wallet_descriptor(&secp, Network::Regtest); + assert!(desc.is_ok()); + + let desc = "sh(wpkh(02864bb4ad00cefa806098a69e192bbda937494e69eb452b87bb3f20f6283baedb))" + .into_wallet_descriptor(&secp, Network::Testnet); + assert!(desc.is_ok()); + + let desc = "sh(wpkh(02864bb4ad00cefa806098a69e192bbda937494e69eb452b87bb3f20f6283baedb))" + .into_wallet_descriptor(&secp, Network::Bitcoin); + assert!(desc.is_ok()); + + let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)" + .into_wallet_descriptor(&secp, Network::Bitcoin); + assert_matches!(desc, Err(DescriptorError::Key(KeyError::InvalidNetwork))); + + let desc = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/2/*)" + .into_wallet_descriptor(&secp, Network::Bitcoin); + assert_matches!(desc, Err(DescriptorError::Key(KeyError::InvalidNetwork))); + } + + // test IntoWalletDescriptor trait from the output of the descriptor!() macro + #[test] + fn test_descriptor_from_str_from_output_of_macro() { + let secp = Secp256k1::new(); + + let tpub = bip32::ExtendedPubKey::from_str("tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK").unwrap(); + let path = bip32::DerivationPath::from_str("m/1/2").unwrap(); + let key = (tpub, path).into_descriptor_key().unwrap(); + + // make a descriptor out of it + let desc = crate::descriptor!(wpkh(key)).unwrap(); + + let (wallet_desc, _) = desc + .into_wallet_descriptor(&secp, Network::Testnet) + .unwrap(); + let wallet_desc_str = wallet_desc.to_string(); + assert_eq!(wallet_desc_str, "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/2/*)#67ju93jw"); + + let (wallet_desc2, _) = wallet_desc_str + .into_wallet_descriptor(&secp, Network::Testnet) + .unwrap(); + assert_eq!(wallet_desc, wallet_desc2) + } + + #[test] + fn test_into_wallet_descriptor_checked() { + let secp = Secp256k1::new(); + + let descriptor = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/0'/1/2/*)"; + let result = into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet); + + assert_matches!(result, Err(DescriptorError::HardenedDerivationXpub)); + + let descriptor = "wsh(multi(2,tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/0/*,tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/0/*))"; + let result = into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet); + + assert!(result.is_err()); + } + + #[test] + fn test_sh_wsh_sortedmulti_redeemscript() { + use miniscript::psbt::PsbtInputExt; + + let secp = Secp256k1::new(); + + let descriptor = "sh(wsh(sortedmulti(3,tpubDEsqS36T4DVsKJd9UH8pAKzrkGBYPLEt9jZMwpKtzh1G6mgYehfHt9WCgk7MJG5QGSFWf176KaBNoXbcuFcuadAFKxDpUdMDKGBha7bY3QM/0/*,tpubDF3cpwfs7fMvXXuoQbohXtLjNM6ehwYT287LWtmLsd4r77YLg6MZg4vTETx5MSJ2zkfigbYWu31VA2Z2Vc1cZugCYXgS7FQu6pE8V6TriEH/0/*,tpubDE1SKfcW76Tb2AASv5bQWMuScYNAdoqLHoexw13sNDXwmUhQDBbCD3QAedKGLhxMrWQdMDKENzYtnXPDRvexQPNuDrLj52wAjHhNEm8sJ4p/0/*,tpubDFLc6oXwJmhm3FGGzXkfJNTh2KitoY3WhmmQvuAjMhD8YbyWn5mAqckbxXfm2etM3p5J6JoTpSrMqRSTfMLtNW46poDaEZJ1kjd3csRSjwH/0/*,tpubDEWD9NBeWP59xXmdqSNt4VYdtTGwbpyP8WS962BuqpQeMZmX9Pur14dhXdZT5a7wR1pK6dPtZ9fP5WR493hPzemnBvkfLLYxnUjAKj1JCQV/0/*,tpubDEHyZkkwd7gZWCTgQuYQ9C4myF2hMEmyHsBCCmLssGqoqUxeT3gzohF5uEVURkf9TtmeepJgkSUmteac38FwZqirjApzNX59XSHLcwaTZCH/0/*,tpubDEqLouCekwnMUWN486kxGzD44qVgeyuqHyxUypNEiQt5RnUZNJe386TKPK99fqRV1vRkZjYAjtXGTECz98MCsdLcnkM67U6KdYRzVubeCgZ/0/*)))"; + let (descriptor, _) = + into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet).unwrap(); + + let descriptor = descriptor.at_derivation_index(0); + + let script = Script::from_str("5321022f533b667e2ea3b36e21961c9fe9dca340fbe0af5210173a83ae0337ab20a57621026bb53a98e810bd0ee61a0ed1164ba6c024786d76554e793e202dc6ce9c78c4ea2102d5b8a7d66a41ffdb6f4c53d61994022e886b4f45001fb158b95c9164d45f8ca3210324b75eead2c1f9c60e8adeb5e7009fec7a29afcdb30d829d82d09562fe8bae8521032d34f8932200833487bd294aa219dcbe000b9f9b3d824799541430009f0fa55121037468f8ea99b6c64788398b5ad25480cad08f4b0d65be54ce3a55fd206b5ae4722103f72d3d96663b0ea99b0aeb0d7f273cab11a8de37885f1dddc8d9112adb87169357ae").unwrap(); + + let mut psbt_input = psbt::Input::default(); + psbt_input + .update_with_descriptor_unchecked(&descriptor) + .unwrap(); + + assert_eq!(psbt_input.redeem_script, Some(script.to_v0_p2wsh())); + assert_eq!(psbt_input.witness_script, Some(script)); + } +} diff --git a/crates/bdk/src/descriptor/policy.rs b/crates/bdk/src/descriptor/policy.rs new file mode 100644 index 00000000..96889ffa --- /dev/null +++ b/crates/bdk/src/descriptor/policy.rs @@ -0,0 +1,1886 @@ +// Bitcoin Dev Kit +// Written in 2020 by Alekos Filini +// +// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Descriptor policy +//! +//! This module implements the logic to extract and represent the spending policies of a descriptor +//! in a more human-readable format. +//! +//! This is an **EXPERIMENTAL** feature, API and other major changes are expected. +//! +//! ## Example +//! +//! ``` +//! # use std::sync::Arc; +//! # use bdk::descriptor::*; +//! # use bdk::wallet::signer::*; +//! # use bdk::bitcoin::secp256k1::Secp256k1; +//! use bdk::descriptor::policy::BuildSatisfaction; +//! let secp = Secp256k1::new(); +//! let desc = "wsh(and_v(v:pk(cV3oCth6zxZ1UVsHLnGothsWNsaoxRhC6aeNi5VbSdFpwUkgkEci),or_d(pk(cVMTy7uebJgvFaSBwcgvwk8qn8xSLc97dKow4MBetjrrahZoimm2),older(12960))))"; +//! +//! let (extended_desc, key_map) = ExtendedDescriptor::parse_descriptor(&secp, desc)?; +//! println!("{:?}", extended_desc); +//! +//! let signers = Arc::new(SignersContainer::build(key_map, &extended_desc, &secp)); +//! let policy = extended_desc.extract_policy(&signers, BuildSatisfaction::None, &secp)?; +//! println!("policy: {}", serde_json::to_string(&policy)?); +//! # Ok::<(), bdk::Error>(()) +//! ``` + +use crate::collections::{BTreeMap, HashSet, VecDeque}; +use alloc::string::String; +use alloc::vec::Vec; +use core::cmp::max; +use core::fmt; + +use serde::ser::SerializeMap; +use serde::{Serialize, Serializer}; + +use bitcoin::hashes::{hash160, ripemd160, sha256}; +use bitcoin::util::bip32::Fingerprint; +use bitcoin::{LockTime, PublicKey, Sequence, XOnlyPublicKey}; + +use miniscript::descriptor::{ + DescriptorPublicKey, ShInner, SinglePub, SinglePubKey, SortedMultiVec, WshInner, +}; +use miniscript::hash256; +use miniscript::{ + Descriptor, Miniscript, Satisfier, ScriptContext, SigType, Terminal, ToPublicKey, +}; + +#[allow(unused_imports)] +use log::{debug, error, info, trace}; + +use crate::descriptor::ExtractPolicy; +use crate::keys::ExtScriptContext; +use crate::wallet::signer::{SignerId, SignersContainer}; +use crate::wallet::utils::{After, Older, SecpCtx}; + +use super::checksum::calc_checksum; +use super::error::Error; +use super::XKeyUtils; +use bitcoin::util::psbt::{Input as PsbtInput, PartiallySignedTransaction as Psbt}; +use miniscript::psbt::PsbtInputSatisfier; + +/// A unique identifier for a key +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum PkOrF { + /// A legacy public key + Pubkey(PublicKey), + /// A x-only public key + XOnlyPubkey(XOnlyPublicKey), + /// An extended key fingerprint + Fingerprint(Fingerprint), +} + +impl PkOrF { + fn from_key(k: &DescriptorPublicKey, secp: &SecpCtx) -> Self { + match k { + DescriptorPublicKey::Single(SinglePub { + key: SinglePubKey::FullKey(pk), + .. + }) => PkOrF::Pubkey(*pk), + DescriptorPublicKey::Single(SinglePub { + key: SinglePubKey::XOnly(pk), + .. + }) => PkOrF::XOnlyPubkey(*pk), + DescriptorPublicKey::XPub(xpub) => PkOrF::Fingerprint(xpub.root_fingerprint(secp)), + } + } +} + +/// An item that needs to be satisfied +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[serde(tag = "type", rename_all = "UPPERCASE")] +pub enum SatisfiableItem { + // Leaves + /// ECDSA Signature for a raw public key + EcdsaSignature(PkOrF), + /// Schnorr Signature for a raw public key + SchnorrSignature(PkOrF), + /// SHA256 preimage hash + Sha256Preimage { + /// The digest value + hash: sha256::Hash, + }, + /// Double SHA256 preimage hash + Hash256Preimage { + /// The digest value + hash: hash256::Hash, + }, + /// RIPEMD160 preimage hash + Ripemd160Preimage { + /// The digest value + hash: ripemd160::Hash, + }, + /// SHA256 then RIPEMD160 preimage hash + Hash160Preimage { + /// The digest value + hash: hash160::Hash, + }, + /// Absolute timeclock timestamp + AbsoluteTimelock { + /// The timelock value + value: LockTime, + }, + /// Relative timelock locktime + RelativeTimelock { + /// The timelock value + value: Sequence, + }, + /// Multi-signature public keys with threshold count + Multisig { + /// The raw public key or extended key fingerprint + keys: Vec, + /// The required threshold count + threshold: usize, + }, + + // Complex item + /// Threshold items with threshold count + Thresh { + /// The policy items + items: Vec, + /// The required threshold count + threshold: usize, + }, +} + +impl SatisfiableItem { + /// Returns whether the [`SatisfiableItem`] is a leaf item + pub fn is_leaf(&self) -> bool { + !matches!( + self, + SatisfiableItem::Thresh { + items: _, + threshold: _, + } + ) + } + + /// Returns a unique id for the [`SatisfiableItem`] + pub fn id(&self) -> String { + calc_checksum(&serde_json::to_string(self).expect("Failed to serialize a SatisfiableItem")) + .expect("Failed to compute a SatisfiableItem id") + } +} + +fn combinations(vec: &[usize], size: usize) -> Vec> { + assert!(vec.len() >= size); + + let mut answer = Vec::new(); + + let mut queue = VecDeque::new(); + for (index, val) in vec.iter().enumerate() { + let mut new_vec = Vec::with_capacity(size); + new_vec.push(*val); + queue.push_back((index, new_vec)); + } + + while let Some((index, vals)) = queue.pop_front() { + if vals.len() >= size { + answer.push(vals); + } else { + for (new_index, val) in vec.iter().skip(index + 1).enumerate() { + let mut cloned = vals.clone(); + cloned.push(*val); + queue.push_front((new_index, cloned)); + } + } + } + + answer +} + +fn mix(vec: Vec>) -> Vec> { + if vec.is_empty() || vec.iter().any(Vec::is_empty) { + return vec![]; + } + + let mut answer = Vec::new(); + let size = vec.len(); + + let mut queue = VecDeque::new(); + for i in &vec[0] { + let mut new_vec = Vec::with_capacity(size); + new_vec.push(i.clone()); + queue.push_back(new_vec); + } + + while let Some(vals) = queue.pop_front() { + if vals.len() >= size { + answer.push(vals); + } else { + let level = vals.len(); + for i in &vec[level] { + let mut cloned = vals.clone(); + cloned.push(i.clone()); + queue.push_front(cloned); + } + } + } + + answer +} + +/// Type for a map of sets of [`Condition`] items keyed by each set's index +pub type ConditionMap = BTreeMap>; +/// Type for a map of folded sets of [`Condition`] items keyed by a vector of the combined set's indexes +pub type FoldedConditionMap = BTreeMap, HashSet>; + +fn serialize_folded_cond_map( + input_map: &FoldedConditionMap, + serializer: S, +) -> Result +where + S: Serializer, +{ + let mut map = serializer.serialize_map(Some(input_map.len()))?; + for (k, v) in input_map { + let k_string = format!("{:?}", k); + map.serialize_entry(&k_string, v)?; + } + map.end() +} + +/// Represent if and how much a policy item is satisfied by the wallet's descriptor +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[serde(tag = "type", rename_all = "UPPERCASE")] +pub enum Satisfaction { + /// Only a partial satisfaction of some kind of threshold policy + Partial { + /// Total number of items + n: usize, + /// Threshold + m: usize, + /// The items that can be satisfied by the descriptor or are satisfied in the PSBT + items: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + /// Whether the items are sorted in lexicographic order (used by `sortedmulti`) + sorted: Option, + #[serde(skip_serializing_if = "BTreeMap::is_empty")] + /// Extra conditions that also need to be satisfied + conditions: ConditionMap, + }, + /// Can reach the threshold of some kind of threshold policy + PartialComplete { + /// Total number of items + n: usize, + /// Threshold + m: usize, + /// The items that can be satisfied by the descriptor + items: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + /// Whether the items are sorted in lexicographic order (used by `sortedmulti`) + sorted: Option, + #[serde( + serialize_with = "serialize_folded_cond_map", + skip_serializing_if = "BTreeMap::is_empty" + )] + /// Extra conditions that also need to be satisfied + conditions: FoldedConditionMap, + }, + + /// Can satisfy the policy item + Complete { + /// Extra conditions that also need to be satisfied + condition: Condition, + }, + /// Cannot satisfy or contribute to the policy item + None, +} + +impl Satisfaction { + /// Returns whether the [`Satisfaction`] is a leaf item + pub fn is_leaf(&self) -> bool { + match self { + Satisfaction::None | Satisfaction::Complete { .. } => true, + Satisfaction::PartialComplete { .. } | Satisfaction::Partial { .. } => false, + } + } + + // add `inner` as one of self's partial items. this only makes sense on partials + fn add(&mut self, inner: &Satisfaction, inner_index: usize) -> Result<(), PolicyError> { + match self { + Satisfaction::None | Satisfaction::Complete { .. } => Err(PolicyError::AddOnLeaf), + Satisfaction::PartialComplete { .. } => Err(PolicyError::AddOnPartialComplete), + Satisfaction::Partial { + n, + ref mut conditions, + ref mut items, + .. + } => { + if inner_index >= *n || items.contains(&inner_index) { + return Err(PolicyError::IndexOutOfRange(inner_index)); + } + + match inner { + // not relevant if not completed yet + Satisfaction::None | Satisfaction::Partial { .. } => return Ok(()), + Satisfaction::Complete { condition } => { + items.push(inner_index); + conditions.insert(inner_index, vec![*condition].into_iter().collect()); + } + Satisfaction::PartialComplete { + conditions: other_conditions, + .. + } => { + items.push(inner_index); + let conditions_set = other_conditions + .values() + .fold(HashSet::new(), |set, i| set.union(i).cloned().collect()); + conditions.insert(inner_index, conditions_set); + } + } + + Ok(()) + } + } + } + + fn finalize(&mut self) { + // if partial try to bump it to a partialcomplete + if let Satisfaction::Partial { + n, + m, + items, + conditions, + sorted, + } = self + { + if items.len() >= *m { + let mut map = BTreeMap::new(); + let indexes = combinations(items, *m); + // `indexes` at this point is a Vec>, with the "n choose k" of items (m of n) + indexes + .into_iter() + // .inspect(|x| println!("--- orig --- {:?}", x)) + // we map each of the combinations of elements into a tuple of ([chosen items], [conditions]). unfortunately, those items have potentially more than one + // condition (think about ORs), so we also use `mix` to expand those, i.e. [[0], [1, 2]] becomes [[0, 1], [0, 2]]. This is necessary to make sure that we + // consider every possible options and check whether or not they are compatible. + // since this step can turn one item of the iterator into multiple ones, we use `flat_map()` to expand them out + .flat_map(|i_vec| { + mix(i_vec + .iter() + .map(|i| { + conditions + .get(i) + .map(|set| set.clone().into_iter().collect()) + .unwrap_or_default() + }) + .collect()) + .into_iter() + .map(|x| (i_vec.clone(), x)) + .collect::, Vec)>>() + }) + // .inspect(|x| println!("flat {:?}", x)) + // try to fold all the conditions for this specific combination of indexes/options. if they are not compatible, try_fold will be Err + .map(|(key, val)| { + ( + key, + val.into_iter() + .try_fold(Condition::default(), |acc, v| acc.merge(&v)), + ) + }) + // .inspect(|x| println!("try_fold {:?}", x)) + // filter out all the incompatible combinations + .filter(|(_, val)| val.is_ok()) + // .inspect(|x| println!("filter {:?}", x)) + // push them into the map + .for_each(|(key, val)| { + map.entry(key) + .or_insert_with(HashSet::new) + .insert(val.unwrap()); + }); + // TODO: if the map is empty, the conditions are not compatible, return an error? + *self = Satisfaction::PartialComplete { + n: *n, + m: *m, + items: items.clone(), + conditions: map, + sorted: *sorted, + }; + } + } + } +} + +impl From for Satisfaction { + fn from(other: bool) -> Self { + if other { + Satisfaction::Complete { + condition: Default::default(), + } + } else { + Satisfaction::None + } + } +} + +/// Descriptor spending policy +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct Policy { + /// Identifier for this policy node + pub id: String, + + /// Type of this policy node + #[serde(flatten)] + pub item: SatisfiableItem, + /// How much a given PSBT already satisfies this policy node in terms of signatures + pub satisfaction: Satisfaction, + /// How the wallet's descriptor can satisfy this policy node + pub contribution: Satisfaction, +} + +/// An extra condition that must be satisfied but that is out of control of the user +/// TODO: use `bitcoin::LockTime` and `bitcoin::Sequence` +#[derive(Hash, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Default, Serialize)] +pub struct Condition { + /// Optional CheckSequenceVerify condition + #[serde(skip_serializing_if = "Option::is_none")] + pub csv: Option, + /// Optional timelock condition + #[serde(skip_serializing_if = "Option::is_none")] + pub timelock: Option, +} + +impl Condition { + fn merge_nlocktime(a: LockTime, b: LockTime) -> Result { + if !a.is_same_unit(b) { + Err(PolicyError::MixedTimelockUnits) + } else if a > b { + Ok(a) + } else { + Ok(b) + } + } + + fn merge_nsequence(a: Sequence, b: Sequence) -> Result { + if a.is_time_locked() != b.is_time_locked() { + Err(PolicyError::MixedTimelockUnits) + } else { + Ok(max(a, b)) + } + } + + pub(crate) fn merge(mut self, other: &Condition) -> Result { + match (self.csv, other.csv) { + (Some(a), Some(b)) => self.csv = Some(Self::merge_nsequence(a, b)?), + (None, any) => self.csv = any, + _ => {} + } + + match (self.timelock, other.timelock) { + (Some(a), Some(b)) => self.timelock = Some(Self::merge_nlocktime(a, b)?), + (None, any) => self.timelock = any, + _ => {} + } + + Ok(self) + } + + /// Returns `true` if there are no extra conditions to verify + pub fn is_null(&self) -> bool { + self.csv.is_none() && self.timelock.is_none() + } +} + +/// Errors that can happen while extracting and manipulating policies +#[derive(Debug, PartialEq, Eq)] +pub enum PolicyError { + /// Not enough items are selected to satisfy a [`SatisfiableItem::Thresh`] or a [`SatisfiableItem::Multisig`] + NotEnoughItemsSelected(String), + /// Index out of range for an item to satisfy a [`SatisfiableItem::Thresh`] or a [`SatisfiableItem::Multisig`] + IndexOutOfRange(usize), + /// Can not add to an item that is [`Satisfaction::None`] or [`Satisfaction::Complete`] + AddOnLeaf, + /// Can not add to an item that is [`Satisfaction::PartialComplete`] + AddOnPartialComplete, + /// Can not merge CSV or timelock values unless both are less than or both are equal or greater than 500_000_000 + MixedTimelockUnits, + /// Incompatible conditions (not currently used) + IncompatibleConditions, +} + +impl fmt::Display for PolicyError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::NotEnoughItemsSelected(err) => write!(f, "Not enought items selected: {}", err), + Self::IndexOutOfRange(index) => write!(f, "Index out of range: {}", index), + Self::AddOnLeaf => write!(f, "Add on leaf"), + Self::AddOnPartialComplete => write!(f, "Add on partial complete"), + Self::MixedTimelockUnits => write!(f, "Mixed timelock units"), + Self::IncompatibleConditions => write!(f, "Incompatible conditions"), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for PolicyError {} + +impl Policy { + fn new(item: SatisfiableItem) -> Self { + Policy { + id: item.id(), + item, + satisfaction: Satisfaction::None, + contribution: Satisfaction::None, + } + } + + fn make_and(a: Option, b: Option) -> Result, PolicyError> { + match (a, b) { + (None, None) => Ok(None), + (Some(x), None) | (None, Some(x)) => Ok(Some(x)), + (Some(a), Some(b)) => Self::make_thresh(vec![a, b], 2), + } + } + + fn make_or(a: Option, b: Option) -> Result, PolicyError> { + match (a, b) { + (None, None) => Ok(None), + (Some(x), None) | (None, Some(x)) => Ok(Some(x)), + (Some(a), Some(b)) => Self::make_thresh(vec![a, b], 1), + } + } + + fn make_thresh(items: Vec, threshold: usize) -> Result, PolicyError> { + if threshold == 0 { + return Ok(None); + } + + let mut contribution = Satisfaction::Partial { + n: items.len(), + m: threshold, + items: vec![], + conditions: Default::default(), + sorted: None, + }; + let mut satisfaction = contribution.clone(); + for (index, item) in items.iter().enumerate() { + contribution.add(&item.contribution, index)?; + satisfaction.add(&item.satisfaction, index)?; + } + + contribution.finalize(); + satisfaction.finalize(); + + let mut policy: Policy = SatisfiableItem::Thresh { items, threshold }.into(); + policy.contribution = contribution; + policy.satisfaction = satisfaction; + + Ok(Some(policy)) + } + + fn make_multisig( + keys: &[DescriptorPublicKey], + signers: &SignersContainer, + build_sat: BuildSatisfaction, + threshold: usize, + sorted: bool, + secp: &SecpCtx, + ) -> Result, PolicyError> { + if threshold == 0 { + return Ok(None); + } + + let parsed_keys = keys.iter().map(|k| PkOrF::from_key(k, secp)).collect(); + + let mut contribution = Satisfaction::Partial { + n: keys.len(), + m: threshold, + items: vec![], + conditions: Default::default(), + sorted: Some(sorted), + }; + let mut satisfaction = contribution.clone(); + + for (index, key) in keys.iter().enumerate() { + if signers.find(signer_id(key, secp)).is_some() { + contribution.add( + &Satisfaction::Complete { + condition: Default::default(), + }, + index, + )?; + } + + if let Some(psbt) = build_sat.psbt() { + if Ctx::find_signature(psbt, key, secp) { + satisfaction.add( + &Satisfaction::Complete { + condition: Default::default(), + }, + index, + )?; + } + } + } + satisfaction.finalize(); + contribution.finalize(); + + let mut policy: Policy = SatisfiableItem::Multisig { + keys: parsed_keys, + threshold, + } + .into(); + policy.contribution = contribution; + policy.satisfaction = satisfaction; + + Ok(Some(policy)) + } + + /// Return whether or not a specific path in the policy tree is required to unambiguously + /// create a transaction + /// + /// What this means is that for some spending policies the user should select which paths in + /// the tree it intends to satisfy while signing, because the transaction must be created differently based + /// on that. + pub fn requires_path(&self) -> bool { + self.get_condition(&BTreeMap::new()).is_err() + } + + /// Return the conditions that are set by the spending policy for a given path in the + /// policy tree + pub fn get_condition( + &self, + path: &BTreeMap>, + ) -> Result { + // if items.len() == threshold, selected can be omitted and we take all of them by default + let default = match &self.item { + SatisfiableItem::Thresh { items, threshold } if items.len() == *threshold => { + (0..*threshold).collect() + } + SatisfiableItem::Multisig { keys, .. } => (0..keys.len()).collect(), + _ => vec![], + }; + let selected = match path.get(&self.id) { + Some(arr) => arr, + _ => &default, + }; + + match &self.item { + SatisfiableItem::Thresh { items, threshold } => { + let mapped_req = items + .iter() + .map(|i| i.get_condition(path)) + .collect::, _>>()?; + + // if all the requirements are null we don't care about `selected` because there + // are no requirements + if mapped_req.iter().all(Condition::is_null) { + return Ok(Condition::default()); + } + + // if we have something, make sure we have enough items. note that the user can set + // an empty value for this step in case of n-of-n, because `selected` is set to all + // the elements above + if selected.len() < *threshold { + return Err(PolicyError::NotEnoughItemsSelected(self.id.clone())); + } + + // check the selected items, see if there are conflicting requirements + let mut requirements = Condition::default(); + for item_index in selected { + requirements = requirements.merge( + mapped_req + .get(*item_index) + .ok_or(PolicyError::IndexOutOfRange(*item_index))?, + )?; + } + + Ok(requirements) + } + SatisfiableItem::Multisig { keys, threshold } => { + if selected.len() < *threshold { + return Err(PolicyError::NotEnoughItemsSelected(self.id.clone())); + } + if let Some(item) = selected.iter().find(|i| **i >= keys.len()) { + return Err(PolicyError::IndexOutOfRange(*item)); + } + + Ok(Condition::default()) + } + SatisfiableItem::AbsoluteTimelock { value } => Ok(Condition { + csv: None, + timelock: Some(*value), + }), + SatisfiableItem::RelativeTimelock { value } => Ok(Condition { + csv: Some(*value), + timelock: None, + }), + _ => Ok(Condition::default()), + } + } +} + +impl From for Policy { + fn from(other: SatisfiableItem) -> Self { + Self::new(other) + } +} + +fn signer_id(key: &DescriptorPublicKey, secp: &SecpCtx) -> SignerId { + // For consistency we always compute the key hash in "ecdsa" form (with the leading sign + // prefix) even if we are in a taproot descriptor. We just want some kind of unique identifier + // for a key, so it doesn't really matter how the identifier is computed. + match key { + DescriptorPublicKey::Single(SinglePub { + key: SinglePubKey::FullKey(pk), + .. + }) => pk.to_pubkeyhash(SigType::Ecdsa).into(), + DescriptorPublicKey::Single(SinglePub { + key: SinglePubKey::XOnly(pk), + .. + }) => pk.to_pubkeyhash(SigType::Ecdsa).into(), + DescriptorPublicKey::XPub(xpub) => xpub.root_fingerprint(secp).into(), + } +} + +fn make_generic_signature SatisfiableItem, F: Fn(&Psbt) -> bool>( + key: &DescriptorPublicKey, + signers: &SignersContainer, + build_sat: BuildSatisfaction, + secp: &SecpCtx, + make_policy: M, + find_sig: F, +) -> Policy { + let mut policy: Policy = make_policy().into(); + + policy.contribution = if signers.find(signer_id(key, secp)).is_some() { + Satisfaction::Complete { + condition: Default::default(), + } + } else { + Satisfaction::None + }; + + if let Some(psbt) = build_sat.psbt() { + policy.satisfaction = if find_sig(psbt) { + Satisfaction::Complete { + condition: Default::default(), + } + } else { + Satisfaction::None + }; + } + + policy +} + +fn generic_sig_in_psbt< + // C is for "check", it's a closure we use to *check* if a psbt input contains the signature + // for a specific key + C: Fn(&PsbtInput, &SinglePubKey) -> bool, + // E is for "extract", it extracts a key from the bip32 derivations found in the psbt input + E: Fn(&PsbtInput, Fingerprint) -> Option, +>( + psbt: &Psbt, + key: &DescriptorPublicKey, + secp: &SecpCtx, + check: C, + extract: E, +) -> bool { + //TODO check signature validity + psbt.inputs.iter().all(|input| match key { + DescriptorPublicKey::Single(SinglePub { key, .. }) => check(input, key), + DescriptorPublicKey::XPub(xpub) => { + //TODO check actual derivation matches + match extract(input, xpub.root_fingerprint(secp)) { + Some(pubkey) => check(input, &pubkey), + None => false, + } + } + }) +} + +trait SigExt: ScriptContext { + fn make_signature( + key: &DescriptorPublicKey, + signers: &SignersContainer, + build_sat: BuildSatisfaction, + secp: &SecpCtx, + ) -> Policy; + + fn find_signature(psbt: &Psbt, key: &DescriptorPublicKey, secp: &SecpCtx) -> bool; +} + +impl SigExt for T { + fn make_signature( + key: &DescriptorPublicKey, + signers: &SignersContainer, + build_sat: BuildSatisfaction, + secp: &SecpCtx, + ) -> Policy { + if T::as_enum().is_taproot() { + make_generic_signature( + key, + signers, + build_sat, + secp, + || SatisfiableItem::SchnorrSignature(PkOrF::from_key(key, secp)), + |psbt| Self::find_signature(psbt, key, secp), + ) + } else { + make_generic_signature( + key, + signers, + build_sat, + secp, + || SatisfiableItem::EcdsaSignature(PkOrF::from_key(key, secp)), + |psbt| Self::find_signature(psbt, key, secp), + ) + } + } + + fn find_signature(psbt: &Psbt, key: &DescriptorPublicKey, secp: &SecpCtx) -> bool { + if T::as_enum().is_taproot() { + generic_sig_in_psbt( + psbt, + key, + secp, + |input, pk| { + let pk = match pk { + SinglePubKey::XOnly(pk) => pk, + _ => return false, + }; + + if input.tap_internal_key == Some(*pk) && input.tap_key_sig.is_some() { + true + } else { + input.tap_script_sigs.keys().any(|(sk, _)| sk == pk) + } + }, + |input, fing| { + input + .tap_key_origins + .iter() + .find(|(_, (_, (f, _)))| f == &fing) + .map(|(pk, _)| SinglePubKey::XOnly(*pk)) + }, + ) + } else { + generic_sig_in_psbt( + psbt, + key, + secp, + |input, pk| match pk { + SinglePubKey::FullKey(pk) => input.partial_sigs.contains_key(pk), + _ => false, + }, + |input, fing| { + input + .bip32_derivation + .iter() + .find(|(_, (f, _))| f == &fing) + .map(|(pk, _)| SinglePubKey::FullKey(PublicKey::new(*pk))) + }, + ) + } + } +} + +impl ExtractPolicy for Miniscript { + fn extract_policy( + &self, + signers: &SignersContainer, + build_sat: BuildSatisfaction, + secp: &SecpCtx, + ) -> Result, Error> { + Ok(match &self.node { + // Leaves + Terminal::True | Terminal::False => None, + Terminal::PkK(pubkey) => Some(Ctx::make_signature(pubkey, signers, build_sat, secp)), + Terminal::PkH(pubkey_hash) => { + Some(Ctx::make_signature(pubkey_hash, signers, build_sat, secp)) + } + Terminal::After(value) => { + let mut policy: Policy = SatisfiableItem::AbsoluteTimelock { + value: value.into(), + } + .into(); + policy.contribution = Satisfaction::Complete { + condition: Condition { + timelock: Some(value.into()), + csv: None, + }, + }; + if let BuildSatisfaction::PsbtTimelocks { + current_height, + psbt, + .. + } = build_sat + { + let after = After::new(Some(current_height), false); + let after_sat = + Satisfier::::check_after(&after, value.into()); + let inputs_sat = psbt_inputs_sat(psbt).all(|sat| { + Satisfier::::check_after(&sat, value.into()) + }); + if after_sat && inputs_sat { + policy.satisfaction = policy.contribution.clone(); + } + } + + Some(policy) + } + Terminal::Older(value) => { + let mut policy: Policy = SatisfiableItem::RelativeTimelock { value: *value }.into(); + policy.contribution = Satisfaction::Complete { + condition: Condition { + timelock: None, + csv: Some(*value), + }, + }; + if let BuildSatisfaction::PsbtTimelocks { + current_height, + input_max_height, + psbt, + } = build_sat + { + let older = Older::new(Some(current_height), Some(input_max_height), false); + let older_sat = Satisfier::::check_older(&older, *value); + let inputs_sat = psbt_inputs_sat(psbt) + .all(|sat| Satisfier::::check_older(&sat, *value)); + if older_sat && inputs_sat { + policy.satisfaction = policy.contribution.clone(); + } + } + + Some(policy) + } + Terminal::Sha256(hash) => Some(SatisfiableItem::Sha256Preimage { hash: *hash }.into()), + Terminal::Hash256(hash) => { + Some(SatisfiableItem::Hash256Preimage { hash: *hash }.into()) + } + Terminal::Ripemd160(hash) => { + Some(SatisfiableItem::Ripemd160Preimage { hash: *hash }.into()) + } + Terminal::Hash160(hash) => { + Some(SatisfiableItem::Hash160Preimage { hash: *hash }.into()) + } + Terminal::Multi(k, pks) | Terminal::MultiA(k, pks) => { + Policy::make_multisig::(pks, signers, build_sat, *k, false, secp)? + } + // Identities + Terminal::Alt(inner) + | Terminal::Swap(inner) + | Terminal::Check(inner) + | Terminal::DupIf(inner) + | Terminal::Verify(inner) + | Terminal::NonZero(inner) + | Terminal::ZeroNotEqual(inner) => inner.extract_policy(signers, build_sat, secp)?, + // Complex policies + Terminal::AndV(a, b) | Terminal::AndB(a, b) => Policy::make_and( + a.extract_policy(signers, build_sat, secp)?, + b.extract_policy(signers, build_sat, secp)?, + )?, + Terminal::AndOr(x, y, z) => Policy::make_or( + Policy::make_and( + x.extract_policy(signers, build_sat, secp)?, + y.extract_policy(signers, build_sat, secp)?, + )?, + z.extract_policy(signers, build_sat, secp)?, + )?, + Terminal::OrB(a, b) + | Terminal::OrD(a, b) + | Terminal::OrC(a, b) + | Terminal::OrI(a, b) => Policy::make_or( + a.extract_policy(signers, build_sat, secp)?, + b.extract_policy(signers, build_sat, secp)?, + )?, + Terminal::Thresh(k, nodes) => { + let mut threshold = *k; + let mapped: Vec<_> = nodes + .iter() + .map(|n| n.extract_policy(signers, build_sat, secp)) + .collect::, _>>()? + .into_iter() + .flatten() + .collect(); + + if mapped.len() < nodes.len() { + threshold = match threshold.checked_sub(nodes.len() - mapped.len()) { + None => return Ok(None), + Some(x) => x, + }; + } + + Policy::make_thresh(mapped, threshold)? + } + + // Unsupported + Terminal::RawPkH(_) => None, + }) + } +} + +fn psbt_inputs_sat(psbt: &Psbt) -> impl Iterator { + (0..psbt.inputs.len()).map(move |i| PsbtInputSatisfier::new(psbt, i)) +} + +/// Options to build the satisfaction field in the policy +#[derive(Debug, Clone, Copy)] +pub enum BuildSatisfaction<'a> { + /// Don't generate `satisfaction` field + None, + /// Analyze the given PSBT to check for existing signatures + Psbt(&'a Psbt), + /// Like `Psbt` variant and also check for expired timelocks + PsbtTimelocks { + /// Given PSBT + psbt: &'a Psbt, + /// Current blockchain height + current_height: u32, + /// The highest confirmation height between the inputs + /// CSV should consider different inputs, but we consider the worst condition for the tx as whole + input_max_height: u32, + }, +} +impl<'a> BuildSatisfaction<'a> { + fn psbt(&self) -> Option<&'a Psbt> { + match self { + BuildSatisfaction::None => None, + BuildSatisfaction::Psbt(psbt) => Some(psbt), + BuildSatisfaction::PsbtTimelocks { psbt, .. } => Some(psbt), + } + } +} + +impl ExtractPolicy for Descriptor { + fn extract_policy( + &self, + signers: &SignersContainer, + build_sat: BuildSatisfaction, + secp: &SecpCtx, + ) -> Result, Error> { + fn make_sortedmulti( + keys: &SortedMultiVec, + signers: &SignersContainer, + build_sat: BuildSatisfaction, + secp: &SecpCtx, + ) -> Result, Error> { + Ok(Policy::make_multisig::( + keys.pks.as_ref(), + signers, + build_sat, + keys.k, + true, + secp, + )?) + } + + match self { + Descriptor::Pkh(pk) => Ok(Some(miniscript::Legacy::make_signature( + pk.as_inner(), + signers, + build_sat, + secp, + ))), + Descriptor::Wpkh(pk) => Ok(Some(miniscript::Segwitv0::make_signature( + pk.as_inner(), + signers, + build_sat, + secp, + ))), + Descriptor::Sh(sh) => match sh.as_inner() { + ShInner::Wpkh(pk) => Ok(Some(miniscript::Segwitv0::make_signature( + pk.as_inner(), + signers, + build_sat, + secp, + ))), + ShInner::Ms(ms) => Ok(ms.extract_policy(signers, build_sat, secp)?), + ShInner::SortedMulti(ref keys) => make_sortedmulti(keys, signers, build_sat, secp), + ShInner::Wsh(wsh) => match wsh.as_inner() { + WshInner::Ms(ms) => Ok(ms.extract_policy(signers, build_sat, secp)?), + WshInner::SortedMulti(ref keys) => { + make_sortedmulti(keys, signers, build_sat, secp) + } + }, + }, + Descriptor::Wsh(wsh) => match wsh.as_inner() { + WshInner::Ms(ms) => Ok(ms.extract_policy(signers, build_sat, secp)?), + WshInner::SortedMulti(ref keys) => make_sortedmulti(keys, signers, build_sat, secp), + }, + Descriptor::Bare(ms) => Ok(ms.as_inner().extract_policy(signers, build_sat, secp)?), + Descriptor::Tr(tr) => { + // If there's no tap tree, treat this as a single sig, otherwise build a `Thresh` + // node with threshold = 1 and the key spend signature plus all the tree leaves + let key_spend_sig = + miniscript::Tap::make_signature(tr.internal_key(), signers, build_sat, secp); + + if tr.taptree().is_none() { + Ok(Some(key_spend_sig)) + } else { + let mut items = vec![key_spend_sig]; + items.append( + &mut tr + .iter_scripts() + .filter_map(|(_, ms)| { + ms.extract_policy(signers, build_sat, secp).transpose() + }) + .collect::, _>>()?, + ); + + Ok(Policy::make_thresh(items, 1)?) + } + } + } + } +} + +#[cfg(test)] +mod test { + use crate::descriptor; + use crate::descriptor::{ExtractPolicy, IntoWalletDescriptor}; + + use super::*; + use crate::descriptor::policy::SatisfiableItem::{EcdsaSignature, Multisig, Thresh}; + use crate::keys::{DescriptorKey, IntoDescriptorKey}; + use crate::wallet::signer::SignersContainer; + use alloc::{string::ToString, sync::Arc}; + use assert_matches::assert_matches; + use bitcoin::secp256k1::Secp256k1; + use bitcoin::util::bip32; + use bitcoin::Network; + use core::str::FromStr; + + const TPRV0_STR:&str = "tprv8ZgxMBicQKsPdZXrcHNLf5JAJWFAoJ2TrstMRdSKtEggz6PddbuSkvHKM9oKJyFgZV1B7rw8oChspxyYbtmEXYyg1AjfWbL3ho3XHDpHRZf"; + const TPRV1_STR:&str = "tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N"; + + const PATH: &str = "m/44'/1'/0'/0"; + + fn setup_keys( + tprv: &str, + path: &str, + secp: &SecpCtx, + ) -> (DescriptorKey, DescriptorKey, Fingerprint) { + let path = bip32::DerivationPath::from_str(path).unwrap(); + let tprv = bip32::ExtendedPrivKey::from_str(tprv).unwrap(); + let tpub = bip32::ExtendedPubKey::from_priv(secp, &tprv); + let fingerprint = tprv.fingerprint(secp); + let prvkey = (tprv, path.clone()).into_descriptor_key().unwrap(); + let pubkey = (tpub, path).into_descriptor_key().unwrap(); + + (prvkey, pubkey, fingerprint) + } + + // test ExtractPolicy trait for simple descriptors; wpkh(), sh(multi()) + + #[test] + fn test_extract_policy_for_wpkh() { + let secp = Secp256k1::new(); + + let (prvkey, pubkey, fingerprint) = setup_keys(TPRV0_STR, PATH, &secp); + let desc = descriptor!(wpkh(pubkey)).unwrap(); + let (wallet_desc, keymap) = desc + .into_wallet_descriptor(&secp, Network::Testnet) + .unwrap(); + let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); + let policy = wallet_desc + .extract_policy(&signers_container, BuildSatisfaction::None, &secp) + .unwrap() + .unwrap(); + + assert_matches!(&policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == &fingerprint); + assert_matches!(&policy.contribution, Satisfaction::None); + + let desc = descriptor!(wpkh(prvkey)).unwrap(); + let (wallet_desc, keymap) = desc + .into_wallet_descriptor(&secp, Network::Testnet) + .unwrap(); + let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); + let policy = wallet_desc + .extract_policy(&signers_container, BuildSatisfaction::None, &secp) + .unwrap() + .unwrap(); + + assert_matches!(&policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == &fingerprint); + assert_matches!(&policy.contribution, Satisfaction::Complete {condition} if condition.csv.is_none() && condition.timelock.is_none()); + } + + // 2 pub keys descriptor, required 2 prv keys + #[test] + fn test_extract_policy_for_sh_multi_partial_0of2() { + let secp = Secp256k1::new(); + let (_prvkey0, pubkey0, fingerprint0) = setup_keys(TPRV0_STR, PATH, &secp); + let (_prvkey1, pubkey1, fingerprint1) = setup_keys(TPRV1_STR, PATH, &secp); + let desc = descriptor!(sh(multi(2, pubkey0, pubkey1))).unwrap(); + let (wallet_desc, keymap) = desc + .into_wallet_descriptor(&secp, Network::Testnet) + .unwrap(); + let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); + let policy = wallet_desc + .extract_policy(&signers_container, BuildSatisfaction::None, &secp) + .unwrap() + .unwrap(); + + assert_matches!(&policy.item, Multisig { keys, threshold } if threshold == &2usize + && keys[0] == PkOrF::Fingerprint(fingerprint0) + && keys[1] == PkOrF::Fingerprint(fingerprint1) + ); + // TODO should this be "Satisfaction::None" since we have no prv keys? + // TODO should items and conditions not be empty? + assert_matches!(&policy.contribution, Satisfaction::Partial { n, m, items, conditions, ..} if n == &2usize + && m == &2usize + && items.is_empty() + && conditions.is_empty() + ); + } + + // 1 prv and 1 pub key descriptor, required 2 prv keys + #[test] + fn test_extract_policy_for_sh_multi_partial_1of2() { + let secp = Secp256k1::new(); + let (prvkey0, _pubkey0, fingerprint0) = setup_keys(TPRV0_STR, PATH, &secp); + let (_prvkey1, pubkey1, fingerprint1) = setup_keys(TPRV1_STR, PATH, &secp); + let desc = descriptor!(sh(multi(2, prvkey0, pubkey1))).unwrap(); + let (wallet_desc, keymap) = desc + .into_wallet_descriptor(&secp, Network::Testnet) + .unwrap(); + let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); + let policy = wallet_desc + .extract_policy(&signers_container, BuildSatisfaction::None, &secp) + .unwrap() + .unwrap(); + assert_matches!(&policy.item, Multisig { keys, threshold } if threshold == &2usize + && keys[0] == PkOrF::Fingerprint(fingerprint0) + && keys[1] == PkOrF::Fingerprint(fingerprint1) + ); + + assert_matches!(&policy.contribution, Satisfaction::Partial { n, m, items, conditions, ..} if n == &2usize + && m == &2usize + && items.len() == 1 + && conditions.contains_key(&0) + ); + } + + // 1 prv and 1 pub key descriptor, required 1 prv keys + #[test] + #[ignore] // see https://github.com/bitcoindevkit/bdk/issues/225 + fn test_extract_policy_for_sh_multi_complete_1of2() { + let secp = Secp256k1::new(); + + let (_prvkey0, pubkey0, fingerprint0) = setup_keys(TPRV0_STR, PATH, &secp); + let (prvkey1, _pubkey1, fingerprint1) = setup_keys(TPRV1_STR, PATH, &secp); + let desc = descriptor!(sh(multi(1, pubkey0, prvkey1))).unwrap(); + let (wallet_desc, keymap) = desc + .into_wallet_descriptor(&secp, Network::Testnet) + .unwrap(); + let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); + let policy = wallet_desc + .extract_policy(&signers_container, BuildSatisfaction::None, &secp) + .unwrap() + .unwrap(); + + assert_matches!(&policy.item, Multisig { keys, threshold } if threshold == &1 + && keys[0] == PkOrF::Fingerprint(fingerprint0) + && keys[1] == PkOrF::Fingerprint(fingerprint1) + ); + assert_matches!(&policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == &2 + && m == &1 + && items.len() == 2 + && conditions.contains_key(&vec![0]) + && conditions.contains_key(&vec![1]) + ); + } + + // 2 prv keys descriptor, required 2 prv keys + #[test] + fn test_extract_policy_for_sh_multi_complete_2of2() { + let secp = Secp256k1::new(); + + let (prvkey0, _pubkey0, fingerprint0) = setup_keys(TPRV0_STR, PATH, &secp); + let (prvkey1, _pubkey1, fingerprint1) = setup_keys(TPRV1_STR, PATH, &secp); + let desc = descriptor!(sh(multi(2, prvkey0, prvkey1))).unwrap(); + let (wallet_desc, keymap) = desc + .into_wallet_descriptor(&secp, Network::Testnet) + .unwrap(); + let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); + let policy = wallet_desc + .extract_policy(&signers_container, BuildSatisfaction::None, &secp) + .unwrap() + .unwrap(); + + assert_matches!(&policy.item, Multisig { keys, threshold } if threshold == &2 + && keys[0] == PkOrF::Fingerprint(fingerprint0) + && keys[1] == PkOrF::Fingerprint(fingerprint1) + ); + + assert_matches!(&policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == &2 + && m == &2 + && items.len() == 2 + && conditions.contains_key(&vec![0,1]) + ); + } + + // test ExtractPolicy trait with extended and single keys + + #[test] + fn test_extract_policy_for_single_wpkh() { + let secp = Secp256k1::new(); + + let (prvkey, pubkey, fingerprint) = setup_keys(TPRV0_STR, PATH, &secp); + let desc = descriptor!(wpkh(pubkey)).unwrap(); + let (wallet_desc, keymap) = desc + .into_wallet_descriptor(&secp, Network::Testnet) + .unwrap(); + let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); + let policy = wallet_desc + .extract_policy(&signers_container, BuildSatisfaction::None, &secp) + .unwrap() + .unwrap(); + + assert_matches!(&policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == &fingerprint); + assert_matches!(&policy.contribution, Satisfaction::None); + + let desc = descriptor!(wpkh(prvkey)).unwrap(); + let (wallet_desc, keymap) = desc + .into_wallet_descriptor(&secp, Network::Testnet) + .unwrap(); + let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); + let policy = wallet_desc + .extract_policy(&signers_container, BuildSatisfaction::None, &secp) + .unwrap() + .unwrap(); + + assert_matches!(policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == fingerprint); + assert_matches!(policy.contribution, Satisfaction::Complete {condition} if condition.csv.is_none() && condition.timelock.is_none()); + } + + // single key, 1 prv and 1 pub key descriptor, required 1 prv keys + #[test] + #[ignore] // see https://github.com/bitcoindevkit/bdk/issues/225 + fn test_extract_policy_for_single_wsh_multi_complete_1of2() { + let secp = Secp256k1::new(); + + let (_prvkey0, pubkey0, fingerprint0) = setup_keys(TPRV0_STR, PATH, &secp); + let (prvkey1, _pubkey1, fingerprint1) = setup_keys(TPRV1_STR, PATH, &secp); + let desc = descriptor!(sh(multi(1, pubkey0, prvkey1))).unwrap(); + let (wallet_desc, keymap) = desc + .into_wallet_descriptor(&secp, Network::Testnet) + .unwrap(); + let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); + let policy = wallet_desc + .extract_policy(&signers_container, BuildSatisfaction::None, &secp) + .unwrap() + .unwrap(); + + assert_matches!(policy.item, Multisig { keys, threshold } if threshold == 1 + && keys[0] == PkOrF::Fingerprint(fingerprint0) + && keys[1] == PkOrF::Fingerprint(fingerprint1) + ); + assert_matches!(policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == 2 + && m == 1 + && items.len() == 2 + && conditions.contains_key(&vec![0]) + && conditions.contains_key(&vec![1]) + ); + } + + // test ExtractPolicy trait with descriptors containing timelocks in a thresh() + + #[test] + #[ignore] // see https://github.com/bitcoindevkit/bdk/issues/225 + fn test_extract_policy_for_wsh_multi_timelock() { + let secp = Secp256k1::new(); + + let (prvkey0, _pubkey0, _fingerprint0) = setup_keys(TPRV0_STR, PATH, &secp); + let (_prvkey1, pubkey1, _fingerprint1) = setup_keys(TPRV1_STR, PATH, &secp); + let sequence = 50; + #[rustfmt::skip] + let desc = descriptor!(wsh(thresh( + 2, + pk(prvkey0), + s:pk(pubkey1), + s:d:v:older(sequence) + ))) + .unwrap(); + + let (wallet_desc, keymap) = desc + .into_wallet_descriptor(&secp, Network::Testnet) + .unwrap(); + let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); + let policy = wallet_desc + .extract_policy(&signers_container, BuildSatisfaction::None, &secp) + .unwrap() + .unwrap(); + + assert_matches!(&policy.item, Thresh { items, threshold } if items.len() == 3 && threshold == &2); + + assert_matches!(&policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == &3 + && m == &2 + && items.len() == 3 + && conditions.get(&vec![0,1]).unwrap().iter().next().unwrap().csv.is_none() + && conditions.get(&vec![0,2]).unwrap().iter().next().unwrap().csv == Some(Sequence(sequence)) + && conditions.get(&vec![1,2]).unwrap().iter().next().unwrap().csv == Some(Sequence(sequence)) + ); + } + + // - mixed timelocks should fail + + #[test] + #[ignore] + fn test_extract_policy_for_wsh_mixed_timelocks() { + let secp = Secp256k1::new(); + let (prvkey0, _pubkey0, _fingerprint0) = setup_keys(TPRV0_STR, PATH, &secp); + let locktime_threshold = 500000000; // if less than this means block number, else block time in seconds + let locktime_blocks = 100; + let locktime_seconds = locktime_blocks + locktime_threshold; + let desc = descriptor!(sh(and_v( + v: pk(prvkey0), + and_v(v: after(locktime_seconds), after(locktime_blocks)) + ))) + .unwrap(); + let (wallet_desc, keymap) = desc + .into_wallet_descriptor(&secp, Network::Testnet) + .unwrap(); + let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); + let _policy = wallet_desc + .extract_policy(&signers_container, BuildSatisfaction::None, &secp) + .unwrap() + .unwrap(); + // println!("desc policy = {:?}", policy); // TODO remove + // TODO how should this fail with mixed timelocks? + } + + // - multiple timelocks of the same type should be correctly merged together + #[test] + #[ignore] + fn test_extract_policy_for_multiple_same_timelocks() { + let secp = Secp256k1::new(); + let (prvkey0, _pubkey0, _fingerprint0) = setup_keys(TPRV0_STR, PATH, &secp); + let locktime_blocks0 = 100; + let locktime_blocks1 = 200; + let desc = descriptor!(sh(and_v( + v: pk(prvkey0), + and_v(v: after(locktime_blocks0), after(locktime_blocks1)) + ))) + .unwrap(); + let (wallet_desc, keymap) = desc + .into_wallet_descriptor(&secp, Network::Testnet) + .unwrap(); + let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); + let _policy = wallet_desc + .extract_policy(&signers_container, BuildSatisfaction::None, &secp) + .unwrap() + .unwrap(); + // println!("desc policy = {:?}", policy); // TODO remove + // TODO how should this merge timelocks? + let (prvkey1, _pubkey1, _fingerprint1) = setup_keys(TPRV0_STR, PATH, &secp); + let locktime_seconds0 = 500000100; + let locktime_seconds1 = 500000200; + let desc = descriptor!(sh(and_v( + v: pk(prvkey1), + and_v(v: after(locktime_seconds0), after(locktime_seconds1)) + ))) + .unwrap(); + let (wallet_desc, keymap) = desc + .into_wallet_descriptor(&secp, Network::Testnet) + .unwrap(); + let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); + let _policy = wallet_desc + .extract_policy(&signers_container, BuildSatisfaction::None, &secp) + .unwrap() + .unwrap(); + + // println!("desc policy = {:?}", policy); // TODO remove + + // TODO how should this merge timelocks? + } + + #[test] + fn test_get_condition_multisig() { + let secp = Secp256k1::new(); + + let (_, pk0, _) = setup_keys(TPRV0_STR, PATH, &secp); + let (_, pk1, _) = setup_keys(TPRV1_STR, PATH, &secp); + + let desc = descriptor!(wsh(multi(1, pk0, pk1))).unwrap(); + let (wallet_desc, keymap) = desc + .into_wallet_descriptor(&secp, Network::Testnet) + .unwrap(); + let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); + + let policy = wallet_desc + .extract_policy(&signers_container, BuildSatisfaction::None, &secp) + .unwrap() + .unwrap(); + + // no args, choose the default + let no_args = policy.get_condition(&vec![].into_iter().collect()); + assert_eq!(no_args, Ok(Condition::default())); + + // enough args + let eq_thresh = + policy.get_condition(&vec![(policy.id.clone(), vec![0])].into_iter().collect()); + assert_eq!(eq_thresh, Ok(Condition::default())); + + // more args, it doesn't really change anything + let gt_thresh = + policy.get_condition(&vec![(policy.id.clone(), vec![0, 1])].into_iter().collect()); + assert_eq!(gt_thresh, Ok(Condition::default())); + + // not enough args, error + let lt_thresh = + policy.get_condition(&vec![(policy.id.clone(), vec![])].into_iter().collect()); + assert_eq!( + lt_thresh, + Err(PolicyError::NotEnoughItemsSelected(policy.id.clone())) + ); + + // index out of range + let out_of_range = + policy.get_condition(&vec![(policy.id.clone(), vec![5])].into_iter().collect()); + assert_eq!(out_of_range, Err(PolicyError::IndexOutOfRange(5))); + } + + const ALICE_TPRV_STR:&str = "tprv8ZgxMBicQKsPf6T5X327efHnvJDr45Xnb8W4JifNWtEoqXu9MRYS4v1oYe6DFcMVETxy5w3bqpubYRqvcVTqovG1LifFcVUuJcbwJwrhYzP"; + const BOB_TPRV_STR:&str = "tprv8ZgxMBicQKsPeinZ155cJAn117KYhbaN6MV3WeG6sWhxWzcvX1eg1awd4C9GpUN1ncLEM2rzEvunAg3GizdZD4QPPCkisTz99tXXB4wZArp"; + const CAROL_TPRV_STR:&str = "tprv8ZgxMBicQKsPdC3CicFifuLCEyVVdXVUNYorxUWj3iGZ6nimnLAYAY9SYB7ib8rKzRxrCKFcEytCt6szwd2GHnGPRCBLAEAoSVDefSNk4Bt"; + const ALICE_BOB_PATH: &str = "m/0'"; + + #[test] + fn test_extract_satisfaction() { + const ALICE_SIGNED_PSBT: &str = "cHNidP8BAFMBAAAAAZb0njwT2wRS3AumaaP3yb7T4MxOePpSWih4Nq+jWChMAQAAAAD/////Af4lAAAAAAAAF6kUXv2Fn+YemPP4PUpNR1ZbU16/eRCHAAAAAAABASuJJgAAAAAAACIAIERw5kTLo9DUH9QDJSClHQwPpC7VGJ+ZMDpa8U+2fzcYIgIDeAtjYQk/Vfu4db2+68hyMKjc38+kWl5sP5QH8L42ZstHMEQCIBj0jLjUeVYXNQ6cqB+gbtvuKMjV54wSgWlm1cfcgpHVAiBa3DtC9l/1Mt4IDCvR7mmwQd3eAP/m5++81euhJNSrgQEBBUdSIQN4C2NhCT9V+7h1vb7ryHIwqNzfz6RaXmw/lAfwvjZmyyEC+GE/y+LptI8xmiR6sOe998IGzybox0Qfz4+BQl1nmYhSriIGAvhhP8vi6bSPMZokerDnvffCBs8m6MdEH8+PgUJdZ5mIDBwu7j4AAACAAAAAACIGA3gLY2EJP1X7uHW9vuvIcjCo3N/PpFpebD+UB/C+NmbLDMkRfC4AAACAAAAAAAAA"; + const BOB_SIGNED_PSBT: &str = "cHNidP8BAFMBAAAAAZb0njwT2wRS3AumaaP3yb7T4MxOePpSWih4Nq+jWChMAQAAAAD/////Af4lAAAAAAAAF6kUXv2Fn+YemPP4PUpNR1ZbU16/eRCHAAAAAAABASuJJgAAAAAAACIAIERw5kTLo9DUH9QDJSClHQwPpC7VGJ+ZMDpa8U+2fzcYIgIC+GE/y+LptI8xmiR6sOe998IGzybox0Qfz4+BQl1nmYhIMEUCIQD5zDtM5MwklurwJ5aW76RsO36Iqyu+6uMdVlhL6ws2GQIgesAiz4dbKS7UmhDsC/c1ezu0o6hp00UUtsCMfUZ4anYBAQVHUiEDeAtjYQk/Vfu4db2+68hyMKjc38+kWl5sP5QH8L42ZsshAvhhP8vi6bSPMZokerDnvffCBs8m6MdEH8+PgUJdZ5mIUq4iBgL4YT/L4um0jzGaJHqw5733wgbPJujHRB/Pj4FCXWeZiAwcLu4+AAAAgAAAAAAiBgN4C2NhCT9V+7h1vb7ryHIwqNzfz6RaXmw/lAfwvjZmywzJEXwuAAAAgAAAAAAAAA=="; + const ALICE_BOB_SIGNED_PSBT: &str = "cHNidP8BAFMBAAAAAZb0njwT2wRS3AumaaP3yb7T4MxOePpSWih4Nq+jWChMAQAAAAD/////Af4lAAAAAAAAF6kUXv2Fn+YemPP4PUpNR1ZbU16/eRCHAAAAAAABASuJJgAAAAAAACIAIERw5kTLo9DUH9QDJSClHQwPpC7VGJ+ZMDpa8U+2fzcYIgIC+GE/y+LptI8xmiR6sOe998IGzybox0Qfz4+BQl1nmYhIMEUCIQD5zDtM5MwklurwJ5aW76RsO36Iqyu+6uMdVlhL6ws2GQIgesAiz4dbKS7UmhDsC/c1ezu0o6hp00UUtsCMfUZ4anYBIgIDeAtjYQk/Vfu4db2+68hyMKjc38+kWl5sP5QH8L42ZstHMEQCIBj0jLjUeVYXNQ6cqB+gbtvuKMjV54wSgWlm1cfcgpHVAiBa3DtC9l/1Mt4IDCvR7mmwQd3eAP/m5++81euhJNSrgQEBBUdSIQN4C2NhCT9V+7h1vb7ryHIwqNzfz6RaXmw/lAfwvjZmyyEC+GE/y+LptI8xmiR6sOe998IGzybox0Qfz4+BQl1nmYhSriIGAvhhP8vi6bSPMZokerDnvffCBs8m6MdEH8+PgUJdZ5mIDBwu7j4AAACAAAAAACIGA3gLY2EJP1X7uHW9vuvIcjCo3N/PpFpebD+UB/C+NmbLDMkRfC4AAACAAAAAAAEHAAEI2wQARzBEAiAY9Iy41HlWFzUOnKgfoG7b7ijI1eeMEoFpZtXH3IKR1QIgWtw7QvZf9TLeCAwr0e5psEHd3gD/5ufvvNXroSTUq4EBSDBFAiEA+cw7TOTMJJbq8CeWlu+kbDt+iKsrvurjHVZYS+sLNhkCIHrAIs+HWyku1JoQ7Av3NXs7tKOoadNFFLbAjH1GeGp2AUdSIQN4C2NhCT9V+7h1vb7ryHIwqNzfz6RaXmw/lAfwvjZmyyEC+GE/y+LptI8xmiR6sOe998IGzybox0Qfz4+BQl1nmYhSrgAA"; + + let secp = Secp256k1::new(); + + let (prvkey_alice, _, _) = setup_keys(ALICE_TPRV_STR, ALICE_BOB_PATH, &secp); + let (prvkey_bob, _, _) = setup_keys(BOB_TPRV_STR, ALICE_BOB_PATH, &secp); + + let desc = descriptor!(wsh(multi(2, prvkey_alice, prvkey_bob))).unwrap(); + + let (wallet_desc, keymap) = desc + .into_wallet_descriptor(&secp, Network::Testnet) + .unwrap(); + + let addr = wallet_desc + .at_derivation_index(0) + .address(Network::Testnet) + .unwrap(); + assert_eq!( + "tb1qg3cwv3xt50gdg875qvjjpfgaps86gtk4rz0ejvp6ttc5ldnlxuvqlcn0xk", + addr.to_string() + ); + + let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); + + let psbt = Psbt::from_str(ALICE_SIGNED_PSBT).unwrap(); + + let policy_alice_psbt = wallet_desc + .extract_policy(&signers_container, BuildSatisfaction::Psbt(&psbt), &secp) + .unwrap() + .unwrap(); + //println!("{}", serde_json::to_string(&policy_alice_psbt).unwrap()); + + assert_matches!(&policy_alice_psbt.satisfaction, Satisfaction::Partial { n, m, items, .. } if n == &2 + && m == &2 + && items == &vec![0] + ); + + let psbt = Psbt::from_str(BOB_SIGNED_PSBT).unwrap(); + let policy_bob_psbt = wallet_desc + .extract_policy(&signers_container, BuildSatisfaction::Psbt(&psbt), &secp) + .unwrap() + .unwrap(); + //println!("{}", serde_json::to_string(&policy_bob_psbt).unwrap()); + + assert_matches!(&policy_bob_psbt.satisfaction, Satisfaction::Partial { n, m, items, .. } if n == &2 + && m == &2 + && items == &vec![1] + ); + + let psbt = Psbt::from_str(ALICE_BOB_SIGNED_PSBT).unwrap(); + let policy_alice_bob_psbt = wallet_desc + .extract_policy(&signers_container, BuildSatisfaction::Psbt(&psbt), &secp) + .unwrap() + .unwrap(); + assert_matches!(&policy_alice_bob_psbt.satisfaction, Satisfaction::PartialComplete { n, m, items, .. } if n == &2 + && m == &2 + && items == &vec![0, 1] + ); + } + + #[test] + fn test_extract_satisfaction_timelock() { + //const PSBT_POLICY_CONSIDER_TIMELOCK_NOT_EXPIRED: &str = "cHNidP8BAFMBAAAAAdld52uJFGT7Yde0YZdSVh2vL020Zm2exadH5R4GSNScAAAAAAD/////ATrcAAAAAAAAF6kUXv2Fn+YemPP4PUpNR1ZbU16/eRCHAAAAAAABASvI3AAAAAAAACIAILhzvvcBzw/Zfnc9ispRK0PCahxn1F6RHXTZAmw5tqNPAQVSdmNSsmlofCEDeAtjYQk/Vfu4db2+68hyMKjc38+kWl5sP5QH8L42Zsusk3whAvhhP8vi6bSPMZokerDnvffCBs8m6MdEH8+PgUJdZ5mIrJNShyIGAvhhP8vi6bSPMZokerDnvffCBs8m6MdEH8+PgUJdZ5mIDBwu7j4AAACAAAAAACIGA3gLY2EJP1X7uHW9vuvIcjCo3N/PpFpebD+UB/C+NmbLDMkRfC4AAACAAAAAAAAA"; + const PSBT_POLICY_CONSIDER_TIMELOCK_EXPIRED: &str = "cHNidP8BAFMCAAAAAdld52uJFGT7Yde0YZdSVh2vL020Zm2exadH5R4GSNScAAAAAAACAAAAATrcAAAAAAAAF6kUXv2Fn+YemPP4PUpNR1ZbU16/eRCHAAAAAAABASvI3AAAAAAAACIAILhzvvcBzw/Zfnc9ispRK0PCahxn1F6RHXTZAmw5tqNPAQVSdmNSsmlofCEDeAtjYQk/Vfu4db2+68hyMKjc38+kWl5sP5QH8L42Zsusk3whAvhhP8vi6bSPMZokerDnvffCBs8m6MdEH8+PgUJdZ5mIrJNShyIGAvhhP8vi6bSPMZokerDnvffCBs8m6MdEH8+PgUJdZ5mIDBwu7j4AAACAAAAAACIGA3gLY2EJP1X7uHW9vuvIcjCo3N/PpFpebD+UB/C+NmbLDMkRfC4AAACAAAAAAAAA"; + const PSBT_POLICY_CONSIDER_TIMELOCK_EXPIRED_SIGNED: &str ="cHNidP8BAFMCAAAAAdld52uJFGT7Yde0YZdSVh2vL020Zm2exadH5R4GSNScAAAAAAACAAAAATrcAAAAAAAAF6kUXv2Fn+YemPP4PUpNR1ZbU16/eRCHAAAAAAABASvI3AAAAAAAACIAILhzvvcBzw/Zfnc9ispRK0PCahxn1F6RHXTZAmw5tqNPIgIDeAtjYQk/Vfu4db2+68hyMKjc38+kWl5sP5QH8L42ZstIMEUCIQCtZxNm6H3Ux3pnc64DSpgohMdBj+57xhFHcURYt2BpPAIgG3OnI7bcj/3GtWX1HHyYGSI7QGa/zq5YnsmK1Cw29NABAQVSdmNSsmlofCEDeAtjYQk/Vfu4db2+68hyMKjc38+kWl5sP5QH8L42Zsusk3whAvhhP8vi6bSPMZokerDnvffCBs8m6MdEH8+PgUJdZ5mIrJNShyIGAvhhP8vi6bSPMZokerDnvffCBs8m6MdEH8+PgUJdZ5mIDBwu7j4AAACAAAAAACIGA3gLY2EJP1X7uHW9vuvIcjCo3N/PpFpebD+UB/C+NmbLDMkRfC4AAACAAAAAAAEHAAEIoAQASDBFAiEArWcTZuh91Md6Z3OuA0qYKITHQY/ue8YRR3FEWLdgaTwCIBtzpyO23I/9xrVl9Rx8mBkiO0Bmv86uWJ7JitQsNvTQAQEBUnZjUrJpaHwhA3gLY2EJP1X7uHW9vuvIcjCo3N/PpFpebD+UB/C+NmbLrJN8IQL4YT/L4um0jzGaJHqw5733wgbPJujHRB/Pj4FCXWeZiKyTUocAAA=="; + + let secp = Secp256k1::new(); + + let (prvkey_alice, _, _) = setup_keys(ALICE_TPRV_STR, ALICE_BOB_PATH, &secp); + let (prvkey_bob, _, _) = setup_keys(BOB_TPRV_STR, ALICE_BOB_PATH, &secp); + + let desc = + descriptor!(wsh(thresh(2,n:d:v:older(2),s:pk(prvkey_alice),s:pk(prvkey_bob)))).unwrap(); + + let (wallet_desc, keymap) = desc + .into_wallet_descriptor(&secp, Network::Testnet) + .unwrap(); + let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); + + let addr = wallet_desc + .at_derivation_index(0) + .address(Network::Testnet) + .unwrap(); + assert_eq!( + "tb1qsydsey4hexagwkvercqsmes6yet0ndkyt6uzcphtqnygjd8hmzmsfxrv58", + addr.to_string() + ); + + let psbt = Psbt::from_str(PSBT_POLICY_CONSIDER_TIMELOCK_EXPIRED).unwrap(); + + let build_sat = BuildSatisfaction::PsbtTimelocks { + psbt: &psbt, + current_height: 10, + input_max_height: 9, + }; + + let policy = wallet_desc + .extract_policy(&signers_container, build_sat, &secp) + .unwrap() + .unwrap(); + assert_matches!(&policy.satisfaction, Satisfaction::Partial { n, m, items, .. } if n == &3 + && m == &2 + && items.is_empty() + ); + //println!("{}", serde_json::to_string(&policy).unwrap()); + + let build_sat_expired = BuildSatisfaction::PsbtTimelocks { + psbt: &psbt, + current_height: 12, + input_max_height: 9, + }; + + let policy_expired = wallet_desc + .extract_policy(&signers_container, build_sat_expired, &secp) + .unwrap() + .unwrap(); + assert_matches!(&policy_expired.satisfaction, Satisfaction::Partial { n, m, items, .. } if n == &3 + && m == &2 + && items == &vec![0] + ); + //println!("{}", serde_json::to_string(&policy_expired).unwrap()); + + let psbt_signed = Psbt::from_str(PSBT_POLICY_CONSIDER_TIMELOCK_EXPIRED_SIGNED).unwrap(); + + let build_sat_expired_signed = BuildSatisfaction::PsbtTimelocks { + psbt: &psbt_signed, + current_height: 12, + input_max_height: 9, + }; + + let policy_expired_signed = wallet_desc + .extract_policy(&signers_container, build_sat_expired_signed, &secp) + .unwrap() + .unwrap(); + assert_matches!(&policy_expired_signed.satisfaction, Satisfaction::PartialComplete { n, m, items, .. } if n == &3 + && m == &2 + && items == &vec![0, 1] + ); + //println!("{}", serde_json::to_string(&policy_expired_signed).unwrap()); + } + + #[test] + fn test_extract_pkh() { + let secp = Secp256k1::new(); + + let (prvkey_alice, _, _) = setup_keys(ALICE_TPRV_STR, ALICE_BOB_PATH, &secp); + let (prvkey_bob, _, _) = setup_keys(BOB_TPRV_STR, ALICE_BOB_PATH, &secp); + let (prvkey_carol, _, _) = setup_keys(CAROL_TPRV_STR, ALICE_BOB_PATH, &secp); + + let desc = descriptor!(wsh(c: andor( + pk(prvkey_alice), + pk_k(prvkey_bob), + pk_h(prvkey_carol), + ))) + .unwrap(); + + let (wallet_desc, keymap) = desc + .into_wallet_descriptor(&secp, Network::Testnet) + .unwrap(); + let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); + + let policy = wallet_desc.extract_policy(&signers_container, BuildSatisfaction::None, &secp); + assert!(policy.is_ok()); + } + + #[test] + fn test_extract_tr_key_spend() { + let secp = Secp256k1::new(); + + let (prvkey, _, fingerprint) = setup_keys(ALICE_TPRV_STR, ALICE_BOB_PATH, &secp); + + let desc = descriptor!(tr(prvkey)).unwrap(); + let (wallet_desc, keymap) = desc + .into_wallet_descriptor(&secp, Network::Testnet) + .unwrap(); + let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); + + let policy = wallet_desc + .extract_policy(&signers_container, BuildSatisfaction::None, &secp) + .unwrap(); + assert_eq!( + policy, + Some(Policy { + id: "48u0tz0n".to_string(), + item: SatisfiableItem::SchnorrSignature(PkOrF::Fingerprint(fingerprint)), + satisfaction: Satisfaction::None, + contribution: Satisfaction::Complete { + condition: Condition::default() + } + }) + ); + } + + #[test] + fn test_extract_tr_script_spend() { + let secp = Secp256k1::new(); + + let (alice_prv, _, alice_fing) = setup_keys(ALICE_TPRV_STR, ALICE_BOB_PATH, &secp); + let (_, bob_pub, bob_fing) = setup_keys(BOB_TPRV_STR, ALICE_BOB_PATH, &secp); + + let desc = descriptor!(tr(bob_pub, pk(alice_prv))).unwrap(); + let (wallet_desc, keymap) = desc + .into_wallet_descriptor(&secp, Network::Testnet) + .unwrap(); + let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); + + let policy = wallet_desc + .extract_policy(&signers_container, BuildSatisfaction::None, &secp) + .unwrap() + .unwrap(); + + assert_matches!(policy.item, SatisfiableItem::Thresh { ref items, threshold: 1 } if items.len() == 2); + assert_matches!(policy.contribution, Satisfaction::PartialComplete { n: 2, m: 1, items, .. } if items == vec![1]); + + let alice_sig = SatisfiableItem::SchnorrSignature(PkOrF::Fingerprint(alice_fing)); + let bob_sig = SatisfiableItem::SchnorrSignature(PkOrF::Fingerprint(bob_fing)); + + let thresh_items = match policy.item { + SatisfiableItem::Thresh { items, .. } => items, + _ => unreachable!(), + }; + + assert_eq!(thresh_items[0].item, bob_sig); + assert_eq!(thresh_items[1].item, alice_sig); + } + + #[test] + fn test_extract_tr_satisfaction_key_spend() { + const UNSIGNED_PSBT: &str = "cHNidP8BAFMBAAAAAUKgMCqtGLSiGYhsTols2UJ/VQQgQi/SXO38uXs2SahdAQAAAAD/////ARyWmAAAAAAAF6kU4R3W8CnGzZcSsaovTYu0X8vHt3WHAAAAAAABASuAlpgAAAAAACJRIEiEBFjbZa1xdjLfFjrKzuC1F1LeRyI/gL6IuGKNmUuSIRYnkGTDxwXMHP32fkDFoGJY28trxbkkVgR2z7jZa2pOJA0AyRF8LgAAAIADAAAAARcgJ5Bkw8cFzBz99n5AxaBiWNvLa8W5JFYEds+42WtqTiQAAA=="; + const SIGNED_PSBT: &str = "cHNidP8BAFMBAAAAAUKgMCqtGLSiGYhsTols2UJ/VQQgQi/SXO38uXs2SahdAQAAAAD/////ARyWmAAAAAAAF6kU4R3W8CnGzZcSsaovTYu0X8vHt3WHAAAAAAABASuAlpgAAAAAACJRIEiEBFjbZa1xdjLfFjrKzuC1F1LeRyI/gL6IuGKNmUuSARNAIsRvARpRxuyQosVA7guRQT9vXr+S25W2tnP2xOGBsSgq7A4RL8yrbvwDmNlWw9R0Nc/6t+IsyCyy7dD/lbUGgyEWJ5Bkw8cFzBz99n5AxaBiWNvLa8W5JFYEds+42WtqTiQNAMkRfC4AAACAAwAAAAEXICeQZMPHBcwc/fZ+QMWgYljby2vFuSRWBHbPuNlrak4kAAA="; + + let unsigned_psbt = Psbt::from_str(UNSIGNED_PSBT).unwrap(); + let signed_psbt = Psbt::from_str(SIGNED_PSBT).unwrap(); + + let secp = Secp256k1::new(); + + let (_, pubkey, _) = setup_keys(ALICE_TPRV_STR, ALICE_BOB_PATH, &secp); + + let desc = descriptor!(tr(pubkey)).unwrap(); + let (wallet_desc, _) = desc + .into_wallet_descriptor(&secp, Network::Testnet) + .unwrap(); + + let policy_unsigned = wallet_desc + .extract_policy( + &SignersContainer::default(), + BuildSatisfaction::Psbt(&unsigned_psbt), + &secp, + ) + .unwrap() + .unwrap(); + let policy_signed = wallet_desc + .extract_policy( + &SignersContainer::default(), + BuildSatisfaction::Psbt(&signed_psbt), + &secp, + ) + .unwrap() + .unwrap(); + + assert_eq!(policy_unsigned.satisfaction, Satisfaction::None); + assert_eq!( + policy_signed.satisfaction, + Satisfaction::Complete { + condition: Default::default() + } + ); + } + + #[test] + fn test_extract_tr_satisfaction_script_spend() { + const UNSIGNED_PSBT: &str = "cHNidP8BAFMBAAAAAWZalxaErOL7P3WPIUc8DsjgE68S+ww+uqiqEI2SAwlPAAAAAAD/////AQiWmAAAAAAAF6kU4R3W8CnGzZcSsaovTYu0X8vHt3WHAAAAAAABASuAlpgAAAAAACJRINa6bLPZwp3/CYWoxyI3mLYcSC5f9LInAMUng94nspa2IhXBgiPY+kcolS1Hp0niOK/+7VHz6F+nsz8JVxnzWzkgToYjIHhGyuexxtRVKevRx4YwWR/W0r7LPHt6oS6DLlzyuYQarMAhFnhGyuexxtRVKevRx4YwWR/W0r7LPHt6oS6DLlzyuYQaLQH2onWFc3UR6I9ZhuHVeJCi5LNAf4APVd7mHn4BhdViHRwu7j4AAACAAgAAACEWgiPY+kcolS1Hp0niOK/+7VHz6F+nsz8JVxnzWzkgToYNAMkRfC4AAACAAgAAAAEXIIIj2PpHKJUtR6dJ4jiv/u1R8+hfp7M/CVcZ81s5IE6GARgg9qJ1hXN1EeiPWYbh1XiQouSzQH+AD1Xe5h5+AYXVYh0AAA=="; + const SIGNED_PSBT: &str = "cHNidP8BAFMBAAAAAWZalxaErOL7P3WPIUc8DsjgE68S+ww+uqiqEI2SAwlPAAAAAAD/////AQiWmAAAAAAAF6kU4R3W8CnGzZcSsaovTYu0X8vHt3WHAAAAAAABASuAlpgAAAAAACJRINa6bLPZwp3/CYWoxyI3mLYcSC5f9LInAMUng94nspa2AQcAAQhCAUALcP9w/+Ddly9DWdhHTnQ9uCDWLPZjR6vKbKePswW2Ee6W5KNfrklus/8z98n7BQ1U4vADHk0FbadeeL8rrbHlARNAC3D/cP/g3ZcvQ1nYR050Pbgg1iz2Y0erymynj7MFthHuluSjX65JbrP/M/fJ+wUNVOLwAx5NBW2nXni/K62x5UEUeEbK57HG1FUp69HHhjBZH9bSvss8e3qhLoMuXPK5hBr2onWFc3UR6I9ZhuHVeJCi5LNAf4APVd7mHn4BhdViHUAXNmWieJ80Fs+PMa2C186YOBPZbYG/ieEUkagMwzJ788SoCucNdp5wnxfpuJVygFhglDrXGzujFtC82PrMohwuIhXBgiPY+kcolS1Hp0niOK/+7VHz6F+nsz8JVxnzWzkgToYjIHhGyuexxtRVKevRx4YwWR/W0r7LPHt6oS6DLlzyuYQarMAhFnhGyuexxtRVKevRx4YwWR/W0r7LPHt6oS6DLlzyuYQaLQH2onWFc3UR6I9ZhuHVeJCi5LNAf4APVd7mHn4BhdViHRwu7j4AAACAAgAAACEWgiPY+kcolS1Hp0niOK/+7VHz6F+nsz8JVxnzWzkgToYNAMkRfC4AAACAAgAAAAEXIIIj2PpHKJUtR6dJ4jiv/u1R8+hfp7M/CVcZ81s5IE6GARgg9qJ1hXN1EeiPWYbh1XiQouSzQH+AD1Xe5h5+AYXVYh0AAA=="; + + let unsigned_psbt = Psbt::from_str(UNSIGNED_PSBT).unwrap(); + let signed_psbt = Psbt::from_str(SIGNED_PSBT).unwrap(); + + let secp = Secp256k1::new(); + + let (_, alice_pub, _) = setup_keys(ALICE_TPRV_STR, ALICE_BOB_PATH, &secp); + let (_, bob_pub, _) = setup_keys(BOB_TPRV_STR, ALICE_BOB_PATH, &secp); + + let desc = descriptor!(tr(bob_pub, pk(alice_pub))).unwrap(); + let (wallet_desc, _) = desc + .into_wallet_descriptor(&secp, Network::Testnet) + .unwrap(); + + let policy_unsigned = wallet_desc + .extract_policy( + &SignersContainer::default(), + BuildSatisfaction::Psbt(&unsigned_psbt), + &secp, + ) + .unwrap() + .unwrap(); + let policy_signed = wallet_desc + .extract_policy( + &SignersContainer::default(), + BuildSatisfaction::Psbt(&signed_psbt), + &secp, + ) + .unwrap() + .unwrap(); + + assert_matches!(policy_unsigned.item, SatisfiableItem::Thresh { ref items, threshold: 1 } if items.len() == 2); + assert_matches!(policy_unsigned.satisfaction, Satisfaction::Partial { n: 2, m: 1, items, .. } if items.is_empty()); + + assert_matches!(policy_signed.item, SatisfiableItem::Thresh { ref items, threshold: 1 } if items.len() == 2); + assert_matches!(policy_signed.satisfaction, Satisfaction::PartialComplete { n: 2, m: 1, items, .. } if items == vec![0, 1]); + + let satisfied_items = match policy_signed.item { + SatisfiableItem::Thresh { items, .. } => items, + _ => unreachable!(), + }; + + assert_eq!( + satisfied_items[0].satisfaction, + Satisfaction::Complete { + condition: Default::default() + } + ); + assert_eq!( + satisfied_items[1].satisfaction, + Satisfaction::Complete { + condition: Default::default() + } + ); + } +} diff --git a/crates/bdk/src/descriptor/template.rs b/crates/bdk/src/descriptor/template.rs new file mode 100644 index 00000000..cf0296c9 --- /dev/null +++ b/crates/bdk/src/descriptor/template.rs @@ -0,0 +1,751 @@ +// Bitcoin Dev Kit +// Written in 2020 by Alekos Filini +// +// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Descriptor templates +//! +//! This module contains the definition of various common script templates that are ready to be +//! used. See the documentation of each template for an example. + +use bitcoin::util::bip32; +use bitcoin::Network; + +use miniscript::{Legacy, Segwitv0}; + +use super::{ExtendedDescriptor, IntoWalletDescriptor, KeyMap}; +use crate::descriptor::DescriptorError; +use crate::keys::{DerivableKey, IntoDescriptorKey, ValidNetworks}; +use crate::wallet::utils::SecpCtx; +use crate::{descriptor, KeychainKind}; + +/// Type alias for the return type of [`DescriptorTemplate`], [`descriptor!`](crate::descriptor!) and others +pub type DescriptorTemplateOut = (ExtendedDescriptor, KeyMap, ValidNetworks); + +/// Trait for descriptor templates that can be built into a full descriptor +/// +/// Since [`IntoWalletDescriptor`] is implemented for any [`DescriptorTemplate`], they can also be +/// passed directly to the [`Wallet`](crate::Wallet) constructor. +/// +/// ## Example +/// +/// ``` +/// use bdk::descriptor::error::Error as DescriptorError; +/// use bdk::keys::{IntoDescriptorKey, KeyError}; +/// use bdk::miniscript::Legacy; +/// use bdk::template::{DescriptorTemplate, DescriptorTemplateOut}; +/// use bitcoin::Network; +/// +/// struct MyP2PKH>(K); +/// +/// impl> DescriptorTemplate for MyP2PKH { +/// fn build(self, network: Network) -> Result { +/// Ok(bdk::descriptor!(pkh(self.0))?) +/// } +/// } +/// ``` +pub trait DescriptorTemplate { + /// Build the complete descriptor + fn build(self, network: Network) -> Result; +} + +/// Turns a [`DescriptorTemplate`] into a valid wallet descriptor by calling its +/// [`build`](DescriptorTemplate::build) method +impl IntoWalletDescriptor for T { + fn into_wallet_descriptor( + self, + secp: &SecpCtx, + network: Network, + ) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> { + self.build(network)?.into_wallet_descriptor(secp, network) + } +} + +/// P2PKH template. Expands to a descriptor `pkh(key)` +/// +/// ## Example +/// +/// ``` +/// # use bdk::bitcoin::{PrivateKey, Network}; +/// # use bdk::Wallet; +/// # use bdk::wallet::AddressIndex::New; +/// use bdk::template::P2Pkh; +/// +/// let key = +/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?; +/// let mut wallet = Wallet::new_no_persist(P2Pkh(key), None, Network::Testnet)?; +/// +/// assert_eq!( +/// wallet.get_address(New).to_string(), +/// "mwJ8hxFYW19JLuc65RCTaP4v1rzVU8cVMT" +/// ); +/// # Ok::<_, Box>(()) +/// ``` +pub struct P2Pkh>(pub K); + +impl> DescriptorTemplate for P2Pkh { + fn build(self, _network: Network) -> Result { + descriptor!(pkh(self.0)) + } +} + +/// P2WPKH-P2SH template. Expands to a descriptor `sh(wpkh(key))` +/// +/// ## Example +/// +/// ``` +/// # use bdk::bitcoin::{PrivateKey, Network}; +/// # use bdk::Wallet; +/// use bdk::template::P2Wpkh_P2Sh; +/// use bdk::wallet::AddressIndex; +/// +/// let key = +/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?; +/// let mut wallet = Wallet::new_no_persist(P2Wpkh_P2Sh(key), None, Network::Testnet)?; +/// +/// assert_eq!( +/// wallet.get_address(AddressIndex::New).to_string(), +/// "2NB4ox5VDRw1ecUv6SnT3VQHPXveYztRqk5" +/// ); +/// # Ok::<_, Box>(()) +/// ``` +#[allow(non_camel_case_types)] +pub struct P2Wpkh_P2Sh>(pub K); + +impl> DescriptorTemplate for P2Wpkh_P2Sh { + fn build(self, _network: Network) -> Result { + descriptor!(sh(wpkh(self.0))) + } +} + +/// P2WPKH template. Expands to a descriptor `wpkh(key)` +/// +/// ## Example +/// +/// ``` +/// # use bdk::bitcoin::{PrivateKey, Network}; +/// # use bdk::{Wallet}; +/// use bdk::template::P2Wpkh; +/// use bdk::wallet::AddressIndex::New; +/// +/// let key = +/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?; +/// let mut wallet = Wallet::new_no_persist(P2Wpkh(key), None, Network::Testnet)?; +/// +/// assert_eq!( +/// wallet.get_address(New).to_string(), +/// "tb1q4525hmgw265tl3drrl8jjta7ayffu6jf68ltjd" +/// ); +/// # Ok::<_, Box>(()) +/// ``` +pub struct P2Wpkh>(pub K); + +impl> DescriptorTemplate for P2Wpkh { + fn build(self, _network: Network) -> Result { + descriptor!(wpkh(self.0)) + } +} + +/// BIP44 template. Expands to `pkh(key/44'/{0,1}'/0'/{0,1}/*)` +/// +/// Since there are hardened derivation steps, this template requires a private derivable key (generally a `xprv`/`tprv`). +/// +/// See [`Bip44Public`] for a template that can work with a `xpub`/`tpub`. +/// +/// ## Example +/// +/// ``` +/// # use std::str::FromStr; +/// # use bdk::bitcoin::{PrivateKey, Network}; +/// # use bdk::{Wallet, KeychainKind}; +/// # use bdk::wallet::AddressIndex::New; +/// use bdk::template::Bip44; +/// +/// let key = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?; +/// let mut wallet = Wallet::new_no_persist( +/// Bip44(key.clone(), KeychainKind::External), +/// Some(Bip44(key, KeychainKind::Internal)), +/// Network::Testnet, +/// )?; +/// +/// assert_eq!(wallet.get_address(New).to_string(), "mmogjc7HJEZkrLqyQYqJmxUqFaC7i4uf89"); +/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "pkh([c55b303f/44'/1'/0']tpubDCuorCpzvYS2LCD75BR46KHE8GdDeg1wsAgNZeNr6DaB5gQK1o14uErKwKLuFmeemkQ6N2m3rNgvctdJLyr7nwu2yia7413Hhg8WWE44cgT/0/*)#5wrnv0xt"); +/// # Ok::<_, Box>(()) +/// ``` +pub struct Bip44>(pub K, pub KeychainKind); + +impl> DescriptorTemplate for Bip44 { + fn build(self, network: Network) -> Result { + P2Pkh(legacy::make_bipxx_private(44, self.0, self.1, network)?).build(network) + } +} + +/// BIP44 public template. Expands to `pkh(key/{0,1}/*)` +/// +/// This assumes that the key used has already been derived with `m/44'/0'/0'` for Mainnet or `m/44'/1'/0'` for Testnet. +/// +/// This template requires the parent fingerprint to populate correctly the metadata of PSBTs. +/// +/// See [`Bip44`] for a template that does the full derivation, but requires private data +/// for the key. +/// +/// ## Example +/// +/// ``` +/// # use std::str::FromStr; +/// # use bdk::bitcoin::{PrivateKey, Network}; +/// # use bdk::{Wallet, KeychainKind}; +/// # use bdk::wallet::AddressIndex::New; +/// use bdk::template::Bip44Public; +/// +/// let key = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU")?; +/// let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f")?; +/// let mut wallet = Wallet::new_no_persist( +/// Bip44Public(key.clone(), fingerprint, KeychainKind::External), +/// Some(Bip44Public(key, fingerprint, KeychainKind::Internal)), +/// Network::Testnet, +/// )?; +/// +/// assert_eq!(wallet.get_address(New).to_string(), "miNG7dJTzJqNbFS19svRdTCisC65dsubtR"); +/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "pkh([c55b303f/44'/1'/0']tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU/0/*)#cfhumdqz"); +/// # Ok::<_, Box>(()) +/// ``` +pub struct Bip44Public>(pub K, pub bip32::Fingerprint, pub KeychainKind); + +impl> DescriptorTemplate for Bip44Public { + fn build(self, network: Network) -> Result { + P2Pkh(legacy::make_bipxx_public( + 44, self.0, self.1, self.2, network, + )?) + .build(network) + } +} + +/// BIP49 template. Expands to `sh(wpkh(key/49'/{0,1}'/0'/{0,1}/*))` +/// +/// Since there are hardened derivation steps, this template requires a private derivable key (generally a `xprv`/`tprv`). +/// +/// See [`Bip49Public`] for a template that can work with a `xpub`/`tpub`. +/// +/// ## Example +/// +/// ``` +/// # use std::str::FromStr; +/// # use bdk::bitcoin::{PrivateKey, Network}; +/// # use bdk::{Wallet, KeychainKind}; +/// # use bdk::wallet::AddressIndex::New; +/// use bdk::template::Bip49; +/// +/// let key = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?; +/// let mut wallet = Wallet::new_no_persist( +/// Bip49(key.clone(), KeychainKind::External), +/// Some(Bip49(key, KeychainKind::Internal)), +/// Network::Testnet, +/// )?; +/// +/// assert_eq!(wallet.get_address(New).to_string(), "2N4zkWAoGdUv4NXhSsU8DvS5MB36T8nKHEB"); +/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "sh(wpkh([c55b303f/49'/1'/0']tpubDDYr4kdnZgjjShzYNjZUZXUUtpXaofdkMaipyS8ThEh45qFmhT4hKYways7UXmg6V7het1QiFo9kf4kYUXyDvV4rHEyvSpys9pjCB3pukxi/0/*))#s9vxlc8e"); +/// # Ok::<_, Box>(()) +/// ``` +pub struct Bip49>(pub K, pub KeychainKind); + +impl> DescriptorTemplate for Bip49 { + fn build(self, network: Network) -> Result { + P2Wpkh_P2Sh(segwit_v0::make_bipxx_private(49, self.0, self.1, network)?).build(network) + } +} + +/// BIP49 public template. Expands to `sh(wpkh(key/{0,1}/*))` +/// +/// This assumes that the key used has already been derived with `m/49'/0'/0'` for Mainnet or `m/49'/1'/0'` for Testnet. +/// +/// This template requires the parent fingerprint to populate correctly the metadata of PSBTs. +/// +/// See [`Bip49`] for a template that does the full derivation, but requires private data +/// for the key. +/// +/// ## Example +/// +/// ``` +/// # use std::str::FromStr; +/// # use bdk::bitcoin::{PrivateKey, Network}; +/// # use bdk::{Wallet, KeychainKind}; +/// # use bdk::wallet::AddressIndex::New; +/// use bdk::template::Bip49Public; +/// +/// let key = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L")?; +/// let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f")?; +/// let mut wallet = Wallet::new_no_persist( +/// Bip49Public(key.clone(), fingerprint, KeychainKind::External), +/// Some(Bip49Public(key, fingerprint, KeychainKind::Internal)), +/// Network::Testnet, +/// )?; +/// +/// assert_eq!(wallet.get_address(New).to_string(), "2N3K4xbVAHoiTQSwxkZjWDfKoNC27pLkYnt"); +/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "sh(wpkh([c55b303f/49'/1'/0']tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L/0/*))#3tka9g0q"); +/// # Ok::<_, Box>(()) +/// ``` +pub struct Bip49Public>(pub K, pub bip32::Fingerprint, pub KeychainKind); + +impl> DescriptorTemplate for Bip49Public { + fn build(self, network: Network) -> Result { + P2Wpkh_P2Sh(segwit_v0::make_bipxx_public( + 49, self.0, self.1, self.2, network, + )?) + .build(network) + } +} + +/// BIP84 template. Expands to `wpkh(key/84'/{0,1}'/0'/{0,1}/*)` +/// +/// Since there are hardened derivation steps, this template requires a private derivable key (generally a `xprv`/`tprv`). +/// +/// See [`Bip84Public`] for a template that can work with a `xpub`/`tpub`. +/// +/// ## Example +/// +/// ``` +/// # use std::str::FromStr; +/// # use bdk::bitcoin::{PrivateKey, Network}; +/// # use bdk::{Wallet, KeychainKind}; +/// # use bdk::wallet::AddressIndex::New; +/// use bdk::template::Bip84; +/// +/// let key = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?; +/// let mut wallet = Wallet::new_no_persist( +/// Bip84(key.clone(), KeychainKind::External), +/// Some(Bip84(key, KeychainKind::Internal)), +/// Network::Testnet, +/// )?; +/// +/// assert_eq!(wallet.get_address(New).to_string(), "tb1qhl85z42h7r4su5u37rvvw0gk8j2t3n9y7zsg4n"); +/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "wpkh([c55b303f/84'/1'/0']tpubDDc5mum24DekpNw92t6fHGp8Gr2JjF9J7i4TZBtN6Vp8xpAULG5CFaKsfugWa5imhrQQUZKXe261asP5koDHo5bs3qNTmf3U3o4v9SaB8gg/0/*)#6kfecsmr"); +/// # Ok::<_, Box>(()) +/// ``` +pub struct Bip84>(pub K, pub KeychainKind); + +impl> DescriptorTemplate for Bip84 { + fn build(self, network: Network) -> Result { + P2Wpkh(segwit_v0::make_bipxx_private(84, self.0, self.1, network)?).build(network) + } +} + +/// BIP84 public template. Expands to `wpkh(key/{0,1}/*)` +/// +/// This assumes that the key used has already been derived with `m/84'/0'/0'` for Mainnet or `m/84'/1'/0'` for Testnet. +/// +/// This template requires the parent fingerprint to populate correctly the metadata of PSBTs. +/// +/// See [`Bip84`] for a template that does the full derivation, but requires private data +/// for the key. +/// +/// ## Example +/// +/// ``` +/// # use std::str::FromStr; +/// # use bdk::bitcoin::{PrivateKey, Network}; +/// # use bdk::{Wallet, KeychainKind}; +/// # use bdk::wallet::AddressIndex::New; +/// use bdk::template::Bip84Public; +/// +/// let key = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q")?; +/// let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f")?; +/// let mut wallet = Wallet::new_no_persist( +/// Bip84Public(key.clone(), fingerprint, KeychainKind::External), +/// Some(Bip84Public(key, fingerprint, KeychainKind::Internal)), +/// Network::Testnet, +/// )?; +/// +/// assert_eq!(wallet.get_address(New).to_string(), "tb1qedg9fdlf8cnnqfd5mks6uz5w4kgpk2pr6y4qc7"); +/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "wpkh([c55b303f/84'/1'/0']tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q/0/*)#dhu402yv"); +/// # Ok::<_, Box>(()) +/// ``` +pub struct Bip84Public>(pub K, pub bip32::Fingerprint, pub KeychainKind); + +impl> DescriptorTemplate for Bip84Public { + fn build(self, network: Network) -> Result { + P2Wpkh(segwit_v0::make_bipxx_public( + 84, self.0, self.1, self.2, network, + )?) + .build(network) + } +} + +macro_rules! expand_make_bipxx { + ( $mod_name:ident, $ctx:ty ) => { + mod $mod_name { + use super::*; + + pub(super) fn make_bipxx_private>( + bip: u32, + key: K, + keychain: KeychainKind, + network: Network, + ) -> Result, DescriptorError> { + let mut derivation_path = alloc::vec::Vec::with_capacity(4); + derivation_path.push(bip32::ChildNumber::from_hardened_idx(bip)?); + + match network { + Network::Bitcoin => { + derivation_path.push(bip32::ChildNumber::from_hardened_idx(0)?); + } + _ => { + derivation_path.push(bip32::ChildNumber::from_hardened_idx(1)?); + } + } + derivation_path.push(bip32::ChildNumber::from_hardened_idx(0)?); + + match keychain { + KeychainKind::External => { + derivation_path.push(bip32::ChildNumber::from_normal_idx(0)?) + } + KeychainKind::Internal => { + derivation_path.push(bip32::ChildNumber::from_normal_idx(1)?) + } + }; + + let derivation_path: bip32::DerivationPath = derivation_path.into(); + + Ok((key, derivation_path)) + } + pub(super) fn make_bipxx_public>( + bip: u32, + key: K, + parent_fingerprint: bip32::Fingerprint, + keychain: KeychainKind, + network: Network, + ) -> Result, DescriptorError> { + let derivation_path: bip32::DerivationPath = match keychain { + KeychainKind::External => vec![bip32::ChildNumber::from_normal_idx(0)?].into(), + KeychainKind::Internal => vec![bip32::ChildNumber::from_normal_idx(1)?].into(), + }; + + let source_path = bip32::DerivationPath::from(vec![ + bip32::ChildNumber::from_hardened_idx(bip)?, + match network { + Network::Bitcoin => bip32::ChildNumber::from_hardened_idx(0)?, + _ => bip32::ChildNumber::from_hardened_idx(1)?, + }, + bip32::ChildNumber::from_hardened_idx(0)?, + ]); + + Ok((key, (parent_fingerprint, source_path), derivation_path)) + } + } + }; +} + +expand_make_bipxx!(legacy, Legacy); +expand_make_bipxx!(segwit_v0, Segwitv0); + +#[cfg(test)] +mod test { + // test existing descriptor templates, make sure they are expanded to the right descriptors + + use alloc::{string::ToString, vec::Vec}; + use core::str::FromStr; + + use super::*; + use crate::descriptor::{DescriptorError, DescriptorMeta}; + use crate::keys::ValidNetworks; + use assert_matches::assert_matches; + use bitcoin::network::constants::Network::Regtest; + use miniscript::descriptor::{DescriptorPublicKey, KeyMap}; + use miniscript::Descriptor; + + // BIP44 `pkh(key/44'/{0,1}'/0'/{0,1}/*)` + #[test] + fn test_bip44_template_cointype() { + use bitcoin::util::bip32::ChildNumber::{self, Hardened}; + + let xprvkey = bitcoin::util::bip32::ExtendedPrivKey::from_str("xprv9s21ZrQH143K2fpbqApQL69a4oKdGVnVN52R82Ft7d1pSqgKmajF62acJo3aMszZb6qQ22QsVECSFxvf9uyxFUvFYQMq3QbtwtRSMjLAhMf").unwrap(); + assert_eq!(Network::Bitcoin, xprvkey.network); + let xdesc = Bip44(xprvkey, KeychainKind::Internal) + .build(Network::Bitcoin) + .unwrap(); + + if let ExtendedDescriptor::Pkh(pkh) = xdesc.0 { + let path: Vec = pkh.into_inner().full_derivation_path().into(); + let purpose = path.get(0).unwrap(); + assert_matches!(purpose, Hardened { index: 44 }); + let coin_type = path.get(1).unwrap(); + assert_matches!(coin_type, Hardened { index: 0 }); + } + + let tprvkey = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); + assert_eq!(Network::Testnet, tprvkey.network); + let tdesc = Bip44(tprvkey, KeychainKind::Internal) + .build(Network::Testnet) + .unwrap(); + + if let ExtendedDescriptor::Pkh(pkh) = tdesc.0 { + let path: Vec = pkh.into_inner().full_derivation_path().into(); + let purpose = path.get(0).unwrap(); + assert_matches!(purpose, Hardened { index: 44 }); + let coin_type = path.get(1).unwrap(); + assert_matches!(coin_type, Hardened { index: 1 }); + } + } + + // verify template descriptor generates expected address(es) + fn check( + desc: Result<(Descriptor, KeyMap, ValidNetworks), DescriptorError>, + is_witness: bool, + is_fixed: bool, + expected: &[&str], + ) { + let (desc, _key_map, _networks) = desc.unwrap(); + assert_eq!(desc.is_witness(), is_witness); + assert_eq!(!desc.has_wildcard(), is_fixed); + for i in 0..expected.len() { + let index = i as u32; + let child_desc = if !desc.has_wildcard() { + desc.at_derivation_index(0) + } else { + desc.at_derivation_index(index) + }; + let address = child_desc.address(Regtest).unwrap(); + assert_eq!(address.to_string(), *expected.get(i).unwrap()); + } + } + + // P2PKH + #[test] + fn test_p2ph_template() { + let prvkey = + bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um") + .unwrap(); + check( + P2Pkh(prvkey).build(Network::Bitcoin), + false, + true, + &["mwJ8hxFYW19JLuc65RCTaP4v1rzVU8cVMT"], + ); + + let pubkey = bitcoin::PublicKey::from_str( + "03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd", + ) + .unwrap(); + check( + P2Pkh(pubkey).build(Network::Bitcoin), + false, + true, + &["muZpTpBYhxmRFuCjLc7C6BBDF32C8XVJUi"], + ); + } + + // P2WPKH-P2SH `sh(wpkh(key))` + #[test] + fn test_p2wphp2sh_template() { + let prvkey = + bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um") + .unwrap(); + check( + P2Wpkh_P2Sh(prvkey).build(Network::Bitcoin), + true, + true, + &["2NB4ox5VDRw1ecUv6SnT3VQHPXveYztRqk5"], + ); + + let pubkey = bitcoin::PublicKey::from_str( + "03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd", + ) + .unwrap(); + check( + P2Wpkh_P2Sh(pubkey).build(Network::Bitcoin), + true, + true, + &["2N5LiC3CqzxDamRTPG1kiNv1FpNJQ7x28sb"], + ); + } + + // P2WPKH `wpkh(key)` + #[test] + fn test_p2wph_template() { + let prvkey = + bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um") + .unwrap(); + check( + P2Wpkh(prvkey).build(Network::Bitcoin), + true, + true, + &["bcrt1q4525hmgw265tl3drrl8jjta7ayffu6jfcwxx9y"], + ); + + let pubkey = bitcoin::PublicKey::from_str( + "03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd", + ) + .unwrap(); + check( + P2Wpkh(pubkey).build(Network::Bitcoin), + true, + true, + &["bcrt1qngw83fg8dz0k749cg7k3emc7v98wy0c7azaa6h"], + ); + } + + // BIP44 `pkh(key/44'/0'/0'/{0,1}/*)` + #[test] + fn test_bip44_template() { + let prvkey = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); + check( + Bip44(prvkey, KeychainKind::External).build(Network::Bitcoin), + false, + false, + &[ + "n453VtnjDHPyDt2fDstKSu7A3YCJoHZ5g5", + "mvfrrumXgTtwFPWDNUecBBgzuMXhYM7KRP", + "mzYvhRAuQqbdSKMVVzXNYyqihgNdRadAUQ", + ], + ); + check( + Bip44(prvkey, KeychainKind::Internal).build(Network::Bitcoin), + false, + false, + &[ + "muHF98X9KxEzdKrnFAX85KeHv96eXopaip", + "n4hpyLJE5ub6B5Bymv4eqFxS5KjrewSmYR", + "mgvkdv1ffmsXd2B1sRKQ5dByK3SzpG42rA", + ], + ); + } + + // BIP44 public `pkh(key/{0,1}/*)` + #[test] + fn test_bip44_public_template() { + let pubkey = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU").unwrap(); + let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f").unwrap(); + check( + Bip44Public(pubkey, fingerprint, KeychainKind::External).build(Network::Bitcoin), + false, + false, + &[ + "miNG7dJTzJqNbFS19svRdTCisC65dsubtR", + "n2UqaDbCjWSFJvpC84m3FjUk5UaeibCzYg", + "muCPpS6Ue7nkzeJMWDViw7Lkwr92Yc4K8g", + ], + ); + check( + Bip44Public(pubkey, fingerprint, KeychainKind::Internal).build(Network::Bitcoin), + false, + false, + &[ + "moDr3vJ8wpt5nNxSK55MPq797nXJb2Ru9H", + "ms7A1Yt4uTezT2XkefW12AvLoko8WfNJMG", + "mhYiyat2rtEnV77cFfQsW32y1m2ceCGHPo", + ], + ); + } + + // BIP49 `sh(wpkh(key/49'/0'/0'/{0,1}/*))` + #[test] + fn test_bip49_template() { + let prvkey = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); + check( + Bip49(prvkey, KeychainKind::External).build(Network::Bitcoin), + true, + false, + &[ + "2N9bCAJXGm168MjVwpkBdNt6ucka3PKVoUV", + "2NDckYkqrYyDMtttEav5hB3Bfw9EGAW5HtS", + "2NAFTVtksF9T4a97M7nyCjwUBD24QevZ5Z4", + ], + ); + check( + Bip49(prvkey, KeychainKind::Internal).build(Network::Bitcoin), + true, + false, + &[ + "2NB3pA8PnzJLGV8YEKNDFpbViZv3Bm1K6CG", + "2NBiX2Wzxngb5rPiWpUiJQ2uLVB4HBjFD4p", + "2NA8ek4CdQ6aMkveYF6AYuEYNrftB47QGTn", + ], + ); + } + + // BIP49 public `sh(wpkh(key/{0,1}/*))` + #[test] + fn test_bip49_public_template() { + let pubkey = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L").unwrap(); + let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f").unwrap(); + check( + Bip49Public(pubkey, fingerprint, KeychainKind::External).build(Network::Bitcoin), + true, + false, + &[ + "2N3K4xbVAHoiTQSwxkZjWDfKoNC27pLkYnt", + "2NCTQfJ1sZa3wQ3pPseYRHbaNEpC3AquEfX", + "2MveFxAuC8BYPzTybx7FxSzW8HSd8ATT4z7", + ], + ); + check( + Bip49Public(pubkey, fingerprint, KeychainKind::Internal).build(Network::Bitcoin), + true, + false, + &[ + "2NF2vttKibwyxigxtx95Zw8K7JhDbo5zPVJ", + "2Mtmyd8taksxNVWCJ4wVvaiss7QPZGcAJuH", + "2NBs3CTVYPr1HCzjB4YFsnWCPCtNg8uMEfp", + ], + ); + } + + // BIP84 `wpkh(key/84'/0'/0'/{0,1}/*)` + #[test] + fn test_bip84_template() { + let prvkey = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); + check( + Bip84(prvkey, KeychainKind::External).build(Network::Bitcoin), + true, + false, + &[ + "bcrt1qkmvk2nadgplmd57ztld8nf8v2yxkzmdvwtjf8s", + "bcrt1qx0v6zgfwe50m4kqc58cqzcyem7ay2sfl3gvqhp", + "bcrt1q4h7fq9zhxst6e69p3n882nfj649l7w9g3zccfp", + ], + ); + check( + Bip84(prvkey, KeychainKind::Internal).build(Network::Bitcoin), + true, + false, + &[ + "bcrt1qtrwtz00wxl69e5xex7amy4xzlxkaefg3gfdkxa", + "bcrt1qqqasfhxpkkf7zrxqnkr2sfhn74dgsrc3e3ky45", + "bcrt1qpks7n0gq74hsgsz3phn5vuazjjq0f5eqhsgyce", + ], + ); + } + + // BIP84 public `wpkh(key/{0,1}/*)` + #[test] + fn test_bip84_public_template() { + let pubkey = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q").unwrap(); + let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f").unwrap(); + check( + Bip84Public(pubkey, fingerprint, KeychainKind::External).build(Network::Bitcoin), + true, + false, + &[ + "bcrt1qedg9fdlf8cnnqfd5mks6uz5w4kgpk2prcdvd0h", + "bcrt1q3lncdlwq3lgcaaeyruynjnlccr0ve0kakh6ana", + "bcrt1qt9800y6xl3922jy3uyl0z33jh5wfpycyhcylr9", + ], + ); + check( + Bip84Public(pubkey, fingerprint, KeychainKind::Internal).build(Network::Bitcoin), + true, + false, + &[ + "bcrt1qm6wqukenh7guu792lj2njgw9n78cmwsy8xy3z2", + "bcrt1q694twxtjn4nnrvnyvra769j0a23rllj5c6cgwp", + "bcrt1qhlac3c5ranv5w5emlnqs7wxhkxt8maelylcarp", + ], + ); + } +} diff --git a/crates/bdk/src/doctest.rs b/crates/bdk/src/doctest.rs new file mode 100644 index 00000000..6c1f772c --- /dev/null +++ b/crates/bdk/src/doctest.rs @@ -0,0 +1,14 @@ +// Bitcoin Dev Kit +// Written in 2020 by Alekos Filini +// +// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +#[doc = include_str!("../README.md")] +#[cfg(doctest)] +pub struct ReadmeDoctests; diff --git a/crates/bdk/src/error.rs b/crates/bdk/src/error.rs new file mode 100644 index 00000000..8cb349a5 --- /dev/null +++ b/crates/bdk/src/error.rs @@ -0,0 +1,291 @@ +// Bitcoin Dev Kit +// Written in 2020 by Alekos Filini +// +// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +use crate::bitcoin::Network; +use crate::{descriptor, wallet}; +use alloc::{string::String, vec::Vec}; +use bitcoin::{OutPoint, Txid}; +use core::fmt; + +/// Errors that can be thrown by the [`Wallet`](crate::wallet::Wallet) +#[derive(Debug)] +pub enum Error { + /// Wrong number of bytes found when trying to convert to u32 + InvalidU32Bytes(Vec), + /// Generic error + Generic(String), + /// This error is thrown when trying to convert Bare and Public key script to address + ScriptDoesntHaveAddressForm, + /// Cannot build a tx without recipients + NoRecipients, + /// `manually_selected_only` option is selected but no utxo has been passed + NoUtxosSelected, + /// Output created is under the dust limit, 546 satoshis + OutputBelowDustLimit(usize), + /// Wallet's UTXO set is not enough to cover recipient's requested plus fee + InsufficientFunds { + /// Sats needed for some transaction + needed: u64, + /// Sats available for spending + available: u64, + }, + /// Branch and bound coin selection possible attempts with sufficiently big UTXO set could grow + /// exponentially, thus a limit is set, and when hit, this error is thrown + BnBTotalTriesExceeded, + /// Branch and bound coin selection tries to avoid needing a change by finding the right inputs for + /// the desired outputs plus fee, if there is not such combination this error is thrown + BnBNoExactMatch, + /// Happens when trying to spend an UTXO that is not in the internal database + UnknownUtxo, + /// Thrown when a tx is not found in the internal database + TransactionNotFound, + /// Happens when trying to bump a transaction that is already confirmed + TransactionConfirmed, + /// Trying to replace a tx that has a sequence >= `0xFFFFFFFE` + IrreplaceableTransaction, + /// When bumping a tx the fee rate requested is lower than required + FeeRateTooLow { + /// Required fee rate (satoshi/vbyte) + required: crate::types::FeeRate, + }, + /// When bumping a tx the absolute fee requested is lower than replaced tx absolute fee + FeeTooLow { + /// Required fee absolute value (satoshi) + required: u64, + }, + /// Node doesn't have data to estimate a fee rate + FeeRateUnavailable, + /// In order to use the [`TxBuilder::add_global_xpubs`] option every extended + /// key in the descriptor must either be a master key itself (having depth = 0) or have an + /// explicit origin provided + /// + /// [`TxBuilder::add_global_xpubs`]: crate::wallet::tx_builder::TxBuilder::add_global_xpubs + MissingKeyOrigin(String), + /// Error while working with [`keys`](crate::keys) + Key(crate::keys::KeyError), + /// Descriptor checksum mismatch + ChecksumMismatch, + /// Spending policy is not compatible with this [`KeychainKind`](crate::types::KeychainKind) + SpendingPolicyRequired(crate::types::KeychainKind), + /// Error while extracting and manipulating policies + InvalidPolicyPathError(crate::descriptor::policy::PolicyError), + /// Signing error + Signer(crate::wallet::signer::SignerError), + /// Invalid network + InvalidNetwork { + /// requested network, for example what is given as bdk-cli option + requested: Network, + /// found network, for example the network of the bitcoin node + found: Network, + }, + + /// Progress value must be between `0.0` (included) and `100.0` (included) + InvalidProgressValue(f32), + /// Progress update error (maybe the channel has been closed) + ProgressUpdateError, + /// Requested outpoint doesn't exist in the tx (vout greater than available outputs) + InvalidOutpoint(OutPoint), + + /// Error related to the parsing and usage of descriptors + Descriptor(crate::descriptor::error::Error), + /// Encoding error + Encode(bitcoin::consensus::encode::Error), + /// Miniscript error + Miniscript(miniscript::Error), + /// Miniscript PSBT error + MiniscriptPsbt(MiniscriptPsbtError), + /// BIP32 error + Bip32(bitcoin::util::bip32::Error), + /// A secp256k1 error + Secp256k1(bitcoin::secp256k1::Error), + /// Error serializing or deserializing JSON data + Json(serde_json::Error), + /// Hex decoding error + Hex(bitcoin::hashes::hex::Error), + /// Partially signed bitcoin transaction error + Psbt(bitcoin::util::psbt::Error), + /// Partially signed bitcoin transaction parse error + PsbtParse(bitcoin::util::psbt::PsbtParseError), + + //KeyMismatch(bitcoin::secp256k1::PublicKey, bitcoin::secp256k1::PublicKey), + //MissingInputUTXO(usize), + //InvalidAddressNetwork(Address), + //DifferentTransactions, + //DifferentDescriptorStructure, + //Uncapable(crate::blockchain::Capability), + //MissingCachedAddresses, + /// [`crate::blockchain::WalletSync`] sync attempt failed due to missing scripts in cache which + /// are needed to satisfy `stop_gap`. + MissingCachedScripts(MissingCachedScripts), +} + +/// Errors returned by miniscript when updating inconsistent PSBTs +#[derive(Debug, Clone)] +pub enum MiniscriptPsbtError { + Conversion(miniscript::descriptor::ConversionError), + UtxoUpdate(miniscript::psbt::UtxoUpdateError), + OutputUpdate(miniscript::psbt::OutputUpdateError), +} + +impl fmt::Display for MiniscriptPsbtError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Conversion(err) => write!(f, "Conversion error: {}", err), + Self::UtxoUpdate(err) => write!(f, "UTXO update error: {}", err), + Self::OutputUpdate(err) => write!(f, "Output update error: {}", err), + } + } +} + +impl std::error::Error for MiniscriptPsbtError {} + +/// Represents the last failed [`crate::blockchain::WalletSync`] sync attempt in which we were short +/// on cached `scriptPubKey`s. +#[derive(Debug)] +pub struct MissingCachedScripts { + /// Number of scripts in which txs were requested during last request. + pub last_count: usize, + /// Minimum number of scripts to cache more of in order to satisfy `stop_gap`. + pub missing_count: usize, +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::InvalidU32Bytes(_) => write!( + f, + "Wrong number of bytes found when trying to convert to u32" + ), + Self::Generic(err) => write!(f, "Generic error: {}", err), + Self::ScriptDoesntHaveAddressForm => write!(f, "Script doesn't have address form"), + Self::NoRecipients => write!(f, "Cannot build tx without recipients"), + Self::NoUtxosSelected => write!(f, "No UTXO selected"), + Self::OutputBelowDustLimit(limit) => { + write!(f, "Output below the dust limit: {}", limit) + } + Self::InsufficientFunds { needed, available } => write!( + f, + "Insufficient funds: {} sat available of {} sat needed", + available, needed + ), + Self::BnBTotalTriesExceeded => { + write!(f, "Branch and bound coin selection: total tries exceeded") + } + Self::BnBNoExactMatch => write!(f, "Branch and bound coin selection: not exact match"), + Self::UnknownUtxo => write!(f, "UTXO not found in the internal database"), + Self::TransactionNotFound => { + write!(f, "Transaction not found in the internal database") + } + Self::TransactionConfirmed => write!(f, "Transaction already confirmed"), + Self::IrreplaceableTransaction => write!(f, "Transaction can't be replaced"), + Self::FeeRateTooLow { required } => write!( + f, + "Fee rate too low: required {} sat/vbyte", + required.as_sat_per_vb() + ), + Self::FeeTooLow { required } => write!(f, "Fee to low: required {} sat", required), + Self::FeeRateUnavailable => write!(f, "Fee rate unavailable"), + Self::MissingKeyOrigin(err) => write!(f, "Missing key origin: {}", err), + Self::Key(err) => write!(f, "Key error: {}", err), + Self::ChecksumMismatch => write!(f, "Descriptor checksum mismatch"), + Self::SpendingPolicyRequired(keychain_kind) => { + write!(f, "Spending policy required: {:?}", keychain_kind) + } + Self::InvalidPolicyPathError(err) => write!(f, "Invalid policy path: {}", err), + Self::Signer(err) => write!(f, "Signer error: {}", err), + Self::InvalidNetwork { requested, found } => write!( + f, + "Invalid network: requested {} but found {}", + requested, found + ), + #[cfg(feature = "verify")] + Self::Verification(err) => write!(f, "Transaction verification error: {}", err), + Self::InvalidProgressValue(progress) => { + write!(f, "Invalid progress value: {}", progress) + } + Self::ProgressUpdateError => write!( + f, + "Progress update error (maybe the channel has been closed)" + ), + Self::InvalidOutpoint(outpoint) => write!( + f, + "Requested outpoint doesn't exist in the tx: {}", + outpoint + ), + Self::Descriptor(err) => write!(f, "Descriptor error: {}", err), + Self::Encode(err) => write!(f, "Encoding error: {}", err), + Self::Miniscript(err) => write!(f, "Miniscript error: {}", err), + Self::MiniscriptPsbt(err) => write!(f, "Miniscript PSBT error: {}", err), + Self::Bip32(err) => write!(f, "BIP32 error: {}", err), + Self::Secp256k1(err) => write!(f, "Secp256k1 error: {}", err), + Self::Json(err) => write!(f, "Serialize/Deserialize JSON error: {}", err), + Self::Hex(err) => write!(f, "Hex decoding error: {}", err), + Self::Psbt(err) => write!(f, "PSBT error: {}", err), + Self::PsbtParse(err) => write!(f, "Impossible to parse PSBT: {}", err), + Self::MissingCachedScripts(missing_cached_scripts) => { + write!(f, "Missing cached scripts: {:?}", missing_cached_scripts) + } + #[cfg(feature = "electrum")] + Self::Electrum(err) => write!(f, "Electrum client error: {}", err), + #[cfg(feature = "esplora")] + Self::Esplora(err) => write!(f, "Esplora client error: {}", err), + #[cfg(feature = "compact_filters")] + Self::CompactFilters(err) => write!(f, "Compact filters client error: {}", err), + #[cfg(feature = "key-value-db")] + Self::Sled(err) => write!(f, "Sled database error: {}", err), + #[cfg(feature = "rpc")] + Self::Rpc(err) => write!(f, "RPC client error: {}", err), + #[cfg(feature = "sqlite")] + Self::Rusqlite(err) => write!(f, "SQLite error: {}", err), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for Error {} + +macro_rules! impl_error { + ( $from:ty, $to:ident ) => { + impl_error!($from, $to, Error); + }; + ( $from:ty, $to:ident, $impl_for:ty ) => { + impl core::convert::From<$from> for $impl_for { + fn from(err: $from) -> Self { + <$impl_for>::$to(err) + } + } + }; +} + +impl_error!(descriptor::error::Error, Descriptor); +impl_error!(descriptor::policy::PolicyError, InvalidPolicyPathError); +impl_error!(wallet::signer::SignerError, Signer); + +impl From for Error { + fn from(key_error: crate::keys::KeyError) -> Error { + match key_error { + crate::keys::KeyError::Miniscript(inner) => Error::Miniscript(inner), + crate::keys::KeyError::Bip32(inner) => Error::Bip32(inner), + crate::keys::KeyError::InvalidChecksum => Error::ChecksumMismatch, + e => Error::Key(e), + } + } +} + +impl_error!(bitcoin::consensus::encode::Error, Encode); +impl_error!(miniscript::Error, Miniscript); +impl_error!(MiniscriptPsbtError, MiniscriptPsbt); +impl_error!(bitcoin::util::bip32::Error, Bip32); +impl_error!(bitcoin::secp256k1::Error, Secp256k1); +impl_error!(serde_json::Error, Json); +impl_error!(bitcoin::hashes::hex::Error, Hex); +impl_error!(bitcoin::util::psbt::Error, Psbt); +impl_error!(bitcoin::util::psbt::PsbtParseError, PsbtParse); diff --git a/crates/bdk/src/keys/bip39.rs b/crates/bdk/src/keys/bip39.rs new file mode 100644 index 00000000..78f54493 --- /dev/null +++ b/crates/bdk/src/keys/bip39.rs @@ -0,0 +1,227 @@ +// Bitcoin Dev Kit +// Written in 2020 by Alekos Filini +// +// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! BIP-0039 + +// TODO: maybe write our own implementation of bip39? Seems stupid to have an extra dependency for +// something that should be fairly simple to re-implement. + +use alloc::string::String; +use bitcoin::util::bip32; +use bitcoin::Network; + +use miniscript::ScriptContext; + +pub use bip39::{Error, Language, Mnemonic}; + +type Seed = [u8; 64]; + +/// Type describing entropy length (aka word count) in the mnemonic +pub enum WordCount { + /// 12 words mnemonic (128 bits entropy) + Words12 = 128, + /// 15 words mnemonic (160 bits entropy) + Words15 = 160, + /// 18 words mnemonic (192 bits entropy) + Words18 = 192, + /// 21 words mnemonic (224 bits entropy) + Words21 = 224, + /// 24 words mnemonic (256 bits entropy) + Words24 = 256, +} + +use super::{ + any_network, DerivableKey, DescriptorKey, ExtendedKey, GeneratableKey, GeneratedKey, KeyError, +}; + +fn set_valid_on_any_network( + descriptor_key: DescriptorKey, +) -> DescriptorKey { + // We have to pick one network to build the xprv, but since the bip39 standard doesn't + // encode the network, the xprv we create is actually valid everywhere. So we override the + // valid networks with `any_network()`. + descriptor_key.override_valid_networks(any_network()) +} + +/// Type for a BIP39 mnemonic with an optional passphrase +pub type MnemonicWithPassphrase = (Mnemonic, Option); + +#[cfg_attr(docsrs, doc(cfg(feature = "keys-bip39")))] +impl DerivableKey for Seed { + fn into_extended_key(self) -> Result, KeyError> { + Ok(bip32::ExtendedPrivKey::new_master(Network::Bitcoin, &self[..])?.into()) + } + + fn into_descriptor_key( + self, + source: Option, + derivation_path: bip32::DerivationPath, + ) -> Result, KeyError> { + let descriptor_key = self + .into_extended_key()? + .into_descriptor_key(source, derivation_path)?; + + Ok(set_valid_on_any_network(descriptor_key)) + } +} + +#[cfg_attr(docsrs, doc(cfg(feature = "keys-bip39")))] +impl DerivableKey for MnemonicWithPassphrase { + fn into_extended_key(self) -> Result, KeyError> { + let (mnemonic, passphrase) = self; + let seed: Seed = mnemonic.to_seed(passphrase.as_deref().unwrap_or("")); + + seed.into_extended_key() + } + + fn into_descriptor_key( + self, + source: Option, + derivation_path: bip32::DerivationPath, + ) -> Result, KeyError> { + let descriptor_key = self + .into_extended_key()? + .into_descriptor_key(source, derivation_path)?; + + Ok(set_valid_on_any_network(descriptor_key)) + } +} + +#[cfg_attr(docsrs, doc(cfg(feature = "keys-bip39")))] +impl DerivableKey for (GeneratedKey, Option) { + fn into_extended_key(self) -> Result, KeyError> { + let (mnemonic, passphrase) = self; + (mnemonic.into_key(), passphrase).into_extended_key() + } + + fn into_descriptor_key( + self, + source: Option, + derivation_path: bip32::DerivationPath, + ) -> Result, KeyError> { + let (mnemonic, passphrase) = self; + (mnemonic.into_key(), passphrase).into_descriptor_key(source, derivation_path) + } +} + +#[cfg_attr(docsrs, doc(cfg(feature = "keys-bip39")))] +impl DerivableKey for Mnemonic { + fn into_extended_key(self) -> Result, KeyError> { + (self, None).into_extended_key() + } + + fn into_descriptor_key( + self, + source: Option, + derivation_path: bip32::DerivationPath, + ) -> Result, KeyError> { + let descriptor_key = self + .into_extended_key()? + .into_descriptor_key(source, derivation_path)?; + + Ok(set_valid_on_any_network(descriptor_key)) + } +} + +#[cfg_attr(docsrs, doc(cfg(feature = "keys-bip39")))] +impl GeneratableKey for Mnemonic { + type Entropy = [u8; 32]; + + type Options = (WordCount, Language); + type Error = Option; + + fn generate_with_entropy( + (word_count, language): Self::Options, + entropy: Self::Entropy, + ) -> Result, Self::Error> { + let entropy = &entropy.as_ref()[..(word_count as usize / 8)]; + let mnemonic = Mnemonic::from_entropy_in(language, entropy)?; + + Ok(GeneratedKey::new(mnemonic, any_network())) + } +} + +#[cfg(test)] +mod test { + use alloc::string::ToString; + use core::str::FromStr; + + use bitcoin::util::bip32; + + use bip39::{Language, Mnemonic}; + + use crate::keys::{any_network, GeneratableKey, GeneratedKey}; + + use super::WordCount; + + #[test] + fn test_keys_bip39_mnemonic() { + let mnemonic = + "aim bunker wash balance finish force paper analyst cabin spoon stable organ"; + let mnemonic = Mnemonic::parse_in(Language::English, mnemonic).unwrap(); + let path = bip32::DerivationPath::from_str("m/44'/0'/0'/0").unwrap(); + + let key = (mnemonic, path); + let (desc, keys, networks) = crate::descriptor!(wpkh(key)).unwrap(); + assert_eq!(desc.to_string(), "wpkh([be83839f/44'/0'/0']xpub6DCQ1YcqvZtSwGWMrwHELPehjWV3f2MGZ69yBADTxFEUAoLwb5Mp5GniQK6tTp3AgbngVz9zEFbBJUPVnkG7LFYt8QMTfbrNqs6FNEwAPKA/0/*)#0r8v4nkv"); + assert_eq!(keys.len(), 1); + assert_eq!(networks.len(), 4); + } + + #[test] + fn test_keys_bip39_mnemonic_passphrase() { + let mnemonic = + "aim bunker wash balance finish force paper analyst cabin spoon stable organ"; + let mnemonic = Mnemonic::parse_in(Language::English, mnemonic).unwrap(); + let path = bip32::DerivationPath::from_str("m/44'/0'/0'/0").unwrap(); + + let key = ((mnemonic, Some("passphrase".into())), path); + let (desc, keys, networks) = crate::descriptor!(wpkh(key)).unwrap(); + assert_eq!(desc.to_string(), "wpkh([8f6cb80c/44'/0'/0']xpub6DWYS8bbihFevy29M4cbw4ZR3P5E12jB8R88gBDWCTCNpYiDHhYWNywrCF9VZQYagzPmsZpxXpytzSoxynyeFr4ZyzheVjnpLKuse4fiwZw/0/*)#h0j0tg5m"); + assert_eq!(keys.len(), 1); + assert_eq!(networks.len(), 4); + } + + #[test] + fn test_keys_generate_bip39() { + let generated_mnemonic: GeneratedKey<_, miniscript::Segwitv0> = + Mnemonic::generate_with_entropy( + (WordCount::Words12, Language::English), + crate::keys::test::TEST_ENTROPY, + ) + .unwrap(); + assert_eq!(generated_mnemonic.valid_networks, any_network()); + assert_eq!( + generated_mnemonic.to_string(), + "primary fetch primary fetch primary fetch primary fetch primary fetch primary fever" + ); + + let generated_mnemonic: GeneratedKey<_, miniscript::Segwitv0> = + Mnemonic::generate_with_entropy( + (WordCount::Words24, Language::English), + crate::keys::test::TEST_ENTROPY, + ) + .unwrap(); + assert_eq!(generated_mnemonic.valid_networks, any_network()); + assert_eq!(generated_mnemonic.to_string(), "primary fetch primary fetch primary fetch primary fetch primary fetch primary fetch primary fetch primary fetch primary fetch primary fetch primary fetch primary foster"); + } + + #[test] + fn test_keys_generate_bip39_random() { + let generated_mnemonic: GeneratedKey<_, miniscript::Segwitv0> = + Mnemonic::generate((WordCount::Words12, Language::English)).unwrap(); + assert_eq!(generated_mnemonic.valid_networks, any_network()); + + let generated_mnemonic: GeneratedKey<_, miniscript::Segwitv0> = + Mnemonic::generate((WordCount::Words24, Language::English)).unwrap(); + assert_eq!(generated_mnemonic.valid_networks, any_network()); + } +} diff --git a/crates/bdk/src/keys/mod.rs b/crates/bdk/src/keys/mod.rs new file mode 100644 index 00000000..0f5e386e --- /dev/null +++ b/crates/bdk/src/keys/mod.rs @@ -0,0 +1,997 @@ +// Bitcoin Dev Kit +// Written in 2020 by Alekos Filini +// +// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Key formats + +use crate::collections::HashSet; +use alloc::string::{String, ToString}; +use alloc::vec::Vec; +use core::any::TypeId; +use core::marker::PhantomData; +use core::ops::Deref; +use core::str::FromStr; + +use bitcoin::secp256k1::{self, Secp256k1, Signing}; + +use bitcoin::util::bip32; +use bitcoin::{Network, PrivateKey, PublicKey, XOnlyPublicKey}; + +use miniscript::descriptor::{Descriptor, DescriptorXKey, Wildcard}; +pub use miniscript::descriptor::{ + DescriptorPublicKey, DescriptorSecretKey, KeyMap, SinglePriv, SinglePub, SinglePubKey, + SortedMultiVec, +}; +pub use miniscript::ScriptContext; +use miniscript::{Miniscript, Terminal}; + +use crate::descriptor::{CheckMiniscript, DescriptorError}; +use crate::wallet::utils::SecpCtx; + +#[cfg(feature = "keys-bip39")] +#[cfg_attr(docsrs, doc(cfg(feature = "keys-bip39")))] +pub mod bip39; + +/// Set of valid networks for a key +pub type ValidNetworks = HashSet; + +/// Create a set containing mainnet, testnet, signet, and regtest +pub fn any_network() -> ValidNetworks { + vec![ + Network::Bitcoin, + Network::Testnet, + Network::Regtest, + Network::Signet, + ] + .into_iter() + .collect() +} +/// Create a set only containing mainnet +pub fn mainnet_network() -> ValidNetworks { + vec![Network::Bitcoin].into_iter().collect() +} +/// Create a set containing testnet and regtest +pub fn test_networks() -> ValidNetworks { + vec![Network::Testnet, Network::Regtest, Network::Signet] + .into_iter() + .collect() +} +/// Compute the intersection of two sets +pub fn merge_networks(a: &ValidNetworks, b: &ValidNetworks) -> ValidNetworks { + a.intersection(b).cloned().collect() +} + +/// Container for public or secret keys +#[derive(Debug)] +pub enum DescriptorKey { + #[doc(hidden)] + Public(DescriptorPublicKey, ValidNetworks, PhantomData), + #[doc(hidden)] + Secret(DescriptorSecretKey, ValidNetworks, PhantomData), +} + +impl DescriptorKey { + /// Create an instance given a public key and a set of valid networks + pub fn from_public(public: DescriptorPublicKey, networks: ValidNetworks) -> Self { + DescriptorKey::Public(public, networks, PhantomData) + } + + /// Create an instance given a secret key and a set of valid networks + pub fn from_secret(secret: DescriptorSecretKey, networks: ValidNetworks) -> Self { + DescriptorKey::Secret(secret, networks, PhantomData) + } + + /// Override the computed set of valid networks + pub fn override_valid_networks(self, networks: ValidNetworks) -> Self { + match self { + DescriptorKey::Public(key, _, _) => DescriptorKey::Public(key, networks, PhantomData), + DescriptorKey::Secret(key, _, _) => DescriptorKey::Secret(key, networks, PhantomData), + } + } + + // This method is used internally by `bdk::fragment!` and `bdk::descriptor!`. It has to be + // public because it is effectively called by external crates once the macros are expanded, + // but since it is not meant to be part of the public api we hide it from the docs. + #[doc(hidden)] + pub fn extract( + self, + secp: &SecpCtx, + ) -> Result<(DescriptorPublicKey, KeyMap, ValidNetworks), KeyError> { + match self { + DescriptorKey::Public(public, valid_networks, _) => { + Ok((public, KeyMap::default(), valid_networks)) + } + DescriptorKey::Secret(secret, valid_networks, _) => { + let mut key_map = KeyMap::with_capacity(1); + + let public = secret + .to_public(secp) + .map_err(|e| miniscript::Error::Unexpected(e.to_string()))?; + key_map.insert(public.clone(), secret); + + Ok((public, key_map, valid_networks)) + } + } + } +} + +/// Enum representation of the known valid [`ScriptContext`]s +#[derive(Debug, Eq, PartialEq, Copy, Clone)] +pub enum ScriptContextEnum { + /// Legacy scripts + Legacy, + /// Segwitv0 scripts + Segwitv0, + /// Taproot scripts + Tap, +} + +impl ScriptContextEnum { + /// Returns whether the script context is [`ScriptContextEnum::Legacy`] + pub fn is_legacy(&self) -> bool { + self == &ScriptContextEnum::Legacy + } + + /// Returns whether the script context is [`ScriptContextEnum::Segwitv0`] + pub fn is_segwit_v0(&self) -> bool { + self == &ScriptContextEnum::Segwitv0 + } + + /// Returns whether the script context is [`ScriptContextEnum::Tap`] + pub fn is_taproot(&self) -> bool { + self == &ScriptContextEnum::Tap + } +} + +/// Trait that adds extra useful methods to [`ScriptContext`]s +pub trait ExtScriptContext: ScriptContext { + /// Returns the [`ScriptContext`] as a [`ScriptContextEnum`] + fn as_enum() -> ScriptContextEnum; + + /// Returns whether the script context is [`Legacy`](miniscript::Legacy) + fn is_legacy() -> bool { + Self::as_enum().is_legacy() + } + + /// Returns whether the script context is [`Segwitv0`](miniscript::Segwitv0) + fn is_segwit_v0() -> bool { + Self::as_enum().is_segwit_v0() + } + + /// Returns whether the script context is [`Tap`](miniscript::Tap), aka Taproot or Segwit V1 + fn is_taproot() -> bool { + Self::as_enum().is_taproot() + } +} + +impl ExtScriptContext for Ctx { + fn as_enum() -> ScriptContextEnum { + match TypeId::of::() { + t if t == TypeId::of::() => ScriptContextEnum::Legacy, + t if t == TypeId::of::() => ScriptContextEnum::Segwitv0, + t if t == TypeId::of::() => ScriptContextEnum::Tap, + _ => unimplemented!("Unknown ScriptContext type"), + } + } +} + +/// Trait for objects that can be turned into a public or secret [`DescriptorKey`] +/// +/// The generic type `Ctx` is used to define the context in which the key is valid: some key +/// formats, like the mnemonics used by Electrum wallets, encode internally whether the wallet is +/// legacy or segwit. Thus, trying to turn a valid legacy mnemonic into a `DescriptorKey` +/// that would become part of a segwit descriptor should fail. +/// +/// For key types that do care about this, the [`ExtScriptContext`] trait provides some useful +/// methods that can be used to check at runtime which `Ctx` is being used. +/// +/// For key types that can do this check statically (because they can only work within a +/// single `Ctx`), the "specialized" trait can be implemented to make the compiler handle the type +/// checking. +/// +/// Keys also have control over the networks they support: constructing the return object with +/// [`DescriptorKey::from_public`] or [`DescriptorKey::from_secret`] allows to specify a set of +/// [`ValidNetworks`]. +/// +/// ## Examples +/// +/// Key type valid in any context: +/// +/// ``` +/// use bdk::bitcoin::PublicKey; +/// +/// use bdk::keys::{DescriptorKey, IntoDescriptorKey, KeyError, ScriptContext}; +/// +/// pub struct MyKeyType { +/// pubkey: PublicKey, +/// } +/// +/// impl IntoDescriptorKey for MyKeyType { +/// fn into_descriptor_key(self) -> Result, KeyError> { +/// self.pubkey.into_descriptor_key() +/// } +/// } +/// ``` +/// +/// Key type that is only valid on mainnet: +/// +/// ``` +/// use bdk::bitcoin::PublicKey; +/// +/// use bdk::keys::{ +/// mainnet_network, DescriptorKey, DescriptorPublicKey, IntoDescriptorKey, KeyError, +/// ScriptContext, SinglePub, SinglePubKey, +/// }; +/// +/// pub struct MyKeyType { +/// pubkey: PublicKey, +/// } +/// +/// impl IntoDescriptorKey for MyKeyType { +/// fn into_descriptor_key(self) -> Result, KeyError> { +/// Ok(DescriptorKey::from_public( +/// DescriptorPublicKey::Single(SinglePub { +/// origin: None, +/// key: SinglePubKey::FullKey(self.pubkey), +/// }), +/// mainnet_network(), +/// )) +/// } +/// } +/// ``` +/// +/// Key type that internally encodes in which context it's valid. The context is checked at runtime: +/// +/// ``` +/// use bdk::bitcoin::PublicKey; +/// +/// use bdk::keys::{DescriptorKey, ExtScriptContext, IntoDescriptorKey, KeyError, ScriptContext}; +/// +/// pub struct MyKeyType { +/// is_legacy: bool, +/// pubkey: PublicKey, +/// } +/// +/// impl IntoDescriptorKey for MyKeyType { +/// fn into_descriptor_key(self) -> Result, KeyError> { +/// if Ctx::is_legacy() == self.is_legacy { +/// self.pubkey.into_descriptor_key() +/// } else { +/// Err(KeyError::InvalidScriptContext) +/// } +/// } +/// } +/// ``` +/// +/// Key type that can only work within [`miniscript::Segwitv0`] context. Only the specialized version +/// of the trait is implemented. +/// +/// This example deliberately fails to compile, to demonstrate how the compiler can catch when keys +/// are misused. In this case, the "segwit-only" key is used to build a `pkh()` descriptor, which +/// makes the compiler (correctly) fail. +/// +/// ```compile_fail +/// use bdk::bitcoin::PublicKey; +/// use core::str::FromStr; +/// +/// use bdk::keys::{DescriptorKey, IntoDescriptorKey, KeyError}; +/// +/// pub struct MySegwitOnlyKeyType { +/// pubkey: PublicKey, +/// } +/// +/// impl IntoDescriptorKey for MySegwitOnlyKeyType { +/// fn into_descriptor_key(self) -> Result, KeyError> { +/// self.pubkey.into_descriptor_key() +/// } +/// } +/// +/// let key = MySegwitOnlyKeyType { +/// pubkey: PublicKey::from_str("...")?, +/// }; +/// let (descriptor, _, _) = bdk::descriptor!(pkh(key))?; +/// // ^^^^^ changing this to `wpkh` would make it compile +/// +/// # Ok::<_, Box>(()) +/// ``` +pub trait IntoDescriptorKey: Sized { + /// Turn the key into a [`DescriptorKey`] within the requested [`ScriptContext`] + fn into_descriptor_key(self) -> Result, KeyError>; +} + +/// Enum for extended keys that can be either `xprv` or `xpub` +/// +/// An instance of [`ExtendedKey`] can be constructed from an [`ExtendedPrivKey`](bip32::ExtendedPrivKey) +/// or an [`ExtendedPubKey`](bip32::ExtendedPubKey) by using the `From` trait. +/// +/// Defaults to the [`Legacy`](miniscript::Legacy) context. +pub enum ExtendedKey { + /// A private extended key, aka an `xprv` + Private((bip32::ExtendedPrivKey, PhantomData)), + /// A public extended key, aka an `xpub` + Public((bip32::ExtendedPubKey, PhantomData)), +} + +impl ExtendedKey { + /// Return whether or not the key contains the private data + pub fn has_secret(&self) -> bool { + match self { + ExtendedKey::Private(_) => true, + ExtendedKey::Public(_) => false, + } + } + + /// Transform the [`ExtendedKey`] into an [`ExtendedPrivKey`](bip32::ExtendedPrivKey) for the + /// given [`Network`], if the key contains the private data + pub fn into_xprv(self, network: Network) -> Option { + match self { + ExtendedKey::Private((mut xprv, _)) => { + xprv.network = network; + Some(xprv) + } + ExtendedKey::Public(_) => None, + } + } + + /// Transform the [`ExtendedKey`] into an [`ExtendedPubKey`](bip32::ExtendedPubKey) for the + /// given [`Network`] + pub fn into_xpub( + self, + network: bitcoin::Network, + secp: &Secp256k1, + ) -> bip32::ExtendedPubKey { + let mut xpub = match self { + ExtendedKey::Private((xprv, _)) => bip32::ExtendedPubKey::from_priv(secp, &xprv), + ExtendedKey::Public((xpub, _)) => xpub, + }; + + xpub.network = network; + xpub + } +} + +impl From for ExtendedKey { + fn from(xpub: bip32::ExtendedPubKey) -> Self { + ExtendedKey::Public((xpub, PhantomData)) + } +} + +impl From for ExtendedKey { + fn from(xprv: bip32::ExtendedPrivKey) -> Self { + ExtendedKey::Private((xprv, PhantomData)) + } +} + +/// Trait for keys that can be derived. +/// +/// When extra metadata are provided, a [`DerivableKey`] can be transformed into a +/// [`DescriptorKey`]: the trait [`IntoDescriptorKey`] is automatically implemented +/// for `(DerivableKey, DerivationPath)` and +/// `(DerivableKey, KeySource, DerivationPath)` tuples. +/// +/// For key types that don't encode any indication about the path to use (like bip39), it's +/// generally recommended to implement this trait instead of [`IntoDescriptorKey`]. The same +/// rules regarding script context and valid networks apply. +/// +/// ## Examples +/// +/// Key types that can be directly converted into an [`ExtendedPrivKey`] or +/// an [`ExtendedPubKey`] can implement only the required `into_extended_key()` method. +/// +/// ``` +/// use bdk::bitcoin; +/// use bdk::bitcoin::util::bip32; +/// use bdk::keys::{DerivableKey, ExtendedKey, KeyError, ScriptContext}; +/// +/// struct MyCustomKeyType { +/// key_data: bitcoin::PrivateKey, +/// chain_code: Vec, +/// network: bitcoin::Network, +/// } +/// +/// impl DerivableKey for MyCustomKeyType { +/// fn into_extended_key(self) -> Result, KeyError> { +/// let xprv = bip32::ExtendedPrivKey { +/// network: self.network, +/// depth: 0, +/// parent_fingerprint: bip32::Fingerprint::default(), +/// private_key: self.key_data.inner, +/// chain_code: bip32::ChainCode::from(self.chain_code.as_ref()), +/// child_number: bip32::ChildNumber::Normal { index: 0 }, +/// }; +/// +/// xprv.into_extended_key() +/// } +/// } +/// ``` +/// +/// Types that don't internally encode the [`Network`](bitcoin::Network) in which they are valid need some extra +/// steps to override the set of valid networks, otherwise only the network specified in the +/// [`ExtendedPrivKey`] or [`ExtendedPubKey`] will be considered valid. +/// +/// ``` +/// use bdk::bitcoin; +/// use bdk::bitcoin::util::bip32; +/// use bdk::keys::{ +/// any_network, DerivableKey, DescriptorKey, ExtendedKey, KeyError, ScriptContext, +/// }; +/// +/// struct MyCustomKeyType { +/// key_data: bitcoin::PrivateKey, +/// chain_code: Vec, +/// } +/// +/// impl DerivableKey for MyCustomKeyType { +/// fn into_extended_key(self) -> Result, KeyError> { +/// let xprv = bip32::ExtendedPrivKey { +/// network: bitcoin::Network::Bitcoin, // pick an arbitrary network here +/// depth: 0, +/// parent_fingerprint: bip32::Fingerprint::default(), +/// private_key: self.key_data.inner, +/// chain_code: bip32::ChainCode::from(self.chain_code.as_ref()), +/// child_number: bip32::ChildNumber::Normal { index: 0 }, +/// }; +/// +/// xprv.into_extended_key() +/// } +/// +/// fn into_descriptor_key( +/// self, +/// source: Option, +/// derivation_path: bip32::DerivationPath, +/// ) -> Result, KeyError> { +/// let descriptor_key = self +/// .into_extended_key()? +/// .into_descriptor_key(source, derivation_path)?; +/// +/// // Override the set of valid networks here +/// Ok(descriptor_key.override_valid_networks(any_network())) +/// } +/// } +/// ``` +/// +/// [`DerivationPath`]: (bip32::DerivationPath) +/// [`ExtendedPrivKey`]: (bip32::ExtendedPrivKey) +/// [`ExtendedPubKey`]: (bip32::ExtendedPubKey) +pub trait DerivableKey: Sized { + /// Consume `self` and turn it into an [`ExtendedKey`] + /// + /// This can be used to get direct access to `xprv`s and `xpub`s for types that implement this trait, + /// like [`Mnemonic`](bip39::Mnemonic) when the `keys-bip39` feature is enabled. + #[cfg_attr( + feature = "keys-bip39", + doc = r##" +```rust +use bdk::bitcoin::Network; +use bdk::keys::{DerivableKey, ExtendedKey}; +use bdk::keys::bip39::{Mnemonic, Language}; + +# fn main() -> Result<(), Box> { +let xkey: ExtendedKey = + Mnemonic::parse_in( + Language::English, + "jelly crash boy whisper mouse ecology tuna soccer memory million news short", + )? + .into_extended_key()?; +let xprv = xkey.into_xprv(Network::Bitcoin).unwrap(); +# Ok(()) } +``` +"## + )] + fn into_extended_key(self) -> Result, KeyError>; + + /// Consume `self` and turn it into a [`DescriptorKey`] by adding the extra metadata, such as + /// key origin and derivation path + fn into_descriptor_key( + self, + origin: Option, + derivation_path: bip32::DerivationPath, + ) -> Result, KeyError> { + match self.into_extended_key()? { + ExtendedKey::Private((xprv, _)) => DescriptorSecretKey::XPrv(DescriptorXKey { + origin, + xkey: xprv, + derivation_path, + wildcard: Wildcard::Unhardened, + }) + .into_descriptor_key(), + ExtendedKey::Public((xpub, _)) => DescriptorPublicKey::XPub(DescriptorXKey { + origin, + xkey: xpub, + derivation_path, + wildcard: Wildcard::Unhardened, + }) + .into_descriptor_key(), + } + } +} + +/// Identity conversion +impl DerivableKey for ExtendedKey { + fn into_extended_key(self) -> Result, KeyError> { + Ok(self) + } +} + +impl DerivableKey for bip32::ExtendedPubKey { + fn into_extended_key(self) -> Result, KeyError> { + Ok(self.into()) + } +} + +impl DerivableKey for bip32::ExtendedPrivKey { + fn into_extended_key(self) -> Result, KeyError> { + Ok(self.into()) + } +} + +/// Output of a [`GeneratableKey`] key generation +pub struct GeneratedKey { + key: K, + valid_networks: ValidNetworks, + phantom: PhantomData, +} + +impl GeneratedKey { + fn new(key: K, valid_networks: ValidNetworks) -> Self { + GeneratedKey { + key, + valid_networks, + phantom: PhantomData, + } + } + + /// Consumes `self` and returns the key + pub fn into_key(self) -> K { + self.key + } +} + +impl Deref for GeneratedKey { + type Target = K; + + fn deref(&self) -> &Self::Target { + &self.key + } +} + +impl Clone for GeneratedKey { + fn clone(&self) -> GeneratedKey { + GeneratedKey { + key: self.key.clone(), + valid_networks: self.valid_networks.clone(), + phantom: self.phantom, + } + } +} + +// Make generated "derivable" keys themselves "derivable". Also make sure they are assigned the +// right `valid_networks`. +impl DerivableKey for GeneratedKey +where + Ctx: ScriptContext, + K: DerivableKey, +{ + fn into_extended_key(self) -> Result, KeyError> { + self.key.into_extended_key() + } + + fn into_descriptor_key( + self, + origin: Option, + derivation_path: bip32::DerivationPath, + ) -> Result, KeyError> { + let descriptor_key = self.key.into_descriptor_key(origin, derivation_path)?; + Ok(descriptor_key.override_valid_networks(self.valid_networks)) + } +} + +// Make generated keys directly usable in descriptors, and make sure they get assigned the right +// `valid_networks`. +impl IntoDescriptorKey for GeneratedKey +where + Ctx: ScriptContext, + K: IntoDescriptorKey, +{ + fn into_descriptor_key(self) -> Result, KeyError> { + let desc_key = self.key.into_descriptor_key()?; + Ok(desc_key.override_valid_networks(self.valid_networks)) + } +} + +/// Trait for keys that can be generated +/// +/// The same rules about [`ScriptContext`] and [`ValidNetworks`] from [`IntoDescriptorKey`] apply. +/// +/// This trait is particularly useful when combined with [`DerivableKey`]: if `Self` +/// implements it, the returned [`GeneratedKey`] will also implement it. The same is true for +/// [`IntoDescriptorKey`]: the generated keys can be directly used in descriptors if `Self` is also +/// [`IntoDescriptorKey`]. +pub trait GeneratableKey: Sized { + /// Type specifying the amount of entropy required e.g. `[u8;32]` + type Entropy: AsMut<[u8]> + Default; + + /// Extra options required by the `generate_with_entropy` + type Options; + /// Returned error in case of failure + type Error: core::fmt::Debug; + + /// Generate a key given the extra options and the entropy + fn generate_with_entropy( + options: Self::Options, + entropy: Self::Entropy, + ) -> Result, Self::Error>; + + /// Generate a key given the options with a random entropy + fn generate(options: Self::Options) -> Result, Self::Error> { + use rand::{thread_rng, Rng}; + + let mut entropy = Self::Entropy::default(); + thread_rng().fill(entropy.as_mut()); + Self::generate_with_entropy(options, entropy) + } +} + +/// Trait that allows generating a key with the default options +/// +/// This trait is automatically implemented if the [`GeneratableKey::Options`] implements [`Default`]. +pub trait GeneratableDefaultOptions: GeneratableKey +where + Ctx: ScriptContext, + >::Options: Default, +{ + /// Generate a key with the default options and a given entropy + fn generate_with_entropy_default( + entropy: Self::Entropy, + ) -> Result, Self::Error> { + Self::generate_with_entropy(Default::default(), entropy) + } + + /// Generate a key with the default options and a random entropy + fn generate_default() -> Result, Self::Error> { + Self::generate(Default::default()) + } +} + +/// Automatic implementation of [`GeneratableDefaultOptions`] for [`GeneratableKey`]s where +/// `Options` implements `Default` +impl GeneratableDefaultOptions for K +where + Ctx: ScriptContext, + K: GeneratableKey, + >::Options: Default, +{ +} + +impl GeneratableKey for bip32::ExtendedPrivKey { + type Entropy = [u8; 32]; + + type Options = (); + type Error = bip32::Error; + + fn generate_with_entropy( + _: Self::Options, + entropy: Self::Entropy, + ) -> Result, Self::Error> { + // pick a arbitrary network here, but say that we support all of them + let xprv = bip32::ExtendedPrivKey::new_master(Network::Bitcoin, entropy.as_ref())?; + Ok(GeneratedKey::new(xprv, any_network())) + } +} + +/// Options for generating a [`PrivateKey`] +/// +/// Defaults to creating compressed keys, which save on-chain bytes and fees +#[derive(Debug, Copy, Clone)] +pub struct PrivateKeyGenerateOptions { + /// Whether the generated key should be "compressed" or not + pub compressed: bool, +} + +impl Default for PrivateKeyGenerateOptions { + fn default() -> Self { + PrivateKeyGenerateOptions { compressed: true } + } +} + +impl GeneratableKey for PrivateKey { + type Entropy = [u8; secp256k1::constants::SECRET_KEY_SIZE]; + + type Options = PrivateKeyGenerateOptions; + type Error = bip32::Error; + + fn generate_with_entropy( + options: Self::Options, + entropy: Self::Entropy, + ) -> Result, Self::Error> { + // pick a arbitrary network here, but say that we support all of them + let inner = secp256k1::SecretKey::from_slice(&entropy)?; + let private_key = PrivateKey { + compressed: options.compressed, + network: Network::Bitcoin, + inner, + }; + + Ok(GeneratedKey::new(private_key, any_network())) + } +} + +impl> IntoDescriptorKey + for (T, bip32::DerivationPath) +{ + fn into_descriptor_key(self) -> Result, KeyError> { + self.0.into_descriptor_key(None, self.1) + } +} + +impl> IntoDescriptorKey + for (T, bip32::KeySource, bip32::DerivationPath) +{ + fn into_descriptor_key(self) -> Result, KeyError> { + self.0.into_descriptor_key(Some(self.1), self.2) + } +} + +fn expand_multi_keys, Ctx: ScriptContext>( + pks: Vec, + secp: &SecpCtx, +) -> Result<(Vec, KeyMap, ValidNetworks), KeyError> { + let (pks, key_maps_networks): (Vec<_>, Vec<_>) = pks + .into_iter() + .map(|key| key.into_descriptor_key()?.extract(secp)) + .collect::, _>>()? + .into_iter() + .map(|(a, b, c)| (a, (b, c))) + .unzip(); + + let (key_map, valid_networks) = key_maps_networks.into_iter().fold( + (KeyMap::default(), any_network()), + |(mut keys_acc, net_acc), (key, net)| { + keys_acc.extend(key.into_iter()); + let net_acc = merge_networks(&net_acc, &net); + + (keys_acc, net_acc) + }, + ); + + Ok((pks, key_map, valid_networks)) +} + +// Used internally by `bdk::fragment!` to build `pk_k()` fragments +#[doc(hidden)] +pub fn make_pk, Ctx: ScriptContext>( + descriptor_key: Pk, + secp: &SecpCtx, +) -> Result<(Miniscript, KeyMap, ValidNetworks), DescriptorError> { + let (key, key_map, valid_networks) = descriptor_key.into_descriptor_key()?.extract(secp)?; + let minisc = Miniscript::from_ast(Terminal::PkK(key))?; + + minisc.check_miniscript()?; + + Ok((minisc, key_map, valid_networks)) +} + +// Used internally by `bdk::fragment!` to build `pk_h()` fragments +#[doc(hidden)] +pub fn make_pkh, Ctx: ScriptContext>( + descriptor_key: Pk, + secp: &SecpCtx, +) -> Result<(Miniscript, KeyMap, ValidNetworks), DescriptorError> { + let (key, key_map, valid_networks) = descriptor_key.into_descriptor_key()?.extract(secp)?; + let minisc = Miniscript::from_ast(Terminal::PkH(key))?; + + minisc.check_miniscript()?; + + Ok((minisc, key_map, valid_networks)) +} + +// Used internally by `bdk::fragment!` to build `multi()` fragments +#[doc(hidden)] +pub fn make_multi< + Pk: IntoDescriptorKey, + Ctx: ScriptContext, + V: Fn(usize, Vec) -> Terminal, +>( + thresh: usize, + variant: V, + pks: Vec, + secp: &SecpCtx, +) -> Result<(Miniscript, KeyMap, ValidNetworks), DescriptorError> { + let (pks, key_map, valid_networks) = expand_multi_keys(pks, secp)?; + let minisc = Miniscript::from_ast(variant(thresh, pks))?; + + minisc.check_miniscript()?; + + Ok((minisc, key_map, valid_networks)) +} + +// Used internally by `bdk::descriptor!` to build `sortedmulti()` fragments +#[doc(hidden)] +pub fn make_sortedmulti( + thresh: usize, + pks: Vec, + build_desc: F, + secp: &SecpCtx, +) -> Result<(Descriptor, KeyMap, ValidNetworks), DescriptorError> +where + Pk: IntoDescriptorKey, + Ctx: ScriptContext, + F: Fn( + usize, + Vec, + ) -> Result<(Descriptor, PhantomData), DescriptorError>, +{ + let (pks, key_map, valid_networks) = expand_multi_keys(pks, secp)?; + let descriptor = build_desc(thresh, pks)?.0; + + Ok((descriptor, key_map, valid_networks)) +} + +/// The "identity" conversion is used internally by some `bdk::fragment`s +impl IntoDescriptorKey for DescriptorKey { + fn into_descriptor_key(self) -> Result, KeyError> { + Ok(self) + } +} + +impl IntoDescriptorKey for DescriptorPublicKey { + fn into_descriptor_key(self) -> Result, KeyError> { + let networks = match self { + DescriptorPublicKey::Single(_) => any_network(), + DescriptorPublicKey::XPub(DescriptorXKey { xkey, .. }) + if xkey.network == Network::Bitcoin => + { + mainnet_network() + } + _ => test_networks(), + }; + + Ok(DescriptorKey::from_public(self, networks)) + } +} + +impl IntoDescriptorKey for PublicKey { + fn into_descriptor_key(self) -> Result, KeyError> { + DescriptorPublicKey::Single(SinglePub { + key: SinglePubKey::FullKey(self), + origin: None, + }) + .into_descriptor_key() + } +} + +impl IntoDescriptorKey for XOnlyPublicKey { + fn into_descriptor_key(self) -> Result, KeyError> { + DescriptorPublicKey::Single(SinglePub { + key: SinglePubKey::XOnly(self), + origin: None, + }) + .into_descriptor_key() + } +} + +impl IntoDescriptorKey for DescriptorSecretKey { + fn into_descriptor_key(self) -> Result, KeyError> { + let networks = match &self { + DescriptorSecretKey::Single(sk) if sk.key.network == Network::Bitcoin => { + mainnet_network() + } + DescriptorSecretKey::XPrv(DescriptorXKey { xkey, .. }) + if xkey.network == Network::Bitcoin => + { + mainnet_network() + } + _ => test_networks(), + }; + + Ok(DescriptorKey::from_secret(self, networks)) + } +} + +impl IntoDescriptorKey for &'_ str { + fn into_descriptor_key(self) -> Result, KeyError> { + DescriptorSecretKey::from_str(self) + .map_err(|e| KeyError::Message(e.to_string()))? + .into_descriptor_key() + } +} + +impl IntoDescriptorKey for PrivateKey { + fn into_descriptor_key(self) -> Result, KeyError> { + DescriptorSecretKey::Single(SinglePriv { + key: self, + origin: None, + }) + .into_descriptor_key() + } +} + +/// Errors thrown while working with [`keys`](crate::keys) +#[derive(Debug)] +pub enum KeyError { + /// The key cannot exist in the given script context + InvalidScriptContext, + /// The key is not valid for the given network + InvalidNetwork, + /// The key has an invalid checksum + InvalidChecksum, + + /// Custom error message + Message(String), + + /// BIP32 error + Bip32(bitcoin::util::bip32::Error), + /// Miniscript error + Miniscript(miniscript::Error), +} + +impl_error!(miniscript::Error, Miniscript, KeyError); +impl_error!(bitcoin::util::bip32::Error, Bip32, KeyError); + +impl std::fmt::Display for KeyError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::InvalidScriptContext => write!(f, "Invalid script context"), + Self::InvalidNetwork => write!(f, "Invalid network"), + Self::InvalidChecksum => write!(f, "Invalid checksum"), + Self::Message(err) => write!(f, "{}", err), + Self::Bip32(err) => write!(f, "BIP32 error: {}", err), + Self::Miniscript(err) => write!(f, "Miniscript error: {}", err), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for KeyError {} + +#[cfg(test)] +pub mod test { + use bitcoin::util::bip32; + + use super::*; + + pub const TEST_ENTROPY: [u8; 32] = [0xAA; 32]; + + #[test] + fn test_keys_generate_xprv() { + let generated_xprv: GeneratedKey<_, miniscript::Segwitv0> = + bip32::ExtendedPrivKey::generate_with_entropy_default(TEST_ENTROPY).unwrap(); + + assert_eq!(generated_xprv.valid_networks, any_network()); + assert_eq!(generated_xprv.to_string(), "xprv9s21ZrQH143K4Xr1cJyqTvuL2FWR8eicgY9boWqMBv8MDVUZ65AXHnzBrK1nyomu6wdcabRgmGTaAKawvhAno1V5FowGpTLVx3jxzE5uk3Q"); + } + + #[test] + fn test_keys_generate_wif() { + let generated_wif: GeneratedKey<_, miniscript::Segwitv0> = + bitcoin::PrivateKey::generate_with_entropy_default(TEST_ENTROPY).unwrap(); + + assert_eq!(generated_wif.valid_networks, any_network()); + assert_eq!( + generated_wif.to_string(), + "L2wTu6hQrnDMiFNWA5na6jB12ErGQqtXwqpSL7aWquJaZG8Ai3ch" + ); + } + + #[cfg(feature = "keys-bip39")] + #[test] + fn test_keys_wif_network_bip39() { + let xkey: ExtendedKey = bip39::Mnemonic::parse_in( + bip39::Language::English, + "jelly crash boy whisper mouse ecology tuna soccer memory million news short", + ) + .unwrap() + .into_extended_key() + .unwrap(); + let xprv = xkey.into_xprv(Network::Testnet).unwrap(); + + assert_eq!(xprv.network, Network::Testnet); + } +} diff --git a/crates/bdk/src/lib.rs b/crates/bdk/src/lib.rs new file mode 100644 index 00000000..e48d3561 --- /dev/null +++ b/crates/bdk/src/lib.rs @@ -0,0 +1,142 @@ +// Bitcoin Dev Kit +// Written in 2020 by Alekos Filini +// +// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. +// +// rustdoc will warn if there are missing docs +#![warn(missing_docs)] +// only enables the `doc_cfg` feature when +// the `docsrs` configuration attribute is defined +#![cfg_attr(docsrs, feature(doc_cfg))] +#![cfg_attr( + docsrs, + doc(html_logo_url = "https://github.com/bitcoindevkit/bdk/raw/master/static/bdk.png") +)] + +//! A modern, lightweight, descriptor-based wallet library written in Rust. +//! +//! # About +//! +//! The BDK library aims to be the core building block for Bitcoin wallets of any kind. +//! +//! * It uses [Miniscript](https://github.com/rust-bitcoin/rust-miniscript) to support descriptors with generalized conditions. This exact same library can be used to build +//! single-sig wallets, multisigs, timelocked contracts and more. +//! * It supports multiple blockchain backends and databases, allowing developers to choose exactly what's right for their projects. +//! * It is built to be cross-platform: the core logic works on desktop, mobile, and even WebAssembly. +//! * It is very easy to extend: developers can implement customized logic for blockchain backends, databases, signers, coin selection, and more, without having to fork and modify this library. +//! +//! ## Generate a few addresses +//! +//! ### Example +//! ``` +//! use bdk::{Wallet}; +//! use bdk::wallet::AddressIndex::New; +//! +//! fn main() -> Result<(), bdk::Error> { +//! let mut wallet = Wallet::new_no_persist( +//! "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)", +//! Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"), +//! bitcoin::Network::Testnet, +//! )?; +//! +//! println!("Address #0: {}", wallet.get_address(New)); +//! println!("Address #1: {}", wallet.get_address(New)); +//! println!("Address #2: {}", wallet.get_address(New)); +//! +//! Ok(()) +//! } +//! ``` +//! ## Sign a transaction +//! +//! ```no_run +//! use core::str::FromStr; +//! +//! use bitcoin::util::psbt::PartiallySignedTransaction as Psbt; +//! +//! use bdk::{Wallet, SignOptions}; +//! +//! fn main() -> Result<(), bdk::Error> { +//! let wallet = Wallet::new_no_persist( +//! "wpkh([c258d2e4/84h/1h/0h]tprv8griRPhA7342zfRyB6CqeKF8CJDXYu5pgnj1cjL1u2ngKcJha5jjTRimG82ABzJQ4MQe71CV54xfn25BbhCNfEGGJZnxvCDQCd6JkbvxW6h/0/*)", +//! Some("wpkh([c258d2e4/84h/1h/0h]tprv8griRPhA7342zfRyB6CqeKF8CJDXYu5pgnj1cjL1u2ngKcJha5jjTRimG82ABzJQ4MQe71CV54xfn25BbhCNfEGGJZnxvCDQCd6JkbvxW6h/1/*)"), +//! bitcoin::Network::Testnet, +//! )?; +//! +//! let psbt = "..."; +//! let mut psbt = Psbt::from_str(psbt)?; +//! +//! let finalized = wallet.sign(&mut psbt, SignOptions::default())?; +//! +//! Ok(()) +//! } +//! ``` +//! +//! # Feature flags +//! +//! BDK uses a set of [feature flags](https://doc.rust-lang.org/cargo/reference/manifest.html#the-features-section) +//! to reduce the amount of compiled code by allowing projects to only enable the features they need. +//! By default, BDK enables two internal features, `key-value-db` and `electrum`. +//! +//! If you are new to BDK we recommended that you use the default features which will enable +//! basic descriptor wallet functionality. More advanced users can disable the `default` features +//! (`--no-default-features`) and build the BDK library with only the features you need. + +//! Below is a list of the available feature flags and the additional functionality they provide. +//! +//! * `all-keys`: all features for working with bitcoin keys +//! * `async-interface`: async functions in bdk traits +//! * `keys-bip39`: [BIP-39](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki) mnemonic codes for generating deterministic keys + +#![no_std] +#[cfg(feature = "std")] +#[macro_use] +extern crate std; + +#[doc(hidden)] +#[macro_use] +pub extern crate alloc; + +pub extern crate bitcoin; +#[cfg(feature = "hardware-signer")] +pub extern crate hwi; +extern crate log; +pub extern crate miniscript; +extern crate serde; +extern crate serde_json; + +#[cfg(feature = "keys-bip39")] +extern crate bip39; + +#[allow(unused_imports)] +#[macro_use] +pub(crate) mod error; +pub mod descriptor; +#[cfg(feature = "test-md-docs")] +mod doctest; +pub mod keys; +pub mod psbt; +pub(crate) mod types; +pub mod wallet; + +pub use descriptor::template; +pub use descriptor::HdKeyPaths; +pub use error::Error; +pub use types::*; +pub use wallet::signer; +pub use wallet::signer::SignOptions; +pub use wallet::tx_builder::TxBuilder; +pub use wallet::Wallet; + +/// Get the version of BDK at runtime +pub fn version() -> &'static str { + env!("CARGO_PKG_VERSION", "unknown") +} + +pub use bdk_chain as chain; +pub(crate) use bdk_chain::collections; diff --git a/crates/bdk/src/psbt/mod.rs b/crates/bdk/src/psbt/mod.rs new file mode 100644 index 00000000..87243236 --- /dev/null +++ b/crates/bdk/src/psbt/mod.rs @@ -0,0 +1,79 @@ +// Bitcoin Dev Kit +// Written in 2020 by Alekos Filini +// +// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Additional functions on the `rust-bitcoin` `PartiallySignedTransaction` structure. + +use crate::FeeRate; +use alloc::vec::Vec; +use bitcoin::util::psbt::PartiallySignedTransaction as Psbt; +use bitcoin::TxOut; + +// TODO upstream the functions here to `rust-bitcoin`? + +/// Trait to add functions to extract utxos and calculate fees. +pub trait PsbtUtils { + /// Get the `TxOut` for the specified input index, if it doesn't exist in the PSBT `None` is returned. + fn get_utxo_for(&self, input_index: usize) -> Option; + + /// The total transaction fee amount, sum of input amounts minus sum of output amounts, in sats. + /// If the PSBT is missing a TxOut for an input returns None. + fn fee_amount(&self) -> Option; + + /// The transaction's fee rate. This value will only be accurate if calculated AFTER the + /// `PartiallySignedTransaction` is finalized and all witness/signature data is added to the + /// transaction. + /// If the PSBT is missing a TxOut for an input returns None. + fn fee_rate(&self) -> Option; +} + +impl PsbtUtils for Psbt { + #[allow(clippy::all)] // We want to allow `manual_map` but it is too new. + fn get_utxo_for(&self, input_index: usize) -> Option { + let tx = &self.unsigned_tx; + + if input_index >= tx.input.len() { + return None; + } + + if let Some(input) = self.inputs.get(input_index) { + if let Some(wit_utxo) = &input.witness_utxo { + Some(wit_utxo.clone()) + } else if let Some(in_tx) = &input.non_witness_utxo { + Some(in_tx.output[tx.input[input_index].previous_output.vout as usize].clone()) + } else { + None + } + } else { + None + } + } + + fn fee_amount(&self) -> Option { + let tx = &self.unsigned_tx; + let utxos: Option> = (0..tx.input.len()).map(|i| self.get_utxo_for(i)).collect(); + + utxos.map(|inputs| { + let input_amount: u64 = inputs.iter().map(|i| i.value).sum(); + let output_amount: u64 = self.unsigned_tx.output.iter().map(|o| o.value).sum(); + input_amount + .checked_sub(output_amount) + .expect("input amount must be greater than output amount") + }) + } + + fn fee_rate(&self) -> Option { + let fee_amount = self.fee_amount(); + fee_amount.map(|fee| { + let weight = self.clone().extract_tx().weight(); + FeeRate::from_wu(fee, weight) + }) + } +} diff --git a/crates/bdk/src/types.rs b/crates/bdk/src/types.rs new file mode 100644 index 00000000..4472508f --- /dev/null +++ b/crates/bdk/src/types.rs @@ -0,0 +1,333 @@ +// Bitcoin Dev Kit +// Written in 2020 by Alekos Filini +// +// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +use alloc::boxed::Box; +use core::convert::AsRef; +use core::ops::Sub; + +use bdk_chain::ConfirmationTime; +use bitcoin::blockdata::transaction::{OutPoint, Transaction, TxOut}; +use bitcoin::{hash_types::Txid, util::psbt}; + +use serde::{Deserialize, Serialize}; + +/// Types of keychains +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)] +pub enum KeychainKind { + /// External + External = 0, + /// Internal, usually used for change outputs + Internal = 1, +} + +impl KeychainKind { + /// Return [`KeychainKind`] as a byte + pub fn as_byte(&self) -> u8 { + match self { + KeychainKind::External => b'e', + KeychainKind::Internal => b'i', + } + } +} + +impl AsRef<[u8]> for KeychainKind { + fn as_ref(&self) -> &[u8] { + match self { + KeychainKind::External => b"e", + KeychainKind::Internal => b"i", + } + } +} + +/// Fee rate +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)] +// Internally stored as satoshi/vbyte +pub struct FeeRate(f32); + +impl FeeRate { + /// Create a new instance checking the value provided + /// + /// ## Panics + /// + /// Panics if the value is not [normal](https://doc.rust-lang.org/std/primitive.f32.html#method.is_normal) (except if it's a positive zero) or negative. + fn new_checked(value: f32) -> Self { + assert!(value.is_normal() || value == 0.0); + assert!(value.is_sign_positive()); + + FeeRate(value) + } + + /// Create a new instance of [`FeeRate`] given a float fee rate in sats/kwu + pub fn from_sat_per_kwu(sat_per_kwu: f32) -> Self { + FeeRate::new_checked(sat_per_kwu / 250.0_f32) + } + + /// Create a new instance of [`FeeRate`] given a float fee rate in sats/kvb + pub fn from_sat_per_kvb(sat_per_kvb: f32) -> Self { + FeeRate::new_checked(sat_per_kvb / 1000.0_f32) + } + + /// Create a new instance of [`FeeRate`] given a float fee rate in btc/kvbytes + /// + /// ## Panics + /// + /// Panics if the value is not [normal](https://doc.rust-lang.org/std/primitive.f32.html#method.is_normal) (except if it's a positive zero) or negative. + pub fn from_btc_per_kvb(btc_per_kvb: f32) -> Self { + FeeRate::new_checked(btc_per_kvb * 1e5) + } + + /// Create a new instance of [`FeeRate`] given a float fee rate in satoshi/vbyte + /// + /// ## Panics + /// + /// Panics if the value is not [normal](https://doc.rust-lang.org/std/primitive.f32.html#method.is_normal) (except if it's a positive zero) or negative. + pub fn from_sat_per_vb(sat_per_vb: f32) -> Self { + FeeRate::new_checked(sat_per_vb) + } + + /// Create a new [`FeeRate`] with the default min relay fee value + pub const fn default_min_relay_fee() -> Self { + FeeRate(1.0) + } + + /// Calculate fee rate from `fee` and weight units (`wu`). + pub fn from_wu(fee: u64, wu: usize) -> FeeRate { + Self::from_vb(fee, wu.vbytes()) + } + + /// Calculate fee rate from `fee` and `vbytes`. + pub fn from_vb(fee: u64, vbytes: usize) -> FeeRate { + let rate = fee as f32 / vbytes as f32; + Self::from_sat_per_vb(rate) + } + + /// Return the value as satoshi/vbyte + pub fn as_sat_per_vb(&self) -> f32 { + self.0 + } + + /// Calculate absolute fee in Satoshis using size in weight units. + pub fn fee_wu(&self, wu: usize) -> u64 { + self.fee_vb(wu.vbytes()) + } + + /// Calculate absolute fee in Satoshis using size in virtual bytes. + pub fn fee_vb(&self, vbytes: usize) -> u64 { + (self.as_sat_per_vb() * vbytes as f32).ceil() as u64 + } +} + +impl Default for FeeRate { + fn default() -> Self { + FeeRate::default_min_relay_fee() + } +} + +impl Sub for FeeRate { + type Output = Self; + + fn sub(self, other: FeeRate) -> Self::Output { + FeeRate(self.0 - other.0) + } +} + +/// Trait implemented by types that can be used to measure weight units. +pub trait Vbytes { + /// Convert weight units to virtual bytes. + fn vbytes(self) -> usize; +} + +impl Vbytes for usize { + fn vbytes(self) -> usize { + // ref: https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#transaction-size-calculations + (self as f32 / 4.0).ceil() as usize + } +} + +/// An unspent output owned by a [`Wallet`]. +/// +/// [`Wallet`]: crate::Wallet +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] +pub struct LocalUtxo { + /// Reference to a transaction output + pub outpoint: OutPoint, + /// Transaction output + pub txout: TxOut, + /// Type of keychain + pub keychain: KeychainKind, + /// Whether this UTXO is spent or not + pub is_spent: bool, + /// The derivation index for the script pubkey in the wallet + pub derivation_index: u32, + /// The confirmation time for transaction containing this utxo + pub confirmation_time: ConfirmationTime, +} + +/// A [`Utxo`] with its `satisfaction_weight`. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct WeightedUtxo { + /// The weight of the witness data and `scriptSig` expressed in [weight units]. This is used to + /// properly maintain the feerate when adding this input to a transaction during coin selection. + /// + /// [weight units]: https://en.bitcoin.it/wiki/Weight_units + pub satisfaction_weight: usize, + /// The UTXO + pub utxo: Utxo, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +/// An unspent transaction output (UTXO). +pub enum Utxo { + /// A UTXO owned by the local wallet. + Local(LocalUtxo), + /// A UTXO owned by another wallet. + Foreign { + /// The location of the output. + outpoint: OutPoint, + /// The information about the input we require to add it to a PSBT. + // Box it to stop the type being too big. + psbt_input: Box, + }, +} + +impl Utxo { + /// Get the location of the UTXO + pub fn outpoint(&self) -> OutPoint { + match &self { + Utxo::Local(local) => local.outpoint, + Utxo::Foreign { outpoint, .. } => *outpoint, + } + } + + /// Get the `TxOut` of the UTXO + pub fn txout(&self) -> &TxOut { + match &self { + Utxo::Local(local) => &local.txout, + Utxo::Foreign { + outpoint, + psbt_input, + } => { + if let Some(prev_tx) = &psbt_input.non_witness_utxo { + return &prev_tx.output[outpoint.vout as usize]; + } + + if let Some(txout) = &psbt_input.witness_utxo { + return txout; + } + + unreachable!("Foreign UTXOs will always have one of these set") + } + } + } +} + +/// A wallet transaction +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct TransactionDetails { + /// Optional transaction + pub transaction: Option, + /// Transaction id + pub txid: Txid, + /// Received value (sats) + /// Sum of owned outputs of this transaction. + pub received: u64, + /// Sent value (sats) + /// Sum of owned inputs of this transaction. + pub sent: u64, + /// Fee value in sats if it was available. + pub fee: Option, + /// If the transaction is confirmed, contains height and Unix timestamp of the block containing the + /// transaction, unconfirmed transaction contains `None`. + pub confirmation_time: ConfirmationTime, +} + +impl PartialOrd for TransactionDetails { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for TransactionDetails { + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + self.confirmation_time + .cmp(&other.confirmation_time) + .then_with(|| self.txid.cmp(&other.txid)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn can_store_feerate_in_const() { + const _MIN_RELAY: FeeRate = FeeRate::default_min_relay_fee(); + } + + #[test] + #[should_panic] + fn test_invalid_feerate_neg_zero() { + let _ = FeeRate::from_sat_per_vb(-0.0); + } + + #[test] + #[should_panic] + fn test_invalid_feerate_neg_value() { + let _ = FeeRate::from_sat_per_vb(-5.0); + } + + #[test] + #[should_panic] + fn test_invalid_feerate_nan() { + let _ = FeeRate::from_sat_per_vb(f32::NAN); + } + + #[test] + #[should_panic] + fn test_invalid_feerate_inf() { + let _ = FeeRate::from_sat_per_vb(f32::INFINITY); + } + + #[test] + fn test_valid_feerate_pos_zero() { + let _ = FeeRate::from_sat_per_vb(0.0); + } + + #[test] + fn test_fee_from_btc_per_kvb() { + let fee = FeeRate::from_btc_per_kvb(1e-5); + assert!((fee.as_sat_per_vb() - 1.0).abs() < f32::EPSILON); + } + + #[test] + fn test_fee_from_sat_per_vbyte() { + let fee = FeeRate::from_sat_per_vb(1.0); + assert!((fee.as_sat_per_vb() - 1.0).abs() < f32::EPSILON); + } + + #[test] + fn test_fee_default_min_relay_fee() { + let fee = FeeRate::default_min_relay_fee(); + assert!((fee.as_sat_per_vb() - 1.0).abs() < f32::EPSILON); + } + + #[test] + fn test_fee_from_sat_per_kvb() { + let fee = FeeRate::from_sat_per_kvb(1000.0); + assert!((fee.as_sat_per_vb() - 1.0).abs() < f32::EPSILON); + } + + #[test] + fn test_fee_from_sat_per_kwu() { + let fee = FeeRate::from_sat_per_kwu(250.0); + assert!((fee.as_sat_per_vb() - 1.0).abs() < f32::EPSILON); + } +} diff --git a/crates/bdk/src/wallet/coin_selection.rs b/crates/bdk/src/wallet/coin_selection.rs new file mode 100644 index 00000000..58ecf3c7 --- /dev/null +++ b/crates/bdk/src/wallet/coin_selection.rs @@ -0,0 +1,1437 @@ +// Bitcoin Dev Kit +// Written in 2020 by Alekos Filini +// +// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Coin selection +//! +//! This module provides the trait [`CoinSelectionAlgorithm`] that can be implemented to +//! define custom coin selection algorithms. +//! +//! You can specify a custom coin selection algorithm through the [`coin_selection`] method on +//! [`TxBuilder`]. [`DefaultCoinSelectionAlgorithm`] aliases the coin selection algorithm that will +//! be used if it is not explicitly set. +//! +//! [`TxBuilder`]: super::tx_builder::TxBuilder +//! [`coin_selection`]: super::tx_builder::TxBuilder::coin_selection +//! +//! ## Example +//! +//! ``` +//! # use std::str::FromStr; +//! # use bitcoin::*; +//! # use bdk::wallet::{self, coin_selection::*}; +//! # use bdk::*; +//! # use bdk::wallet::coin_selection::decide_change; +//! # const TXIN_BASE_WEIGHT: usize = (32 + 4 + 4) * 4; +//! #[derive(Debug)] +//! struct AlwaysSpendEverything; +//! +//! impl CoinSelectionAlgorithm for AlwaysSpendEverything { +//! fn coin_select( +//! &self, +//! required_utxos: Vec, +//! optional_utxos: Vec, +//! fee_rate: FeeRate, +//! target_amount: u64, +//! drain_script: &Script, +//! ) -> Result { +//! let mut selected_amount = 0; +//! let mut additional_weight = 0; +//! let all_utxos_selected = required_utxos +//! .into_iter() +//! .chain(optional_utxos) +//! .scan( +//! (&mut selected_amount, &mut additional_weight), +//! |(selected_amount, additional_weight), weighted_utxo| { +//! **selected_amount += weighted_utxo.utxo.txout().value; +//! **additional_weight += TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight; +//! Some(weighted_utxo.utxo) +//! }, +//! ) +//! .collect::>(); +//! let additional_fees = fee_rate.fee_wu(additional_weight); +//! let amount_needed_with_fees = additional_fees + target_amount; +//! if selected_amount < amount_needed_with_fees { +//! return Err(bdk::Error::InsufficientFunds { +//! needed: amount_needed_with_fees, +//! available: selected_amount, +//! }); +//! } +//! +//! let remaining_amount = selected_amount - amount_needed_with_fees; +//! +//! let excess = decide_change(remaining_amount, fee_rate, drain_script); +//! +//! Ok(CoinSelectionResult { +//! selected: all_utxos_selected, +//! fee_amount: additional_fees, +//! excess, +//! }) +//! } +//! } +//! +//! # let mut wallet = doctest_wallet!(); +//! // create wallet, sync, ... +//! +//! let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap(); +//! let (psbt, details) = { +//! let mut builder = wallet.build_tx().coin_selection(AlwaysSpendEverything); +//! builder.add_recipient(to_address.script_pubkey(), 50_000); +//! builder.finish()? +//! }; +//! +//! // inspect, sign, broadcast, ... +//! +//! # Ok::<(), bdk::Error>(()) +//! ``` + +use crate::types::FeeRate; +use crate::wallet::utils::IsDust; +use crate::WeightedUtxo; +use crate::{error::Error, Utxo}; + +use alloc::vec::Vec; +use bitcoin::consensus::encode::serialize; +use bitcoin::Script; + +use core::convert::TryInto; +use rand::seq::SliceRandom; + +/// Default coin selection algorithm used by [`TxBuilder`](super::tx_builder::TxBuilder) if not +/// overridden +pub type DefaultCoinSelectionAlgorithm = BranchAndBoundCoinSelection; + +// Base weight of a Txin, not counting the weight needed for satisfying it. +// prev_txid (32 bytes) + prev_vout (4 bytes) + sequence (4 bytes) +pub(crate) const TXIN_BASE_WEIGHT: usize = (32 + 4 + 4) * 4; + +#[derive(Debug)] +/// Remaining amount after performing coin selection +pub enum Excess { + /// It's not possible to create spendable output from excess using the current drain output + NoChange { + /// Threshold to consider amount as dust for this particular change script_pubkey + dust_threshold: u64, + /// Exceeding amount of current selection over outgoing value and fee costs + remaining_amount: u64, + /// The calculated fee for the drain TxOut with the selected script_pubkey + change_fee: u64, + }, + /// It's possible to create spendable output from excess using the current drain output + Change { + /// Effective amount available to create change after deducting the change output fee + amount: u64, + /// The deducted change output fee + fee: u64, + }, +} + +/// Result of a successful coin selection +#[derive(Debug)] +pub struct CoinSelectionResult { + /// List of outputs selected for use as inputs + pub selected: Vec, + /// Total fee amount for the selected utxos in satoshis + pub fee_amount: u64, + /// Remaining amount after deducing fees and outgoing outputs + pub excess: Excess, +} + +impl CoinSelectionResult { + /// The total value of the inputs selected. + pub fn selected_amount(&self) -> u64 { + self.selected.iter().map(|u| u.txout().value).sum() + } + + /// The total value of the inputs selected from the local wallet. + pub fn local_selected_amount(&self) -> u64 { + self.selected + .iter() + .filter_map(|u| match u { + Utxo::Local(_) => Some(u.txout().value), + _ => None, + }) + .sum() + } +} + +/// Trait for generalized coin selection algorithms +/// +/// This trait can be implemented to make the [`Wallet`](super::Wallet) use a customized coin +/// selection algorithm when it creates transactions. +/// +/// For an example see [this module](crate::wallet::coin_selection)'s documentation. +pub trait CoinSelectionAlgorithm: core::fmt::Debug { + /// Perform the coin selection + /// + /// - `database`: a reference to the wallet's database that can be used to lookup additional + /// details for a specific UTXO + /// - `required_utxos`: the utxos that must be spent regardless of `target_amount` with their + /// weight cost + /// - `optional_utxos`: the remaining available utxos to satisfy `target_amount` with their + /// weight cost + /// - `fee_rate`: fee rate to use + /// - `target_amount`: the outgoing amount in satoshis and the fees already + /// accumulated from added outputs and transaction’s header. + /// - `drain_script`: the script to use in case of change + #[allow(clippy::too_many_arguments)] + fn coin_select( + &self, + required_utxos: Vec, + optional_utxos: Vec, + fee_rate: FeeRate, + target_amount: u64, + drain_script: &Script, + ) -> Result; +} + +/// Simple and dumb coin selection +/// +/// This coin selection algorithm sorts the available UTXOs by value and then picks them starting +/// from the largest ones until the required amount is reached. +#[derive(Debug, Default, Clone, Copy)] +pub struct LargestFirstCoinSelection; + +impl CoinSelectionAlgorithm for LargestFirstCoinSelection { + fn coin_select( + &self, + required_utxos: Vec, + mut optional_utxos: Vec, + fee_rate: FeeRate, + target_amount: u64, + drain_script: &Script, + ) -> Result { + log::debug!( + "target_amount = `{}`, fee_rate = `{:?}`", + target_amount, + fee_rate + ); + + // We put the "required UTXOs" first and make sure the optional UTXOs are sorted, + // initially smallest to largest, before being reversed with `.rev()`. + let utxos = { + optional_utxos.sort_unstable_by_key(|wu| wu.utxo.txout().value); + required_utxos + .into_iter() + .map(|utxo| (true, utxo)) + .chain(optional_utxos.into_iter().rev().map(|utxo| (false, utxo))) + }; + + select_sorted_utxos(utxos, fee_rate, target_amount, drain_script) + } +} + +/// OldestFirstCoinSelection always picks the utxo with the smallest blockheight to add to the selected coins next +/// +/// This coin selection algorithm sorts the available UTXOs by blockheight and then picks them starting +/// from the oldest ones until the required amount is reached. +#[derive(Debug, Default, Clone, Copy)] +pub struct OldestFirstCoinSelection; + +impl CoinSelectionAlgorithm for OldestFirstCoinSelection { + fn coin_select( + &self, + required_utxos: Vec, + mut optional_utxos: Vec, + fee_rate: FeeRate, + target_amount: u64, + drain_script: &Script, + ) -> Result { + // We put the "required UTXOs" first and make sure the optional UTXOs are sorted from + // oldest to newest according to blocktime + // For utxo that doesn't exist in DB, they will have lowest priority to be selected + let utxos = { + optional_utxos.sort_unstable_by_key(|wu| match &wu.utxo { + Utxo::Local(local) => Some(local.confirmation_time), + Utxo::Foreign { .. } => None, + }); + + required_utxos + .into_iter() + .map(|utxo| (true, utxo)) + .chain(optional_utxos.into_iter().map(|utxo| (false, utxo))) + }; + + select_sorted_utxos(utxos, fee_rate, target_amount, drain_script) + } +} + +/// Decide if change can be created +/// +/// - `remaining_amount`: the amount in which the selected coins exceed the target amount +/// - `fee_rate`: required fee rate for the current selection +/// - `drain_script`: script to consider change creation +pub fn decide_change(remaining_amount: u64, fee_rate: FeeRate, drain_script: &Script) -> Excess { + // drain_output_len = size(len(script_pubkey)) + len(script_pubkey) + size(output_value) + let drain_output_len = serialize(drain_script).len() + 8usize; + let change_fee = fee_rate.fee_vb(drain_output_len); + let drain_val = remaining_amount.saturating_sub(change_fee); + + if drain_val.is_dust(drain_script) { + let dust_threshold = drain_script.dust_value().to_sat(); + Excess::NoChange { + dust_threshold, + change_fee, + remaining_amount, + } + } else { + Excess::Change { + amount: drain_val, + fee: change_fee, + } + } +} + +fn select_sorted_utxos( + utxos: impl Iterator, + fee_rate: FeeRate, + target_amount: u64, + drain_script: &Script, +) -> Result { + let mut selected_amount = 0; + let mut fee_amount = 0; + let selected = utxos + .scan( + (&mut selected_amount, &mut fee_amount), + |(selected_amount, fee_amount), (must_use, weighted_utxo)| { + if must_use || **selected_amount < target_amount + **fee_amount { + **fee_amount += + fee_rate.fee_wu(TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight); + **selected_amount += weighted_utxo.utxo.txout().value; + + log::debug!( + "Selected {}, updated fee_amount = `{}`", + weighted_utxo.utxo.outpoint(), + fee_amount + ); + + Some(weighted_utxo.utxo) + } else { + None + } + }, + ) + .collect::>(); + + let amount_needed_with_fees = target_amount + fee_amount; + if selected_amount < amount_needed_with_fees { + return Err(Error::InsufficientFunds { + needed: amount_needed_with_fees, + available: selected_amount, + }); + } + + let remaining_amount = selected_amount - amount_needed_with_fees; + + let excess = decide_change(remaining_amount, fee_rate, drain_script); + + Ok(CoinSelectionResult { + selected, + fee_amount, + excess, + }) +} + +#[derive(Debug, Clone)] +// Adds fee information to an UTXO. +struct OutputGroup { + weighted_utxo: WeightedUtxo, + // Amount of fees for spending a certain utxo, calculated using a certain FeeRate + fee: u64, + // The effective value of the UTXO, i.e., the utxo value minus the fee for spending it + effective_value: i64, +} + +impl OutputGroup { + fn new(weighted_utxo: WeightedUtxo, fee_rate: FeeRate) -> Self { + let fee = fee_rate.fee_wu(TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight); + let effective_value = weighted_utxo.utxo.txout().value as i64 - fee as i64; + OutputGroup { + weighted_utxo, + fee, + effective_value, + } + } +} + +/// Branch and bound coin selection +/// +/// Code adapted from Bitcoin Core's implementation and from Mark Erhardt Master's Thesis: +#[derive(Debug, Clone)] +pub struct BranchAndBoundCoinSelection { + size_of_change: u64, +} + +impl Default for BranchAndBoundCoinSelection { + fn default() -> Self { + Self { + // P2WPKH cost of change -> value (8 bytes) + script len (1 bytes) + script (22 bytes) + size_of_change: 8 + 1 + 22, + } + } +} + +impl BranchAndBoundCoinSelection { + /// Create new instance with target size for change output + pub fn new(size_of_change: u64) -> Self { + Self { size_of_change } + } +} + +const BNB_TOTAL_TRIES: usize = 100_000; + +impl CoinSelectionAlgorithm for BranchAndBoundCoinSelection { + fn coin_select( + &self, + required_utxos: Vec, + optional_utxos: Vec, + fee_rate: FeeRate, + target_amount: u64, + drain_script: &Script, + ) -> Result { + // Mapping every (UTXO, usize) to an output group + let required_utxos: Vec = required_utxos + .into_iter() + .map(|u| OutputGroup::new(u, fee_rate)) + .collect(); + + // Mapping every (UTXO, usize) to an output group, filtering UTXOs with a negative + // effective value + let optional_utxos: Vec = optional_utxos + .into_iter() + .map(|u| OutputGroup::new(u, fee_rate)) + .filter(|u| u.effective_value.is_positive()) + .collect(); + + let curr_value = required_utxos + .iter() + .fold(0, |acc, x| acc + x.effective_value); + + let curr_available_value = optional_utxos + .iter() + .fold(0, |acc, x| acc + x.effective_value); + + let cost_of_change = self.size_of_change as f32 * fee_rate.as_sat_per_vb(); + + // `curr_value` and `curr_available_value` are both the sum of *effective_values* of + // the UTXOs. For the optional UTXOs (curr_available_value) we filter out UTXOs with + // negative effective value, so it will always be positive. + // + // Since we are required to spend the required UTXOs (curr_value) we have to consider + // all their effective values, even when negative, which means that curr_value could + // be negative as well. + // + // If the sum of curr_value and curr_available_value is negative or lower than our target, + // we can immediately exit with an error, as it's guaranteed we will never find a solution + // if we actually run the BnB. + let total_value: Result = (curr_available_value + curr_value).try_into(); + match total_value { + Ok(v) if v >= target_amount => {} + _ => { + // Assume we spend all the UTXOs we can (all the required + all the optional with + // positive effective value), sum their value and their fee cost. + let (utxo_fees, utxo_value) = required_utxos + .iter() + .chain(optional_utxos.iter()) + .fold((0, 0), |(mut fees, mut value), utxo| { + fees += utxo.fee; + value += utxo.weighted_utxo.utxo.txout().value; + + (fees, value) + }); + + // Add to the target the fee cost of the UTXOs + return Err(Error::InsufficientFunds { + needed: target_amount + utxo_fees, + available: utxo_value, + }); + } + } + + let target_amount = target_amount + .try_into() + .expect("Bitcoin amount to fit into i64"); + + if curr_value > target_amount { + // remaining_amount can't be negative as that would mean the + // selection wasn't successful + // target_amount = amount_needed + (fee_amount - vin_fees) + let remaining_amount = (curr_value - target_amount) as u64; + + let excess = decide_change(remaining_amount, fee_rate, drain_script); + + return Ok(BranchAndBoundCoinSelection::calculate_cs_result( + vec![], + required_utxos, + excess, + )); + } + + Ok(self + .bnb( + required_utxos.clone(), + optional_utxos.clone(), + curr_value, + curr_available_value, + target_amount, + cost_of_change, + drain_script, + fee_rate, + ) + .unwrap_or_else(|_| { + self.single_random_draw( + required_utxos, + optional_utxos, + curr_value, + target_amount, + drain_script, + fee_rate, + ) + })) + } +} + +impl BranchAndBoundCoinSelection { + // TODO: make this more Rust-onic :) + // (And perhaps refactor with less arguments?) + #[allow(clippy::too_many_arguments)] + fn bnb( + &self, + required_utxos: Vec, + mut optional_utxos: Vec, + mut curr_value: i64, + mut curr_available_value: i64, + target_amount: i64, + cost_of_change: f32, + drain_script: &Script, + fee_rate: FeeRate, + ) -> Result { + // current_selection[i] will contain true if we are using optional_utxos[i], + // false otherwise. Note that current_selection.len() could be less than + // optional_utxos.len(), it just means that we still haven't decided if we should keep + // certain optional_utxos or not. + let mut current_selection: Vec = Vec::with_capacity(optional_utxos.len()); + + // Sort the utxo_pool + optional_utxos.sort_unstable_by_key(|a| a.effective_value); + optional_utxos.reverse(); + + // Contains the best selection we found + let mut best_selection = Vec::new(); + let mut best_selection_value = None; + + // Depth First search loop for choosing the UTXOs + for _ in 0..BNB_TOTAL_TRIES { + // Conditions for starting a backtrack + let mut backtrack = false; + // Cannot possibly reach target with the amount remaining in the curr_available_value, + // or the selected value is out of range. + // Go back and try other branch + if curr_value + curr_available_value < target_amount + || curr_value > target_amount + cost_of_change as i64 + { + backtrack = true; + } else if curr_value >= target_amount { + // Selected value is within range, there's no point in going forward. Start + // backtracking + backtrack = true; + + // If we found a solution better than the previous one, or if there wasn't previous + // solution, update the best solution + if best_selection_value.is_none() || curr_value < best_selection_value.unwrap() { + best_selection = current_selection.clone(); + best_selection_value = Some(curr_value); + } + + // If we found a perfect match, break here + if curr_value == target_amount { + break; + } + } + + // Backtracking, moving backwards + if backtrack { + // Walk backwards to find the last included UTXO that still needs to have its omission branch traversed. + while let Some(false) = current_selection.last() { + current_selection.pop(); + curr_available_value += optional_utxos[current_selection.len()].effective_value; + } + + if current_selection.last_mut().is_none() { + // We have walked back to the first utxo and no branch is untraversed. All solutions searched + // If best selection is empty, then there's no exact match + if best_selection.is_empty() { + return Err(Error::BnBNoExactMatch); + } + break; + } + + if let Some(c) = current_selection.last_mut() { + // Output was included on previous iterations, try excluding now. + *c = false; + } + + let utxo = &optional_utxos[current_selection.len() - 1]; + curr_value -= utxo.effective_value; + } else { + // Moving forwards, continuing down this branch + let utxo = &optional_utxos[current_selection.len()]; + + // Remove this utxo from the curr_available_value utxo amount + curr_available_value -= utxo.effective_value; + + // Inclusion branch first (Largest First Exploration) + current_selection.push(true); + curr_value += utxo.effective_value; + } + } + + // Check for solution + if best_selection.is_empty() { + return Err(Error::BnBTotalTriesExceeded); + } + + // Set output set + let selected_utxos = optional_utxos + .into_iter() + .zip(best_selection) + .filter_map(|(optional, is_in_best)| if is_in_best { Some(optional) } else { None }) + .collect::>(); + + let selected_amount = best_selection_value.unwrap(); + + // remaining_amount can't be negative as that would mean the + // selection wasn't successful + // target_amount = amount_needed + (fee_amount - vin_fees) + let remaining_amount = (selected_amount - target_amount) as u64; + + let excess = decide_change(remaining_amount, fee_rate, drain_script); + + Ok(BranchAndBoundCoinSelection::calculate_cs_result( + selected_utxos, + required_utxos, + excess, + )) + } + + #[allow(clippy::too_many_arguments)] + fn single_random_draw( + &self, + required_utxos: Vec, + mut optional_utxos: Vec, + curr_value: i64, + target_amount: i64, + drain_script: &Script, + fee_rate: FeeRate, + ) -> CoinSelectionResult { + optional_utxos.shuffle(&mut rand::thread_rng()); + let selected_utxos = optional_utxos.into_iter().fold( + (curr_value, vec![]), + |(mut amount, mut utxos), utxo| { + if amount >= target_amount { + (amount, utxos) + } else { + amount += utxo.effective_value; + utxos.push(utxo); + (amount, utxos) + } + }, + ); + + // remaining_amount can't be negative as that would mean the + // selection wasn't successful + // target_amount = amount_needed + (fee_amount - vin_fees) + let remaining_amount = (selected_utxos.0 - target_amount) as u64; + + let excess = decide_change(remaining_amount, fee_rate, drain_script); + + BranchAndBoundCoinSelection::calculate_cs_result(selected_utxos.1, required_utxos, excess) + } + + fn calculate_cs_result( + mut selected_utxos: Vec, + mut required_utxos: Vec, + excess: Excess, + ) -> CoinSelectionResult { + selected_utxos.append(&mut required_utxos); + let fee_amount = selected_utxos.iter().map(|u| u.fee).sum::(); + let selected = selected_utxos + .into_iter() + .map(|u| u.weighted_utxo.utxo) + .collect::>(); + + CoinSelectionResult { + selected, + fee_amount, + excess, + } + } +} + +#[cfg(test)] +mod test { + use assert_matches::assert_matches; + use core::str::FromStr; + + use bdk_chain::ConfirmationTime; + use bitcoin::{OutPoint, Script, TxOut}; + + use super::*; + use crate::types::*; + use crate::wallet::Vbytes; + + use rand::rngs::StdRng; + use rand::seq::SliceRandom; + use rand::{Rng, RngCore, SeedableRng}; + + // n. of items on witness (1WU) + signature len (1WU) + signature and sighash (72WU) + // + pubkey len (1WU) + pubkey (33WU) + script sig len (1 byte, 4WU) + const P2WPKH_SATISFACTION_SIZE: usize = 1 + 1 + 72 + 1 + 33 + 4; + + const FEE_AMOUNT: u64 = 50; + + fn utxo(value: u64, index: u32, confirmation_time: ConfirmationTime) -> WeightedUtxo { + assert!(index < 10); + let outpoint = OutPoint::from_str(&format!( + "000000000000000000000000000000000000000000000000000000000000000{}:0", + index + )) + .unwrap(); + WeightedUtxo { + satisfaction_weight: P2WPKH_SATISFACTION_SIZE, + utxo: Utxo::Local(LocalUtxo { + outpoint, + txout: TxOut { + value, + script_pubkey: Script::new(), + }, + keychain: KeychainKind::External, + is_spent: false, + derivation_index: 42, + confirmation_time, + }), + } + } + + fn get_test_utxos() -> Vec { + vec![ + utxo(100_000, 0, ConfirmationTime::Unconfirmed), + utxo(FEE_AMOUNT as u64 - 40, 1, ConfirmationTime::Unconfirmed), + utxo(200_000, 2, ConfirmationTime::Unconfirmed), + ] + } + + fn get_oldest_first_test_utxos() -> Vec { + // ensure utxos are from different tx + let utxo1 = utxo( + 120_000, + 1, + ConfirmationTime::Confirmed { + height: 1, + time: 1231006505, + }, + ); + let utxo2 = utxo( + 80_000, + 2, + ConfirmationTime::Confirmed { + height: 2, + time: 1231006505, + }, + ); + let utxo3 = utxo( + 300_000, + 3, + ConfirmationTime::Confirmed { + height: 3, + time: 1231006505, + }, + ); + vec![utxo1, utxo2, utxo3] + } + + fn generate_random_utxos(rng: &mut StdRng, utxos_number: usize) -> Vec { + let mut res = Vec::new(); + for _ in 0..utxos_number { + res.push(WeightedUtxo { + satisfaction_weight: P2WPKH_SATISFACTION_SIZE, + utxo: Utxo::Local(LocalUtxo { + outpoint: OutPoint::from_str( + "ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:0", + ) + .unwrap(), + txout: TxOut { + value: rng.gen_range(0..200000000), + script_pubkey: Script::new(), + }, + keychain: KeychainKind::External, + is_spent: false, + derivation_index: rng.next_u32(), + confirmation_time: if rng.gen_bool(0.5) { + ConfirmationTime::Confirmed { + height: rng.next_u32(), + time: rng.next_u64(), + } + } else { + ConfirmationTime::Unconfirmed + }, + }), + }); + } + res + } + + fn generate_same_value_utxos(utxos_value: u64, utxos_number: usize) -> Vec { + let utxo = WeightedUtxo { + satisfaction_weight: P2WPKH_SATISFACTION_SIZE, + utxo: Utxo::Local(LocalUtxo { + outpoint: OutPoint::from_str( + "ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:0", + ) + .unwrap(), + txout: TxOut { + value: utxos_value, + script_pubkey: Script::new(), + }, + keychain: KeychainKind::External, + is_spent: false, + derivation_index: 42, + confirmation_time: ConfirmationTime::Unconfirmed, + }), + }; + vec![utxo; utxos_number] + } + + fn sum_random_utxos(mut rng: &mut StdRng, utxos: &mut Vec) -> u64 { + let utxos_picked_len = rng.gen_range(2..utxos.len() / 2); + utxos.shuffle(&mut rng); + utxos[..utxos_picked_len] + .iter() + .map(|u| u.utxo.txout().value) + .sum() + } + + #[test] + fn test_largest_first_coin_selection_success() { + let utxos = get_test_utxos(); + let drain_script = Script::default(); + let target_amount = 250_000 + FEE_AMOUNT; + + let result = LargestFirstCoinSelection::default() + .coin_select( + utxos, + vec![], + FeeRate::from_sat_per_vb(1.0), + target_amount, + &drain_script, + ) + .unwrap(); + + assert_eq!(result.selected.len(), 3); + assert_eq!(result.selected_amount(), 300_010); + assert_eq!(result.fee_amount, 204) + } + + #[test] + fn test_largest_first_coin_selection_use_all() { + let utxos = get_test_utxos(); + let drain_script = Script::default(); + let target_amount = 20_000 + FEE_AMOUNT; + + let result = LargestFirstCoinSelection::default() + .coin_select( + utxos, + vec![], + FeeRate::from_sat_per_vb(1.0), + target_amount, + &drain_script, + ) + .unwrap(); + + assert_eq!(result.selected.len(), 3); + assert_eq!(result.selected_amount(), 300_010); + assert_eq!(result.fee_amount, 204); + } + + #[test] + fn test_largest_first_coin_selection_use_only_necessary() { + let utxos = get_test_utxos(); + let drain_script = Script::default(); + let target_amount = 20_000 + FEE_AMOUNT; + + let result = LargestFirstCoinSelection::default() + .coin_select( + vec![], + utxos, + FeeRate::from_sat_per_vb(1.0), + target_amount, + &drain_script, + ) + .unwrap(); + + assert_eq!(result.selected.len(), 1); + assert_eq!(result.selected_amount(), 200_000); + assert_eq!(result.fee_amount, 68); + } + + #[test] + #[should_panic(expected = "InsufficientFunds")] + fn test_largest_first_coin_selection_insufficient_funds() { + let utxos = get_test_utxos(); + let drain_script = Script::default(); + let target_amount = 500_000 + FEE_AMOUNT; + + LargestFirstCoinSelection::default() + .coin_select( + vec![], + utxos, + FeeRate::from_sat_per_vb(1.0), + target_amount, + &drain_script, + ) + .unwrap(); + } + + #[test] + #[should_panic(expected = "InsufficientFunds")] + fn test_largest_first_coin_selection_insufficient_funds_high_fees() { + let utxos = get_test_utxos(); + let drain_script = Script::default(); + let target_amount = 250_000 + FEE_AMOUNT; + + LargestFirstCoinSelection::default() + .coin_select( + vec![], + utxos, + FeeRate::from_sat_per_vb(1000.0), + target_amount, + &drain_script, + ) + .unwrap(); + } + + #[test] + fn test_oldest_first_coin_selection_success() { + let utxos = get_oldest_first_test_utxos(); + let drain_script = Script::default(); + let target_amount = 180_000 + FEE_AMOUNT; + + let result = OldestFirstCoinSelection::default() + .coin_select( + vec![], + utxos, + FeeRate::from_sat_per_vb(1.0), + target_amount, + &drain_script, + ) + .unwrap(); + + assert_eq!(result.selected.len(), 2); + assert_eq!(result.selected_amount(), 200_000); + assert_eq!(result.fee_amount, 136) + } + + #[test] + fn test_oldest_first_coin_selection_use_all() { + let utxos = get_oldest_first_test_utxos(); + let drain_script = Script::default(); + let target_amount = 20_000 + FEE_AMOUNT; + + let result = OldestFirstCoinSelection::default() + .coin_select( + utxos, + vec![], + FeeRate::from_sat_per_vb(1.0), + target_amount, + &drain_script, + ) + .unwrap(); + + assert_eq!(result.selected.len(), 3); + assert_eq!(result.selected_amount(), 500_000); + assert_eq!(result.fee_amount, 204); + } + + #[test] + fn test_oldest_first_coin_selection_use_only_necessary() { + let utxos = get_oldest_first_test_utxos(); + let drain_script = Script::default(); + let target_amount = 20_000 + FEE_AMOUNT; + + let result = OldestFirstCoinSelection::default() + .coin_select( + vec![], + utxos, + FeeRate::from_sat_per_vb(1.0), + target_amount, + &drain_script, + ) + .unwrap(); + + assert_eq!(result.selected.len(), 1); + assert_eq!(result.selected_amount(), 120_000); + assert_eq!(result.fee_amount, 68); + } + + #[test] + #[should_panic(expected = "InsufficientFunds")] + fn test_oldest_first_coin_selection_insufficient_funds() { + let utxos = get_oldest_first_test_utxos(); + let drain_script = Script::default(); + let target_amount = 600_000 + FEE_AMOUNT; + + OldestFirstCoinSelection::default() + .coin_select( + vec![], + utxos, + FeeRate::from_sat_per_vb(1.0), + target_amount, + &drain_script, + ) + .unwrap(); + } + + #[test] + #[should_panic(expected = "InsufficientFunds")] + fn test_oldest_first_coin_selection_insufficient_funds_high_fees() { + let utxos = get_oldest_first_test_utxos(); + + let target_amount: u64 = utxos.iter().map(|wu| wu.utxo.txout().value).sum::() - 50; + let drain_script = Script::default(); + + OldestFirstCoinSelection::default() + .coin_select( + vec![], + utxos, + FeeRate::from_sat_per_vb(1000.0), + target_amount, + &drain_script, + ) + .unwrap(); + } + + #[test] + fn test_bnb_coin_selection_success() { + // In this case bnb won't find a suitable match and single random draw will + // select three outputs + let utxos = generate_same_value_utxos(100_000, 20); + + let drain_script = Script::default(); + + let target_amount = 250_000 + FEE_AMOUNT; + + let result = BranchAndBoundCoinSelection::default() + .coin_select( + vec![], + utxos, + FeeRate::from_sat_per_vb(1.0), + target_amount, + &drain_script, + ) + .unwrap(); + + assert_eq!(result.selected.len(), 3); + assert_eq!(result.selected_amount(), 300_000); + assert_eq!(result.fee_amount, 204); + } + + #[test] + fn test_bnb_coin_selection_required_are_enough() { + let utxos = get_test_utxos(); + let drain_script = Script::default(); + let target_amount = 20_000 + FEE_AMOUNT; + + let result = BranchAndBoundCoinSelection::default() + .coin_select( + utxos.clone(), + utxos, + FeeRate::from_sat_per_vb(1.0), + target_amount, + &drain_script, + ) + .unwrap(); + + assert_eq!(result.selected.len(), 3); + assert_eq!(result.selected_amount(), 300_010); + assert_eq!(result.fee_amount, 204); + } + + #[test] + fn test_bnb_coin_selection_optional_are_enough() { + let utxos = get_test_utxos(); + let drain_script = Script::default(); + let target_amount = 299756 + FEE_AMOUNT; + + let result = BranchAndBoundCoinSelection::default() + .coin_select( + vec![], + utxos, + FeeRate::from_sat_per_vb(1.0), + target_amount, + &drain_script, + ) + .unwrap(); + + assert_eq!(result.selected.len(), 2); + assert_eq!(result.selected_amount(), 300000); + assert_eq!(result.fee_amount, 136); + } + + #[test] + #[ignore] + fn test_bnb_coin_selection_required_not_enough() { + let utxos = get_test_utxos(); + + let required = vec![utxos[0].clone()]; + let mut optional = utxos[1..].to_vec(); + optional.push(utxo(500_000, 3, ConfirmationTime::Unconfirmed)); + + // Defensive assertions, for sanity and in case someone changes the test utxos vector. + let amount: u64 = required.iter().map(|u| u.utxo.txout().value).sum(); + assert_eq!(amount, 100_000); + let amount: u64 = optional.iter().map(|u| u.utxo.txout().value).sum(); + assert!(amount > 150_000); + let drain_script = Script::default(); + + let target_amount = 150_000 + FEE_AMOUNT; + + let result = BranchAndBoundCoinSelection::default() + .coin_select( + required, + optional, + FeeRate::from_sat_per_vb(1.0), + target_amount, + &drain_script, + ) + .unwrap(); + + assert_eq!(result.selected.len(), 2); + assert_eq!(result.selected_amount(), 300_000); + assert_eq!(result.fee_amount, 136); + } + + #[test] + #[should_panic(expected = "InsufficientFunds")] + fn test_bnb_coin_selection_insufficient_funds() { + let utxos = get_test_utxos(); + let drain_script = Script::default(); + let target_amount = 500_000 + FEE_AMOUNT; + + BranchAndBoundCoinSelection::default() + .coin_select( + vec![], + utxos, + FeeRate::from_sat_per_vb(1.0), + target_amount, + &drain_script, + ) + .unwrap(); + } + + #[test] + #[should_panic(expected = "InsufficientFunds")] + fn test_bnb_coin_selection_insufficient_funds_high_fees() { + let utxos = get_test_utxos(); + let drain_script = Script::default(); + let target_amount = 250_000 + FEE_AMOUNT; + + BranchAndBoundCoinSelection::default() + .coin_select( + vec![], + utxos, + FeeRate::from_sat_per_vb(1000.0), + target_amount, + &drain_script, + ) + .unwrap(); + } + + #[test] + fn test_bnb_coin_selection_check_fee_rate() { + let utxos = get_test_utxos(); + let drain_script = Script::default(); + let target_amount = 99932; // first utxo's effective value + + let result = BranchAndBoundCoinSelection::new(0) + .coin_select( + vec![], + utxos, + FeeRate::from_sat_per_vb(1.0), + target_amount, + &drain_script, + ) + .unwrap(); + + assert_eq!(result.selected.len(), 1); + assert_eq!(result.selected_amount(), 100_000); + let input_size = (TXIN_BASE_WEIGHT + P2WPKH_SATISFACTION_SIZE).vbytes(); + // the final fee rate should be exactly the same as the fee rate given + assert!((1.0 - (result.fee_amount as f32 / input_size as f32)).abs() < f32::EPSILON); + } + + #[test] + fn test_bnb_coin_selection_exact_match() { + let seed = [0; 32]; + let mut rng: StdRng = SeedableRng::from_seed(seed); + + for _i in 0..200 { + let mut optional_utxos = generate_random_utxos(&mut rng, 16); + let target_amount = sum_random_utxos(&mut rng, &mut optional_utxos); + let drain_script = Script::default(); + let result = BranchAndBoundCoinSelection::new(0) + .coin_select( + vec![], + optional_utxos, + FeeRate::from_sat_per_vb(0.0), + target_amount, + &drain_script, + ) + .unwrap(); + assert_eq!(result.selected_amount(), target_amount); + } + } + + #[test] + #[should_panic(expected = "BnBNoExactMatch")] + fn test_bnb_function_no_exact_match() { + let fee_rate = FeeRate::from_sat_per_vb(10.0); + let utxos: Vec = get_test_utxos() + .into_iter() + .map(|u| OutputGroup::new(u, fee_rate)) + .collect(); + + let curr_available_value = utxos.iter().fold(0, |acc, x| acc + x.effective_value); + + let size_of_change = 31; + let cost_of_change = size_of_change as f32 * fee_rate.as_sat_per_vb(); + + let drain_script = Script::default(); + let target_amount = 20_000 + FEE_AMOUNT; + BranchAndBoundCoinSelection::new(size_of_change) + .bnb( + vec![], + utxos, + 0, + curr_available_value, + target_amount as i64, + cost_of_change, + &drain_script, + fee_rate, + ) + .unwrap(); + } + + #[test] + #[should_panic(expected = "BnBTotalTriesExceeded")] + fn test_bnb_function_tries_exceeded() { + let fee_rate = FeeRate::from_sat_per_vb(10.0); + let utxos: Vec = generate_same_value_utxos(100_000, 100_000) + .into_iter() + .map(|u| OutputGroup::new(u, fee_rate)) + .collect(); + + let curr_available_value = utxos.iter().fold(0, |acc, x| acc + x.effective_value); + + let size_of_change = 31; + let cost_of_change = size_of_change as f32 * fee_rate.as_sat_per_vb(); + let target_amount = 20_000 + FEE_AMOUNT; + + let drain_script = Script::default(); + + BranchAndBoundCoinSelection::new(size_of_change) + .bnb( + vec![], + utxos, + 0, + curr_available_value, + target_amount as i64, + cost_of_change, + &drain_script, + fee_rate, + ) + .unwrap(); + } + + // The match won't be exact but still in the range + #[test] + fn test_bnb_function_almost_exact_match_with_fees() { + let fee_rate = FeeRate::from_sat_per_vb(1.0); + let size_of_change = 31; + let cost_of_change = size_of_change as f32 * fee_rate.as_sat_per_vb(); + + let utxos: Vec<_> = generate_same_value_utxos(50_000, 10) + .into_iter() + .map(|u| OutputGroup::new(u, fee_rate)) + .collect(); + + let curr_value = 0; + + let curr_available_value = utxos.iter().fold(0, |acc, x| acc + x.effective_value); + + // 2*(value of 1 utxo) - 2*(1 utxo fees with 1.0sat/vbyte fee rate) - + // cost_of_change + 5. + let target_amount = 2 * 50_000 - 2 * 67 - cost_of_change.ceil() as i64 + 5; + + let drain_script = Script::default(); + + let result = BranchAndBoundCoinSelection::new(size_of_change) + .bnb( + vec![], + utxos, + curr_value, + curr_available_value, + target_amount, + cost_of_change, + &drain_script, + fee_rate, + ) + .unwrap(); + assert_eq!(result.selected_amount(), 100_000); + assert_eq!(result.fee_amount, 136); + } + + // TODO: bnb() function should be optimized, and this test should be done with more utxos + #[test] + fn test_bnb_function_exact_match_more_utxos() { + let seed = [0; 32]; + let mut rng: StdRng = SeedableRng::from_seed(seed); + let fee_rate = FeeRate::from_sat_per_vb(0.0); + + for _ in 0..200 { + let optional_utxos: Vec<_> = generate_random_utxos(&mut rng, 40) + .into_iter() + .map(|u| OutputGroup::new(u, fee_rate)) + .collect(); + + let curr_value = 0; + + let curr_available_value = optional_utxos + .iter() + .fold(0, |acc, x| acc + x.effective_value); + + let target_amount = + optional_utxos[3].effective_value + optional_utxos[23].effective_value; + + let drain_script = Script::default(); + + let result = BranchAndBoundCoinSelection::new(0) + .bnb( + vec![], + optional_utxos, + curr_value, + curr_available_value, + target_amount, + 0.0, + &drain_script, + fee_rate, + ) + .unwrap(); + assert_eq!(result.selected_amount(), target_amount as u64); + } + } + + #[test] + fn test_single_random_draw_function_success() { + let seed = [0; 32]; + let mut rng: StdRng = SeedableRng::from_seed(seed); + let mut utxos = generate_random_utxos(&mut rng, 300); + let target_amount = sum_random_utxos(&mut rng, &mut utxos) + FEE_AMOUNT; + + let fee_rate = FeeRate::from_sat_per_vb(1.0); + let utxos: Vec = utxos + .into_iter() + .map(|u| OutputGroup::new(u, fee_rate)) + .collect(); + + let drain_script = Script::default(); + + let result = BranchAndBoundCoinSelection::default().single_random_draw( + vec![], + utxos, + 0, + target_amount as i64, + &drain_script, + fee_rate, + ); + + assert!(result.selected_amount() > target_amount); + assert_eq!(result.fee_amount, (result.selected.len() * 68) as u64); + } + + #[test] + fn test_bnb_exclude_negative_effective_value() { + let utxos = get_test_utxos(); + let drain_script = Script::default(); + + let selection = BranchAndBoundCoinSelection::default().coin_select( + vec![], + utxos, + FeeRate::from_sat_per_vb(10.0), + 500_000, + &drain_script, + ); + + assert_matches!( + selection, + Err(Error::InsufficientFunds { + available: 300_000, + .. + }) + ); + } + + #[test] + fn test_bnb_include_negative_effective_value_when_required() { + let utxos = get_test_utxos(); + let drain_script = Script::default(); + + let (required, optional) = utxos + .into_iter() + .partition(|u| matches!(u, WeightedUtxo { utxo, .. } if utxo.txout().value < 1000)); + + let selection = BranchAndBoundCoinSelection::default().coin_select( + required, + optional, + FeeRate::from_sat_per_vb(10.0), + 500_000, + &drain_script, + ); + + assert_matches!( + selection, + Err(Error::InsufficientFunds { + available: 300_010, + .. + }) + ); + } + + #[test] + fn test_bnb_sum_of_effective_value_negative() { + let utxos = get_test_utxos(); + let drain_script = Script::default(); + + let selection = BranchAndBoundCoinSelection::default().coin_select( + utxos, + vec![], + FeeRate::from_sat_per_vb(10_000.0), + 500_000, + &drain_script, + ); + + assert_matches!( + selection, + Err(Error::InsufficientFunds { + available: 300_010, + .. + }) + ); + } +} diff --git a/crates/bdk/src/wallet/export.rs b/crates/bdk/src/wallet/export.rs new file mode 100644 index 00000000..3ec43d2b --- /dev/null +++ b/crates/bdk/src/wallet/export.rs @@ -0,0 +1,342 @@ +// Bitcoin Dev Kit +// Written in 2020 by Alekos Filini +// +// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Wallet export +//! +//! This modules implements the wallet export format used by [FullyNoded](https://github.com/Fonta1n3/FullyNoded/blob/10b7808c8b929b171cca537fb50522d015168ac9/Docs/Wallets/Wallet-Export-Spec.md). +//! +//! ## Examples +//! +//! ### Import from JSON +//! +//! ``` +//! # use std::str::FromStr; +//! # use bitcoin::*; +//! # use bdk::wallet::export::*; +//! # use bdk::*; +//! let import = r#"{ +//! "descriptor": "wpkh([c258d2e4\/84h\/1h\/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe\/0\/*)", +//! "blockheight":1782088, +//! "label":"testnet" +//! }"#; +//! +//! let import = FullyNodedExport::from_str(import)?; +//! let wallet = Wallet::new_no_persist( +//! &import.descriptor(), +//! import.change_descriptor().as_ref(), +//! Network::Testnet, +//! )?; +//! # Ok::<_, bdk::Error>(()) +//! ``` +//! +//! ### Export a `Wallet` +//! ``` +//! # use bitcoin::*; +//! # use bdk::wallet::export::*; +//! # use bdk::*; +//! let wallet = Wallet::new_no_persist( +//! "wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/0/*)", +//! Some("wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/1/*)"), +//! Network::Testnet, +//! )?; +//! let export = FullyNodedExport::export_wallet(&wallet, "exported wallet", true) +//! .map_err(ToString::to_string) +//! .map_err(bdk::Error::Generic)?; +//! +//! println!("Exported: {}", export.to_string()); +//! # Ok::<_, bdk::Error>(()) +//! ``` + +use core::str::FromStr; + +use alloc::string::{String, ToString}; +use bdk_chain::sparse_chain::ChainPosition; +use serde::{Deserialize, Serialize}; + +use miniscript::descriptor::{ShInner, WshInner}; +use miniscript::{Descriptor, ScriptContext, Terminal}; + +use crate::types::KeychainKind; +use crate::wallet::Wallet; + +/// Alias for [`FullyNodedExport`] +#[deprecated(since = "0.18.0", note = "Please use [`FullyNodedExport`] instead")] +pub type WalletExport = FullyNodedExport; + +/// Structure that contains the export of a wallet +/// +/// For a usage example see [this module](crate::wallet::export)'s documentation. +#[derive(Debug, Serialize, Deserialize)] +pub struct FullyNodedExport { + descriptor: String, + /// Earliest block to rescan when looking for the wallet's transactions + pub blockheight: u32, + /// Arbitrary label for the wallet + pub label: String, +} + +impl ToString for FullyNodedExport { + fn to_string(&self) -> String { + serde_json::to_string(self).unwrap() + } +} + +impl FromStr for FullyNodedExport { + type Err = serde_json::Error; + + fn from_str(s: &str) -> Result { + serde_json::from_str(s) + } +} + +fn remove_checksum(s: String) -> String { + s.split_once('#').map(|(a, _)| String::from(a)).unwrap() +} + +impl FullyNodedExport { + /// Export a wallet + /// + /// This function returns an error if it determines that the `wallet`'s descriptor(s) are not + /// supported by Bitcoin Core or don't follow the standard derivation paths defined by BIP44 + /// and others. + /// + /// If `include_blockheight` is `true`, this function will look into the `wallet`'s database + /// for the oldest transaction it knows and use that as the earliest block to rescan. + /// + /// If the database is empty or `include_blockheight` is false, the `blockheight` field + /// returned will be `0`. + pub fn export_wallet( + wallet: &Wallet, + label: &str, + include_blockheight: bool, + ) -> Result { + let descriptor = wallet + .get_descriptor_for_keychain(KeychainKind::External) + .to_string_with_secret( + &wallet + .get_signers(KeychainKind::External) + .as_key_map(wallet.secp_ctx()), + ); + let descriptor = remove_checksum(descriptor); + Self::is_compatible_with_core(&descriptor)?; + + let blockheight = if include_blockheight { + wallet + .transactions() + .next() + .and_then(|(pos, _)| pos.height().into()) + .unwrap_or(0) + } else { + 0 + }; + + let export = FullyNodedExport { + descriptor, + label: label.into(), + blockheight, + }; + + let change_descriptor = match wallet.public_descriptor(KeychainKind::Internal).is_some() { + false => None, + true => { + let descriptor = wallet + .get_descriptor_for_keychain(KeychainKind::Internal) + .to_string_with_secret( + &wallet + .get_signers(KeychainKind::Internal) + .as_key_map(wallet.secp_ctx()), + ); + Some(remove_checksum(descriptor)) + } + }; + if export.change_descriptor() != change_descriptor { + return Err("Incompatible change descriptor"); + } + + Ok(export) + } + + fn is_compatible_with_core(descriptor: &str) -> Result<(), &'static str> { + fn check_ms( + terminal: &Terminal, + ) -> Result<(), &'static str> { + if let Terminal::Multi(_, _) = terminal { + Ok(()) + } else { + Err("The descriptor contains operators not supported by Bitcoin Core") + } + } + + // pkh(), wpkh(), sh(wpkh()) are always fine, as well as multi() and sortedmulti() + match Descriptor::::from_str(descriptor).map_err(|_| "Invalid descriptor")? { + Descriptor::Pkh(_) | Descriptor::Wpkh(_) => Ok(()), + Descriptor::Sh(sh) => match sh.as_inner() { + ShInner::Wpkh(_) => Ok(()), + ShInner::SortedMulti(_) => Ok(()), + ShInner::Wsh(wsh) => match wsh.as_inner() { + WshInner::SortedMulti(_) => Ok(()), + WshInner::Ms(ms) => check_ms(&ms.node), + }, + ShInner::Ms(ms) => check_ms(&ms.node), + }, + Descriptor::Wsh(wsh) => match wsh.as_inner() { + WshInner::SortedMulti(_) => Ok(()), + WshInner::Ms(ms) => check_ms(&ms.node), + }, + _ => Err("The descriptor is not compatible with Bitcoin Core"), + } + } + + /// Return the external descriptor + pub fn descriptor(&self) -> String { + self.descriptor.clone() + } + + /// Return the internal descriptor, if present + pub fn change_descriptor(&self) -> Option { + let replaced = self.descriptor.replace("/0/*", "/1/*"); + + if replaced != self.descriptor { + Some(replaced) + } else { + None + } + } +} + +#[cfg(test)] +mod test { + use core::str::FromStr; + + use bdk_chain::{BlockId, ConfirmationTime}; + use bitcoin::hashes::Hash; + use bitcoin::{BlockHash, Network, Transaction}; + + use super::*; + use crate::wallet::Wallet; + + fn get_test_wallet( + descriptor: &str, + change_descriptor: Option<&str>, + network: Network, + ) -> Wallet<()> { + let mut wallet = Wallet::new_no_persist(descriptor, change_descriptor, network).unwrap(); + let transaction = Transaction { + input: vec![], + output: vec![], + version: 0, + lock_time: bitcoin::PackedLockTime::ZERO, + }; + wallet + .insert_checkpoint(BlockId { + height: 5001, + hash: BlockHash::all_zeros(), + }) + .unwrap(); + wallet + .insert_tx( + transaction, + ConfirmationTime::Confirmed { + height: 5000, + time: 0, + }, + ) + .unwrap(); + wallet + } + + #[test] + fn test_export_bip44() { + let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)"; + let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/1/*)"; + + let wallet = get_test_wallet(descriptor, Some(change_descriptor), Network::Bitcoin); + let export = FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap(); + + assert_eq!(export.descriptor(), descriptor); + assert_eq!(export.change_descriptor(), Some(change_descriptor.into())); + assert_eq!(export.blockheight, 5000); + assert_eq!(export.label, "Test Label"); + } + + #[test] + #[should_panic(expected = "Incompatible change descriptor")] + fn test_export_no_change() { + // This wallet explicitly doesn't have a change descriptor. It should be impossible to + // export, because exporting this kind of external descriptor normally implies the + // existence of an internal descriptor + + let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)"; + + let wallet = get_test_wallet(descriptor, None, Network::Bitcoin); + FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap(); + } + + #[test] + #[should_panic(expected = "Incompatible change descriptor")] + fn test_export_incompatible_change() { + // This wallet has a change descriptor, but the derivation path is not in the "standard" + // bip44/49/etc format + + let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)"; + let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/50'/0'/1/*)"; + + let wallet = get_test_wallet(descriptor, Some(change_descriptor), Network::Bitcoin); + FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap(); + } + + #[test] + fn test_export_multi() { + let descriptor = "wsh(multi(2,\ + [73756c7f/48'/0'/0'/2']tpubDCKxNyM3bLgbEX13Mcd8mYxbVg9ajDkWXMh29hMWBurKfVmBfWAM96QVP3zaUcN51HvkZ3ar4VwP82kC8JZhhux8vFQoJintSpVBwpFvyU3/0/*,\ + [f9f62194/48'/0'/0'/2']tpubDDp3ZSH1yCwusRppH7zgSxq2t1VEUyXSeEp8E5aFS8m43MknUjiF1bSLo3CGWAxbDyhF1XowA5ukPzyJZjznYk3kYi6oe7QxtX2euvKWsk4/0/*,\ + [c98b1535/48'/0'/0'/2']tpubDCDi5W4sP6zSnzJeowy8rQDVhBdRARaPhK1axABi8V1661wEPeanpEXj4ZLAUEoikVtoWcyK26TKKJSecSfeKxwHCcRrge9k1ybuiL71z4a/0/*\ + ))"; + let change_descriptor = "wsh(multi(2,\ + [73756c7f/48'/0'/0'/2']tpubDCKxNyM3bLgbEX13Mcd8mYxbVg9ajDkWXMh29hMWBurKfVmBfWAM96QVP3zaUcN51HvkZ3ar4VwP82kC8JZhhux8vFQoJintSpVBwpFvyU3/1/*,\ + [f9f62194/48'/0'/0'/2']tpubDDp3ZSH1yCwusRppH7zgSxq2t1VEUyXSeEp8E5aFS8m43MknUjiF1bSLo3CGWAxbDyhF1XowA5ukPzyJZjznYk3kYi6oe7QxtX2euvKWsk4/1/*,\ + [c98b1535/48'/0'/0'/2']tpubDCDi5W4sP6zSnzJeowy8rQDVhBdRARaPhK1axABi8V1661wEPeanpEXj4ZLAUEoikVtoWcyK26TKKJSecSfeKxwHCcRrge9k1ybuiL71z4a/1/*\ + ))"; + + let wallet = get_test_wallet(descriptor, Some(change_descriptor), Network::Testnet); + let export = FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap(); + + assert_eq!(export.descriptor(), descriptor); + assert_eq!(export.change_descriptor(), Some(change_descriptor.into())); + assert_eq!(export.blockheight, 5000); + assert_eq!(export.label, "Test Label"); + } + + #[test] + fn test_export_to_json() { + let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)"; + let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/1/*)"; + + let wallet = get_test_wallet(descriptor, Some(change_descriptor), Network::Bitcoin); + let export = FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap(); + + assert_eq!(export.to_string(), "{\"descriptor\":\"wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44\'/0\'/0\'/0/*)\",\"blockheight\":5000,\"label\":\"Test Label\"}"); + } + + #[test] + fn test_export_from_json() { + let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)"; + let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/1/*)"; + + let import_str = "{\"descriptor\":\"wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44\'/0\'/0\'/0/*)\",\"blockheight\":5000,\"label\":\"Test Label\"}"; + let export = FullyNodedExport::from_str(import_str).unwrap(); + + assert_eq!(export.descriptor(), descriptor); + assert_eq!(export.change_descriptor(), Some(change_descriptor.into())); + assert_eq!(export.blockheight, 5000); + assert_eq!(export.label, "Test Label"); + } +} diff --git a/crates/bdk/src/wallet/hardwaresigner.rs b/crates/bdk/src/wallet/hardwaresigner.rs new file mode 100644 index 00000000..b014bb53 --- /dev/null +++ b/crates/bdk/src/wallet/hardwaresigner.rs @@ -0,0 +1,98 @@ +// Bitcoin Dev Kit +// Written in 2020 by Alekos Filini +// +// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! HWI Signer +//! +//! This module contains HWISigner, an implementation of a [TransactionSigner] to be +//! used with hardware wallets. +//! ```no_run +//! # use bdk::bitcoin::Network; +//! # use bdk::signer::SignerOrdering; +//! # use bdk::wallet::hardwaresigner::HWISigner; +//! # use bdk::wallet::AddressIndex::New; +//! # use bdk::{FeeRate, KeychainKind, SignOptions, SyncOptions, Wallet}; +//! # use hwi::{types::HWIChain, HWIClient}; +//! # use std::sync::Arc; +//! # +//! # fn main() -> Result<(), Box> { +//! let mut devices = HWIClient::enumerate()?; +//! if devices.is_empty() { +//! panic!("No devices found!"); +//! } +//! let first_device = devices.remove(0)?; +//! let custom_signer = HWISigner::from_device(&first_device, HWIChain::Test)?; +//! +//! # let mut wallet = Wallet::new_no_persist( +//! # "", +//! # None, +//! # Network::Testnet, +//! # )?; +//! # +//! // Adding the hardware signer to the BDK wallet +//! wallet.add_signer( +//! KeychainKind::External, +//! SignerOrdering(200), +//! Arc::new(custom_signer), +//! ); +//! +//! # Ok(()) +//! # } +//! ``` + +use bitcoin::psbt::PartiallySignedTransaction; +use bitcoin::secp256k1::{All, Secp256k1}; +use bitcoin::util::bip32::Fingerprint; + +use hwi::error::Error; +use hwi::types::{HWIChain, HWIDevice}; +use hwi::HWIClient; + +use crate::signer::{SignerCommon, SignerError, SignerId, TransactionSigner}; + +#[derive(Debug)] +/// Custom signer for Hardware Wallets +/// +/// This ignores `sign_options` and leaves the decisions up to the hardware wallet. +pub struct HWISigner { + fingerprint: Fingerprint, + client: HWIClient, +} + +impl HWISigner { + /// Create a instance from the specified device and chain + pub fn from_device(device: &HWIDevice, chain: HWIChain) -> Result { + let client = HWIClient::get_client(device, false, chain)?; + Ok(HWISigner { + fingerprint: device.fingerprint, + client, + }) + } +} + +impl SignerCommon for HWISigner { + fn id(&self, _secp: &Secp256k1) -> SignerId { + SignerId::Fingerprint(self.fingerprint) + } +} + +/// This implementation ignores `sign_options` +impl TransactionSigner for HWISigner { + fn sign_transaction( + &self, + psbt: &mut PartiallySignedTransaction, + _sign_options: &crate::SignOptions, + _secp: &crate::wallet::utils::SecpCtx, + ) -> Result<(), SignerError> { + psbt.combine(self.client.sign_tx(psbt)?.psbt) + .expect("Failed to combine HW signed psbt with passed PSBT"); + Ok(()) + } +} diff --git a/crates/bdk/src/wallet/mod.rs b/crates/bdk/src/wallet/mod.rs new file mode 100644 index 00000000..d4dd571f --- /dev/null +++ b/crates/bdk/src/wallet/mod.rs @@ -0,0 +1,1795 @@ +// Bitcoin Dev Kit +// Written in 2020 by Alekos Filini +// +// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Wallet +//! +//! This module defines the [`Wallet`] structure. +use crate::collections::{BTreeMap, HashMap, HashSet}; +use alloc::{ + boxed::Box, + string::{String, ToString}, + sync::Arc, + vec::Vec, +}; +pub use bdk_chain::keychain::Balance; +use bdk_chain::{ + chain_graph, + keychain::{KeychainChangeSet, KeychainScan, KeychainTracker}, + sparse_chain, BlockId, ConfirmationTime, IntoOwned, +}; +use bitcoin::consensus::encode::serialize; +use bitcoin::secp256k1::Secp256k1; +use bitcoin::util::psbt; +use bitcoin::{ + Address, BlockHash, EcdsaSighashType, LockTime, Network, OutPoint, SchnorrSighashType, Script, + Sequence, Transaction, TxOut, Txid, Witness, +}; +use core::fmt; +use core::ops::Deref; +use miniscript::psbt::{PsbtExt, PsbtInputExt, PsbtInputSatisfier}; + +#[allow(unused_imports)] +use log::{debug, error, info, trace}; + +pub mod coin_selection; +pub mod export; +pub mod signer; +pub mod tx_builder; +pub(crate) mod utils; + +#[cfg(feature = "hardware-signer")] +#[cfg_attr(docsrs, doc(cfg(feature = "hardware-signer")))] +pub mod hardwaresigner; +pub mod persist; + +pub use utils::IsDust; + +#[allow(deprecated)] +use coin_selection::DefaultCoinSelectionAlgorithm; +use signer::{SignOptions, SignerOrdering, SignersContainer, TransactionSigner}; +use tx_builder::{BumpFee, CreateTx, FeePolicy, TxBuilder, TxParams}; +use utils::{check_nsequence_rbf, After, Older, SecpCtx}; + +use crate::descriptor::policy::BuildSatisfaction; +use crate::descriptor::{ + calc_checksum, into_wallet_descriptor_checked, DerivedDescriptor, DescriptorMeta, + ExtendedDescriptor, ExtractPolicy, IntoWalletDescriptor, Policy, XKeyUtils, +}; +use crate::error::{Error, MiniscriptPsbtError}; +use crate::psbt::PsbtUtils; +use crate::signer::SignerError; +use crate::types::*; +use crate::wallet::coin_selection::Excess::{Change, NoChange}; + +const COINBASE_MATURITY: u32 = 100; + +/// A Bitcoin wallet +/// +/// The `Wallet` struct acts as a way of coherently interfacing with output descriptors and related transactions. +/// Its main components are: +/// +/// 1. output *descriptors* from which it can derive addresses. +/// 2. [`signer`]s that can contribute signatures to addresses instantiated from the descriptors. +/// +/// [`signer`]: crate::signer +#[derive(Debug)] +pub struct Wallet { + signers: Arc, + change_signers: Arc, + keychain_tracker: KeychainTracker, + persist: persist::Persist, + network: Network, + secp: SecpCtx, +} + +/// The update to a [`Wallet`] used in [Wallet::apply_update]. This is usually returned from blockchain data sources. +/// The type parameter `T` indicates the kind of transaction contained in the update. It's usually a [`bitcoin::Transaction`]. +pub type Update = KeychainScan; +/// Error indicating that something was wrong with an [`Update`]. +pub type UpdateError = chain_graph::UpdateError; +/// The changeset produced internally by applying an update +pub(crate) type ChangeSet = KeychainChangeSet; + +/// The address index selection strategy to use to derived an address from the wallet's external +/// descriptor. See [`Wallet::get_address`]. If you're unsure which one to use use `WalletIndex::New`. +#[derive(Debug)] +pub enum AddressIndex { + /// Return a new address after incrementing the current descriptor index. + New, + /// Return the address for the current descriptor index if it has not been used in a received + /// transaction. Otherwise return a new address as with [`AddressIndex::New`]. + /// + /// Use with caution, if the wallet has not yet detected an address has been used it could + /// return an already used address. This function is primarily meant for situations where the + /// caller is untrusted; for example when deriving donation addresses on-demand for a public + /// web page. + LastUnused, + /// Return the address for a specific descriptor index. Does not change the current descriptor + /// index used by `AddressIndex::New` and `AddressIndex::LastUsed`. + /// + /// Use with caution, if an index is given that is less than the current descriptor index + /// then the returned address may have already been used. + Peek(u32), +} + +/// A derived address and the index it was found at. +/// For convenience this automatically derefs to `Address` +#[derive(Debug, PartialEq, Eq)] +pub struct AddressInfo { + /// Child index of this address + pub index: u32, + /// Address + pub address: Address, + /// Type of keychain + pub keychain: KeychainKind, +} + +impl Deref for AddressInfo { + type Target = Address; + + fn deref(&self) -> &Self::Target { + &self.address + } +} + +impl fmt::Display for AddressInfo { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.address) + } +} + +impl Wallet { + /// Creates a wallet that does not persist data. + pub fn new_no_persist( + descriptor: E, + change_descriptor: Option, + network: Network, + ) -> Result { + Self::new(descriptor, change_descriptor, (), network).map_err(|e| match e { + NewError::Descriptor(e) => e, + NewError::Persist(_) => unreachable!("no persistence so it can't fail"), + }) + } +} + +#[derive(Debug)] +/// Error returned from [`Wallet::new`] +pub enum NewError

{ + /// There was problem with the descriptors passed in + Descriptor(crate::descriptor::DescriptorError), + /// We were unable to load the wallet's data from the persistance backend + Persist(P), +} + +impl

core::fmt::Display for NewError

+where + P: core::fmt::Display, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + NewError::Descriptor(e) => e.fmt(f), + NewError::Persist(e) => { + write!(f, "failed to load wallet from persistance backend: {}", e) + } + } + } +} + +#[cfg(feautre = "std")] +impl std::error::Error for NewError

{} + +impl Wallet { + /// Create a wallet from a `descriptor` (and an optional `change_descriptor`) and load related + /// transaction data from `db`. + pub fn new( + descriptor: E, + change_descriptor: Option, + mut db: D, + network: Network, + ) -> Result> + where + D: persist::Backend, + { + let secp = Secp256k1::new(); + + let mut keychain_tracker = KeychainTracker::default(); + let (descriptor, keymap) = into_wallet_descriptor_checked(descriptor, &secp, network) + .map_err(NewError::Descriptor)?; + keychain_tracker + .txout_index + .add_keychain(KeychainKind::External, descriptor.clone()); + let signers = Arc::new(SignersContainer::build(keymap, &descriptor, &secp)); + let change_signers = match change_descriptor { + Some(desc) => { + let (change_descriptor, change_keymap) = + into_wallet_descriptor_checked(desc, &secp, network) + .map_err(NewError::Descriptor)?; + + let change_signers = Arc::new(SignersContainer::build( + change_keymap, + &change_descriptor, + &secp, + )); + + keychain_tracker + .txout_index + .add_keychain(KeychainKind::Internal, change_descriptor); + + change_signers + } + None => Arc::new(SignersContainer::new()), + }; + + db.load_into_keychain_tracker(&mut keychain_tracker) + .map_err(NewError::Persist)?; + + let persist = persist::Persist::new(db); + + Ok(Wallet { + signers, + change_signers, + network, + persist, + secp, + keychain_tracker, + }) + } + + /// Get the Bitcoin network the wallet is using. + pub fn network(&self) -> Network { + self.network + } + + /// Iterator over all keychains in this wallet + pub fn keychanins(&self) -> &BTreeMap { + self.keychain_tracker.txout_index.keychains() + } + + /// Return a derived address using the external descriptor, see [`AddressIndex`] for + /// available address index selection strategies. If none of the keys in the descriptor are derivable + /// (i.e. does not end with /*) then the same address will always be returned for any [`AddressIndex`]. + pub fn get_address(&mut self, address_index: AddressIndex) -> AddressInfo + where + D: persist::Backend, + { + self._get_address(address_index, KeychainKind::External) + } + + /// Return a derived address using the internal (change) descriptor. + /// + /// If the wallet doesn't have an internal descriptor it will use the external descriptor. + /// + /// see [`AddressIndex`] for available address index selection strategies. If none of the keys + /// in the descriptor are derivable (i.e. does not end with /*) then the same address will always + /// be returned for any [`AddressIndex`]. + pub fn get_internal_address(&mut self, address_index: AddressIndex) -> AddressInfo + where + D: persist::Backend, + { + self._get_address(address_index, KeychainKind::Internal) + } + + fn _get_address(&mut self, address_index: AddressIndex, keychain: KeychainKind) -> AddressInfo + where + D: persist::Backend, + { + let keychain = self.map_keychain(keychain); + let txout_index = &mut self.keychain_tracker.txout_index; + let (index, spk) = match address_index { + AddressIndex::New => { + let ((index, spk), changeset) = txout_index.reveal_next_spk(&keychain); + let spk = spk.clone(); + + self.persist.stage(changeset.into()); + self.persist.commit().expect("TODO"); + (index, spk) + } + AddressIndex::LastUnused => { + let index = txout_index.last_revealed_index(&keychain); + match index { + Some(index) if !txout_index.is_used(&(keychain, index)) => ( + index, + txout_index + .spk_at_index(&(keychain, index)) + .expect("must exist") + .clone(), + ), + _ => return self._get_address(AddressIndex::New, keychain), + } + } + AddressIndex::Peek(index) => txout_index + .spks_of_keychain(&keychain) + .take(index as usize + 1) + .last() + .unwrap(), + }; + let info = AddressInfo { + index, + address: Address::from_script(&spk, self.network) + .expect("descriptor must have address form"), + keychain, + }; + info + } + + /// Return whether or not a `script` is part of this wallet (either internal or external) + pub fn is_mine(&self, script: &Script) -> bool { + self.keychain_tracker + .txout_index + .index_of_spk(script) + .is_some() + } + + /// Finds how the wallet derived the script pubkey `spk`. + /// + /// Will only return `Some(_)` if the wallet has given out the spk. + pub fn derivation_of_spk(&self, spk: &Script) -> Option<(KeychainKind, u32)> { + self.keychain_tracker.txout_index.index_of_spk(spk).copied() + } + + /// Return the list of unspent outputs of this wallet + /// + /// Note that this method only operates on the internal database, which first needs to be + /// [`Wallet::sync`] manually. + pub fn list_unspent(&self) -> Vec { + self.keychain_tracker + .full_utxos() + .map(|(&(keychain, derivation_index), utxo)| LocalUtxo { + outpoint: utxo.outpoint, + txout: utxo.txout, + keychain: keychain.clone(), + is_spent: false, + derivation_index, + confirmation_time: utxo.chain_position, + }) + .collect() + } + + /// Get all the checkpoints the wallet is currently storing indexed by height. + pub fn checkpoints(&self) -> &BTreeMap { + self.keychain_tracker.chain().checkpoints() + } + + /// Returns the latest checkpoint. + pub fn latest_checkpoint(&self) -> Option { + self.keychain_tracker.chain().latest_checkpoint() + } + + /// Returns a iterators of all the script pubkeys for the `Internal` and External` variants in `KeychainKind`. + /// + /// This is inteded to be used when doing a full scan of your addresses (e.g. after restoring + /// from seed words). You pass the `BTreeMap` of iterators to a blockchain data source (e.g. + /// electrum server) which will go through each address until it reaches a *stop grap*. + /// + /// Note carefully that iterators go over **all** script pubkeys on the keychains (not what + /// script pubkeys the wallet is storing internally). + pub fn spks_of_all_keychains( + &self, + ) -> BTreeMap + Clone> { + self.keychain_tracker.txout_index.spks_of_all_keychains() + } + + /// Gets an iterator over all the script pubkeys in a single keychain. + /// + /// See [`spks_of_all_keychains`] for more documentation + /// + /// [`spks_of_all_keychains`]: Self::spks_of_all_keychains + pub fn spks_of_keychain( + &self, + keychain: KeychainKind, + ) -> impl Iterator + Clone { + self.keychain_tracker + .txout_index + .spks_of_keychain(&keychain) + } + + /// Returns the utxo owned by this wallet corresponding to `outpoint` if it exists in the + /// wallet's database. + pub fn get_utxo(&self, op: OutPoint) -> Option { + self.keychain_tracker + .full_utxos() + .find_map(|(&(keychain, derivation_index), txo)| { + if op == txo.outpoint { + Some(LocalUtxo { + outpoint: txo.outpoint, + txout: txo.txout, + keychain, + is_spent: txo.spent_by.is_none(), + derivation_index, + confirmation_time: txo.chain_position, + }) + } else { + None + } + }) + } + + /// Return a single transactions made and received by the wallet + /// + /// Optionally fill the [`TransactionDetails::transaction`] field with the raw transaction if + /// `include_raw` is `true`. + /// + /// Note that this method only operates on the internal database, which first needs to be + /// [`Wallet::sync`] manually. + pub fn get_tx(&self, txid: Txid, include_raw: bool) -> Option { + let (&confirmation_time, tx) = self.keychain_tracker.chain_graph().get_tx_in_chain(txid)?; + let graph = self.keychain_tracker.graph(); + let txout_index = &self.keychain_tracker.txout_index; + + let received = tx + .output + .iter() + .map(|txout| { + if txout_index.index_of_spk(&txout.script_pubkey).is_some() { + txout.value + } else { + 0 + } + }) + .sum(); + + let sent = tx + .input + .iter() + .map(|txin| { + if let Some((_, txout)) = txout_index.txout(txin.previous_output) { + txout.value + } else { + 0 + } + }) + .sum(); + + let inputs = tx + .input + .iter() + .map(|txin| { + graph + .get_txout(txin.previous_output) + .map(|txout| txout.value) + }) + .sum::>(); + let outputs = tx.output.iter().map(|txout| txout.value).sum(); + let fee = inputs.map(|inputs| inputs.saturating_sub(outputs)); + + Some(TransactionDetails { + transaction: if include_raw { Some(tx.clone()) } else { None }, + txid, + received, + sent, + fee, + confirmation_time, + }) + } + + /// Add a new checkpoint to the wallet's internal view of the chain. + /// This stages but does not [`commit`] the change. + /// + /// Returns whether anything changed with the insertion (e.g. `false` if checkpoint was already + /// there). + /// + /// [`commit`]: Self::commit + pub fn insert_checkpoint( + &mut self, + block_id: BlockId, + ) -> Result { + let changeset = self.keychain_tracker.insert_checkpoint(block_id)?; + let changed = changeset.is_empty(); + self.persist.stage(changeset); + Ok(changed) + } + + /// Add a transaction to the wallet's internal view of the chain. + /// This stages but does not [`commit`] the change. + /// + /// There are a number reasons `tx` could be rejected with an `Err(_)`. The most important one + /// is that the transaction is at a height that is greater than [`latest_checkpoint`]. Therefore + /// you should use [`insert_checkpoint`] to insert new checkpoints before manually inserting new + /// transactions. + /// + /// Returns whether anything changed with the transaction insertion (e.g. `false` if the + /// transaction was already inserted at the same position). + /// + /// [`commit`]: Self::commit + /// [`latest_checkpoint`]: Self::latest_checkpoint + /// [`insert_checkpoint`]: Self::insert_checkpoint + pub fn insert_tx( + &mut self, + tx: Transaction, + position: ConfirmationTime, + ) -> Result> { + let changeset = self.keychain_tracker.insert_tx(tx, position)?; + let changed = changeset.is_empty(); + self.persist.stage(changeset); + Ok(changed) + } + + #[deprecated(note = "use Wallet::transactions instead")] + /// Deprecated. use `Wallet::transactions` instead. + pub fn list_transactions(&self, include_raw: bool) -> Vec { + self.keychain_tracker + .chain() + .txids() + .map(|&(_, txid)| self.get_tx(txid, include_raw).expect("must exist")) + .collect() + } + + /// Iterate over the transactions in the wallet in order of ascending confirmation time with + /// unconfirmed transactions last. + pub fn transactions( + &self, + ) -> impl DoubleEndedIterator + '_ { + self.keychain_tracker + .chain_graph() + .transactions_in_chain() + .map(|(pos, tx)| (*pos, tx)) + } + + /// Return the balance, separated into available, trusted-pending, untrusted-pending and immature + /// values. + /// + /// Note that this method only operates on the internal database, which first needs to be + /// [`Wallet::sync`] manually. + pub fn get_balance(&self) -> Balance { + self.keychain_tracker.balance(|keychain| match keychain { + KeychainKind::External => false, + KeychainKind::Internal => true, + }) + } + + /// Add an external signer + /// + /// See [the `signer` module](signer) for an example. + pub fn add_signer( + &mut self, + keychain: KeychainKind, + ordering: SignerOrdering, + signer: Arc, + ) { + let signers = match keychain { + KeychainKind::External => Arc::make_mut(&mut self.signers), + KeychainKind::Internal => Arc::make_mut(&mut self.change_signers), + }; + + signers.add_external(signer.id(&self.secp), ordering, signer); + } + + /// Get the signers + /// + /// ## Example + /// + /// ``` + /// # use bdk::{Wallet, KeychainKind}; + /// # use bdk::bitcoin::Network; + /// let wallet = Wallet::new_no_persist("wpkh(tprv8ZgxMBicQKsPe73PBRSmNbTfbcsZnwWhz5eVmhHpi31HW29Z7mc9B4cWGRQzopNUzZUT391DeDJxL2PefNunWyLgqCKRMDkU1s2s8bAfoSk/84'/0'/0'/0/*)", None, Network::Testnet)?; + /// for secret_key in wallet.get_signers(KeychainKind::External).signers().iter().filter_map(|s| s.descriptor_secret_key()) { + /// // secret_key: tprv8ZgxMBicQKsPe73PBRSmNbTfbcsZnwWhz5eVmhHpi31HW29Z7mc9B4cWGRQzopNUzZUT391DeDJxL2PefNunWyLgqCKRMDkU1s2s8bAfoSk/84'/0'/0'/0/* + /// println!("secret_key: {}", secret_key); + /// } + /// + /// Ok::<(), Box>(()) + /// ``` + pub fn get_signers(&self, keychain: KeychainKind) -> Arc { + match keychain { + KeychainKind::External => Arc::clone(&self.signers), + KeychainKind::Internal => Arc::clone(&self.change_signers), + } + } + + /// Start building a transaction. + /// + /// This returns a blank [`TxBuilder`] from which you can specify the parameters for the transaction. + /// + /// ## Example + /// + /// ``` + /// # use std::str::FromStr; + /// # use bitcoin::*; + /// # use bdk::*; + /// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)"; + /// # let mut wallet = doctest_wallet!(); + /// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap(); + /// let (psbt, details) = { + /// let mut builder = wallet.build_tx(); + /// builder + /// .add_recipient(to_address.script_pubkey(), 50_000); + /// builder.finish()? + /// }; + /// + /// // sign and broadcast ... + /// # Ok::<(), bdk::Error>(()) + /// ``` + /// + /// [`TxBuilder`]: crate::TxBuilder + pub fn build_tx(&mut self) -> TxBuilder<'_, D, DefaultCoinSelectionAlgorithm, CreateTx> { + TxBuilder { + wallet: alloc::rc::Rc::new(core::cell::RefCell::new(self)), + params: TxParams::default(), + coin_selection: DefaultCoinSelectionAlgorithm::default(), + phantom: core::marker::PhantomData, + } + } + + pub(crate) fn create_tx( + &mut self, + coin_selection: Cs, + params: TxParams, + ) -> Result<(psbt::PartiallySignedTransaction, TransactionDetails), Error> + where + D: persist::Backend, + { + let external_descriptor = self + .keychain_tracker + .txout_index + .keychains() + .get(&KeychainKind::External) + .expect("must exist"); + let internal_descriptor = self + .keychain_tracker + .txout_index + .keychains() + .get(&KeychainKind::Internal); + + let external_policy = external_descriptor + .extract_policy(&self.signers, BuildSatisfaction::None, &self.secp)? + .unwrap(); + let internal_policy = internal_descriptor + .as_ref() + .map(|desc| { + Ok::<_, Error>( + desc.extract_policy(&self.change_signers, BuildSatisfaction::None, &self.secp)? + .unwrap(), + ) + }) + .transpose()?; + + // The policy allows spending external outputs, but it requires a policy path that hasn't been + // provided + if params.change_policy != tx_builder::ChangeSpendPolicy::OnlyChange + && external_policy.requires_path() + && params.external_policy_path.is_none() + { + return Err(Error::SpendingPolicyRequired(KeychainKind::External)); + }; + // Same for the internal_policy path, if present + if let Some(internal_policy) = &internal_policy { + if params.change_policy != tx_builder::ChangeSpendPolicy::ChangeForbidden + && internal_policy.requires_path() + && params.internal_policy_path.is_none() + { + return Err(Error::SpendingPolicyRequired(KeychainKind::Internal)); + }; + } + + let external_requirements = external_policy.get_condition( + params + .external_policy_path + .as_ref() + .unwrap_or(&BTreeMap::new()), + )?; + let internal_requirements = internal_policy + .map(|policy| { + Ok::<_, Error>( + policy.get_condition( + params + .internal_policy_path + .as_ref() + .unwrap_or(&BTreeMap::new()), + )?, + ) + }) + .transpose()?; + + let requirements = + external_requirements.merge(&internal_requirements.unwrap_or_default())?; + debug!("Policy requirements: {:?}", requirements); + + let version = match params.version { + Some(tx_builder::Version(0)) => { + return Err(Error::Generic("Invalid version `0`".into())) + } + Some(tx_builder::Version(1)) if requirements.csv.is_some() => { + return Err(Error::Generic( + "TxBuilder requested version `1`, but at least `2` is needed to use OP_CSV" + .into(), + )) + } + Some(tx_builder::Version(x)) => x, + None if requirements.csv.is_some() => 2, + _ => 1, + }; + + // We use a match here instead of a map_or_else as it's way more readable :) + let current_height = match params.current_height { + // If they didn't tell us the current height, we assume it's the latest sync height. + None => self + .keychain_tracker + .chain() + .latest_checkpoint() + .and_then(|cp| cp.height.into()) + .map(|height| LockTime::from_height(height).expect("Invalid height")), + h => h, + }; + + let lock_time = match params.locktime { + // When no nLockTime is specified, we try to prevent fee sniping, if possible + None => { + // Fee sniping can be partially prevented by setting the timelock + // to current_height. If we don't know the current_height, + // we default to 0. + let fee_sniping_height = current_height.unwrap_or(LockTime::ZERO); + + // We choose the biggest between the required nlocktime and the fee sniping + // height + match requirements.timelock { + // No requirement, just use the fee_sniping_height + None => fee_sniping_height, + // There's a block-based requirement, but the value is lower than the fee_sniping_height + Some(value @ LockTime::Blocks(_)) if value < fee_sniping_height => fee_sniping_height, + // There's a time-based requirement or a block-based requirement greater + // than the fee_sniping_height use that value + Some(value) => value, + } + } + // Specific nLockTime required and we have no constraints, so just set to that value + Some(x) if requirements.timelock.is_none() => x, + // Specific nLockTime required and it's compatible with the constraints + Some(x) if requirements.timelock.unwrap().is_same_unit(x) && x >= requirements.timelock.unwrap() => x, + // Invalid nLockTime required + Some(x) => return Err(Error::Generic(format!("TxBuilder requested timelock of `{:?}`, but at least `{:?}` is required to spend from this script", x, requirements.timelock.unwrap()))) + }; + + let n_sequence = match (params.rbf, requirements.csv) { + // No RBF or CSV but there's an nLockTime, so the nSequence cannot be final + (None, None) if lock_time != LockTime::ZERO => Sequence::ENABLE_LOCKTIME_NO_RBF, + // No RBF, CSV or nLockTime, make the transaction final + (None, None) => Sequence::MAX, + + // No RBF requested, use the value from CSV. Note that this value is by definition + // non-final, so even if a timelock is enabled this nSequence is fine, hence why we + // don't bother checking for it here. The same is true for all the other branches below + (None, Some(csv)) => csv, + + // RBF with a specific value but that value is too high + (Some(tx_builder::RbfValue::Value(rbf)), _) if !rbf.is_rbf() => { + return Err(Error::Generic( + "Cannot enable RBF with a nSequence >= 0xFFFFFFFE".into(), + )) + } + // RBF with a specific value requested, but the value is incompatible with CSV + (Some(tx_builder::RbfValue::Value(rbf)), Some(csv)) + if !check_nsequence_rbf(rbf, csv) => + { + return Err(Error::Generic(format!( + "Cannot enable RBF with nSequence `{:?}` given a required OP_CSV of `{:?}`", + rbf, csv + ))) + } + + // RBF enabled with the default value with CSV also enabled. CSV takes precedence + (Some(tx_builder::RbfValue::Default), Some(csv)) => csv, + // Valid RBF, either default or with a specific value. We ignore the `CSV` value + // because we've already checked it before + (Some(rbf), _) => rbf.get_value(), + }; + + let (fee_rate, mut fee_amount) = match params + .fee_policy + .as_ref() + .unwrap_or(&FeePolicy::FeeRate(FeeRate::default())) + { + //FIXME: see https://github.com/bitcoindevkit/bdk/issues/256 + FeePolicy::FeeAmount(fee) => { + if let Some(previous_fee) = params.bumping_fee { + if *fee < previous_fee.absolute { + return Err(Error::FeeTooLow { + required: previous_fee.absolute, + }); + } + } + (FeeRate::from_sat_per_vb(0.0), *fee) + } + FeePolicy::FeeRate(rate) => { + if let Some(previous_fee) = params.bumping_fee { + let required_feerate = FeeRate::from_sat_per_vb(previous_fee.rate + 1.0); + if *rate < required_feerate { + return Err(Error::FeeRateTooLow { + required: required_feerate, + }); + } + } + (*rate, 0) + } + }; + + let mut tx = Transaction { + version, + lock_time: lock_time.into(), + input: vec![], + output: vec![], + }; + + if params.manually_selected_only && params.utxos.is_empty() { + return Err(Error::NoUtxosSelected); + } + + // we keep it as a float while we accumulate it, and only round it at the end + let mut outgoing: u64 = 0; + let mut received: u64 = 0; + + let recipients = params.recipients.iter().map(|(r, v)| (r, *v)); + + for (index, (script_pubkey, value)) in recipients.enumerate() { + if !params.allow_dust + && value.is_dust(script_pubkey) + && !script_pubkey.is_provably_unspendable() + { + return Err(Error::OutputBelowDustLimit(index)); + } + + if self.is_mine(script_pubkey) { + received += value; + } + + let new_out = TxOut { + script_pubkey: script_pubkey.clone(), + value, + }; + + tx.output.push(new_out); + + outgoing += value; + } + + fee_amount += fee_rate.fee_wu(tx.weight()); + + // Segwit transactions' header is 2WU larger than legacy txs' header, + // as they contain a witness marker (1WU) and a witness flag (1WU) (see BIP144). + // At this point we really don't know if the resulting transaction will be segwit + // or legacy, so we just add this 2WU to the fee_amount - overshooting the fee amount + // is better than undershooting it. + // If we pass a fee_amount that is slightly higher than the final fee_amount, we + // end up with a transaction with a slightly higher fee rate than the requested one. + // If, instead, we undershoot, we may end up with a feerate lower than the requested one + // - we might come up with non broadcastable txs! + fee_amount += fee_rate.fee_wu(2); + + if params.change_policy != tx_builder::ChangeSpendPolicy::ChangeAllowed + && internal_descriptor.is_none() + { + return Err(Error::Generic( + "The `change_policy` can be set only if the wallet has a change_descriptor".into(), + )); + } + + let (required_utxos, optional_utxos) = self.preselect_utxos( + params.change_policy, + ¶ms.unspendable, + params.utxos.clone(), + params.drain_wallet, + params.manually_selected_only, + params.bumping_fee.is_some(), // we mandate confirmed transactions if we're bumping the fee + current_height.map(LockTime::to_consensus_u32), + ); + + // get drain script + let drain_script = match params.drain_to { + Some(ref drain_recipient) => drain_recipient.clone(), + None => { + let change_keychain = self.map_keychain(KeychainKind::Internal); + let ((index, spk), changeset) = self + .keychain_tracker + .txout_index + .next_unused_spk(&change_keychain); + let spk = spk.clone(); + self.keychain_tracker + .txout_index + .mark_used(&change_keychain, index); + self.persist.stage(changeset.into()); + self.persist.commit().expect("TODO"); + spk + } + }; + + let coin_selection = coin_selection.coin_select( + required_utxos, + optional_utxos, + fee_rate, + outgoing + fee_amount, + &drain_script, + )?; + fee_amount += coin_selection.fee_amount; + let excess = &coin_selection.excess; + + tx.input = coin_selection + .selected + .iter() + .map(|u| bitcoin::TxIn { + previous_output: u.outpoint(), + script_sig: Script::default(), + sequence: n_sequence, + witness: Witness::new(), + }) + .collect(); + + if tx.output.is_empty() { + // Uh oh, our transaction has no outputs. + // We allow this when: + // - We have a drain_to address and the utxos we must spend (this happens, + // for example, when we RBF) + // - We have a drain_to address and drain_wallet set + // Otherwise, we don't know who we should send the funds to, and how much + // we should send! + if params.drain_to.is_some() && (params.drain_wallet || !params.utxos.is_empty()) { + if let NoChange { + dust_threshold, + remaining_amount, + change_fee, + } = excess + { + return Err(Error::InsufficientFunds { + needed: *dust_threshold, + available: remaining_amount.saturating_sub(*change_fee), + }); + } + } else { + return Err(Error::NoRecipients); + } + } + + match excess { + NoChange { + remaining_amount, .. + } => fee_amount += remaining_amount, + Change { amount, fee } => { + if self.is_mine(&drain_script) { + received += amount; + } + fee_amount += fee; + + // create drain output + let drain_output = TxOut { + value: *amount, + script_pubkey: drain_script, + }; + + // TODO: We should pay attention when adding a new output: this might increase + // the lenght of the "number of vouts" parameter by 2 bytes, potentially making + // our feerate too low + tx.output.push(drain_output); + } + }; + + // sort input/outputs according to the chosen algorithm + params.ordering.sort_tx(&mut tx); + + let txid = tx.txid(); + let sent = coin_selection.local_selected_amount(); + let psbt = self.complete_transaction(tx, coin_selection.selected, params)?; + + let transaction_details = TransactionDetails { + transaction: None, + txid, + confirmation_time: ConfirmationTime::Unconfirmed, + received, + sent, + fee: Some(fee_amount), + }; + + Ok((psbt, transaction_details)) + } + + /// Bump the fee of a transaction previously created with this wallet. + /// + /// Returns an error if the transaction is already confirmed or doesn't explicitly signal + /// *replace by fee* (RBF). If the transaction can be fee bumped then it returns a [`TxBuilder`] + /// pre-populated with the inputs and outputs of the original transaction. + /// + /// ## Example + /// + /// ```no_run + /// # // TODO: remove norun -- bumping fee seems to need the tx in the wallet database first. + /// # use std::str::FromStr; + /// # use bitcoin::*; + /// # use bdk::*; + /// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)"; + /// # let mut wallet = doctest_wallet!(); + /// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap(); + /// let (mut psbt, _) = { + /// let mut builder = wallet.build_tx(); + /// builder + /// .add_recipient(to_address.script_pubkey(), 50_000) + /// .enable_rbf(); + /// builder.finish()? + /// }; + /// let _ = wallet.sign(&mut psbt, SignOptions::default())?; + /// let tx = psbt.extract_tx(); + /// // broadcast tx but it's taking too long to confirm so we want to bump the fee + /// let (mut psbt, _) = { + /// let mut builder = wallet.build_fee_bump(tx.txid())?; + /// builder + /// .fee_rate(FeeRate::from_sat_per_vb(5.0)); + /// builder.finish()? + /// }; + /// + /// let _ = wallet.sign(&mut psbt, SignOptions::default())?; + /// let fee_bumped_tx = psbt.extract_tx(); + /// // broadcast fee_bumped_tx to replace original + /// # Ok::<(), bdk::Error>(()) + /// ``` + // TODO: support for merging multiple transactions while bumping the fees + pub fn build_fee_bump( + &mut self, + txid: Txid, + ) -> Result, Error> { + let graph = self.keychain_tracker.graph(); + let txout_index = &self.keychain_tracker.txout_index; + let tx_and_height = self.keychain_tracker.chain_graph().get_tx_in_chain(txid); + let mut tx = match tx_and_height { + None => return Err(Error::TransactionNotFound), + Some((ConfirmationTime::Confirmed { .. }, _tx)) => { + return Err(Error::TransactionConfirmed) + } + Some((_, tx)) => tx.clone(), + }; + + if !tx + .input + .iter() + .any(|txin| txin.sequence.to_consensus_u32() <= 0xFFFFFFFD) + { + return Err(Error::IrreplaceableTransaction); + } + + let fee = graph.calculate_fee(&tx).ok_or(Error::FeeRateUnavailable)?; + if fee < 0 { + // It's available but it's wrong so let's say it's unavailable + return Err(Error::FeeRateUnavailable)?; + } + let fee = fee as u64; + let feerate = FeeRate::from_wu(fee, tx.weight()); + + // remove the inputs from the tx and process them + let original_txin = tx.input.drain(..).collect::>(); + let original_utxos = original_txin + .iter() + .map(|txin| -> Result<_, Error> { + let (&confirmation_time, prev_tx) = self + .keychain_tracker + .chain_graph() + .get_tx_in_chain(txin.previous_output.txid) + .ok_or(Error::UnknownUtxo)?; + let txout = &prev_tx.output[txin.previous_output.vout as usize]; + + let weighted_utxo = match txout_index.index_of_spk(&txout.script_pubkey) { + Some(&(keychain, derivation_index)) => { + let satisfaction_weight = self + .get_descriptor_for_keychain(keychain) + .max_satisfaction_weight() + .unwrap(); + WeightedUtxo { + utxo: Utxo::Local(LocalUtxo { + outpoint: txin.previous_output, + txout: txout.clone(), + keychain, + is_spent: true, + derivation_index, + confirmation_time, + }), + satisfaction_weight, + } + } + None => { + let satisfaction_weight = + serialize(&txin.script_sig).len() * 4 + serialize(&txin.witness).len(); + WeightedUtxo { + satisfaction_weight, + utxo: Utxo::Foreign { + outpoint: txin.previous_output, + psbt_input: Box::new(psbt::Input { + witness_utxo: Some(txout.clone()), + non_witness_utxo: Some(prev_tx.clone()), + ..Default::default() + }), + }, + } + } + }; + + Ok(weighted_utxo) + }) + .collect::, _>>()?; + + if tx.output.len() > 1 { + let mut change_index = None; + for (index, txout) in tx.output.iter().enumerate() { + let change_type = self.map_keychain(KeychainKind::Internal); + match txout_index.index_of_spk(&txout.script_pubkey) { + Some(&(keychain, _)) if keychain == change_type => change_index = Some(index), + _ => {} + } + } + + if let Some(change_index) = change_index { + tx.output.remove(change_index); + } + } + + let params = TxParams { + // TODO: figure out what rbf option should be? + version: Some(tx_builder::Version(tx.version)), + recipients: tx + .output + .into_iter() + .map(|txout| (txout.script_pubkey, txout.value)) + .collect(), + utxos: original_utxos, + bumping_fee: Some(tx_builder::PreviousFee { + absolute: fee, + rate: feerate.as_sat_per_vb(), + }), + ..Default::default() + }; + + Ok(TxBuilder { + wallet: alloc::rc::Rc::new(core::cell::RefCell::new(self)), + params, + coin_selection: DefaultCoinSelectionAlgorithm::default(), + phantom: core::marker::PhantomData, + }) + } + + /// Sign a transaction with all the wallet's signers, in the order specified by every signer's + /// [`SignerOrdering`]. This function returns the `Result` type with an encapsulated `bool` that has the value true if the PSBT was finalized, or false otherwise. + /// + /// The [`SignOptions`] can be used to tweak the behavior of the software signers, and the way + /// the transaction is finalized at the end. Note that it can't be guaranteed that *every* + /// signers will follow the options, but the "software signers" (WIF keys and `xprv`) defined + /// in this library will. + /// + /// ## Example + /// + /// ``` + /// # use std::str::FromStr; + /// # use bitcoin::*; + /// # use bdk::*; + /// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)"; + /// # let mut wallet = doctest_wallet!(); + /// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap(); + /// let (mut psbt, _) = { + /// let mut builder = wallet.build_tx(); + /// builder.add_recipient(to_address.script_pubkey(), 50_000); + /// builder.finish()? + /// }; + /// let finalized = wallet.sign(&mut psbt, SignOptions::default())?; + /// assert!(finalized, "we should have signed all the inputs"); + /// # Ok::<(), bdk::Error>(()) + pub fn sign( + &self, + psbt: &mut psbt::PartiallySignedTransaction, + sign_options: SignOptions, + ) -> Result { + // This adds all the PSBT metadata for the inputs, which will help us later figure out how + // to derive our keys + self.update_psbt_with_descriptor(psbt)?; + + // If we aren't allowed to use `witness_utxo`, ensure that every input (except p2tr and finalized ones) + // has the `non_witness_utxo` + if !sign_options.trust_witness_utxo + && psbt + .inputs + .iter() + .filter(|i| i.final_script_witness.is_none() && i.final_script_sig.is_none()) + .filter(|i| i.tap_internal_key.is_none() && i.tap_merkle_root.is_none()) + .any(|i| i.non_witness_utxo.is_none()) + { + return Err(Error::Signer(signer::SignerError::MissingNonWitnessUtxo)); + } + + // If the user hasn't explicitly opted-in, refuse to sign the transaction unless every input + // is using `SIGHASH_ALL` or `SIGHASH_DEFAULT` for taproot + if !sign_options.allow_all_sighashes + && !psbt.inputs.iter().all(|i| { + i.sighash_type.is_none() + || i.sighash_type == Some(EcdsaSighashType::All.into()) + || i.sighash_type == Some(SchnorrSighashType::All.into()) + || i.sighash_type == Some(SchnorrSighashType::Default.into()) + }) + { + return Err(Error::Signer(signer::SignerError::NonStandardSighash)); + } + + for signer in self + .signers + .signers() + .iter() + .chain(self.change_signers.signers().iter()) + { + signer.sign_transaction(psbt, &sign_options, &self.secp)?; + } + + // attempt to finalize + if sign_options.try_finalize { + self.finalize_psbt(psbt, sign_options) + } else { + Ok(false) + } + } + + /// Return the spending policies for the wallet's descriptor + pub fn policies(&self, keychain: KeychainKind) -> Result, Error> { + let signers = match keychain { + KeychainKind::External => &self.signers, + KeychainKind::Internal => &self.change_signers, + }; + + match self.public_descriptor(keychain) { + Some(desc) => Ok(desc.extract_policy(signers, BuildSatisfaction::None, &self.secp)?), + None => Ok(None), + } + } + + /// Return the "public" version of the wallet's descriptor, meaning a new descriptor that has + /// the same structure but with every secret key removed + /// + /// This can be used to build a watch-only version of a wallet + pub fn public_descriptor(&self, keychain: KeychainKind) -> Option<&ExtendedDescriptor> { + self.keychain_tracker.txout_index.keychains().get(&keychain) + } + + /// Finalize a PSBT, i.e., for each input determine if sufficient data is available to pass + /// validation and construct the respective `scriptSig` or `scriptWitness`. Please refer to + /// [BIP174](https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki#Input_Finalizer) + /// for further information. + /// + /// Returns `true` if the PSBT could be finalized, and `false` otherwise. + /// + /// The [`SignOptions`] can be used to tweak the behavior of the finalizer. + pub fn finalize_psbt( + &self, + psbt: &mut psbt::PartiallySignedTransaction, + sign_options: SignOptions, + ) -> Result { + let tx = &psbt.unsigned_tx; + let mut finished = true; + + for (n, input) in tx.input.iter().enumerate() { + let psbt_input = &psbt + .inputs + .get(n) + .ok_or(Error::Signer(SignerError::InputIndexOutOfRange))?; + if psbt_input.final_script_sig.is_some() || psbt_input.final_script_witness.is_some() { + continue; + } + let confirmation_height = self + .keychain_tracker + .chain() + .tx_position(input.previous_output.txid) + .map(|conftime| match conftime { + &ConfirmationTime::Confirmed { height, .. } => height, + ConfirmationTime::Unconfirmed => u32::MAX, + }); + let last_sync_height = self + .keychain_tracker + .chain() + .latest_checkpoint() + .map(|block_id| block_id.height); + let current_height = sign_options.assume_height.or(last_sync_height); + + debug!( + "Input #{} - {}, using `confirmation_height` = {:?}, `current_height` = {:?}", + n, input.previous_output, confirmation_height, current_height + ); + + // - Try to derive the descriptor by looking at the txout. If it's in our database, we + // know exactly which `keychain` to use, and which derivation index it is + // - If that fails, try to derive it by looking at the psbt input: the complete logic + // is in `src/descriptor/mod.rs`, but it will basically look at `bip32_derivation`, + // `redeem_script` and `witness_script` to determine the right derivation + // - If that also fails, it will try it on the internal descriptor, if present + let desc = psbt + .get_utxo_for(n) + .map(|txout| self.get_descriptor_for_txout(&txout)) + .flatten() + .or_else(|| { + self.keychain_tracker + .txout_index + .keychains() + .iter() + .find_map(|(_, desc)| { + desc.derive_from_psbt_input( + psbt_input, + psbt.get_utxo_for(n), + &self.secp, + ) + }) + }); + + match desc { + Some(desc) => { + let mut tmp_input = bitcoin::TxIn::default(); + match desc.satisfy( + &mut tmp_input, + ( + PsbtInputSatisfier::new(psbt, n), + After::new(current_height, false), + Older::new(current_height, confirmation_height, false), + ), + ) { + Ok(_) => { + let psbt_input = &mut psbt.inputs[n]; + psbt_input.final_script_sig = Some(tmp_input.script_sig); + psbt_input.final_script_witness = Some(tmp_input.witness); + if sign_options.remove_partial_sigs { + psbt_input.partial_sigs.clear(); + } + } + Err(e) => { + debug!("satisfy error {:?} for input {}", e, n); + finished = false + } + } + } + None => finished = false, + } + } + + Ok(finished) + } + + /// Return the secp256k1 context used for all signing operations + pub fn secp_ctx(&self) -> &SecpCtx { + &self.secp + } + + /// Returns the descriptor used to create addresses for a particular `keychain`. + pub fn get_descriptor_for_keychain(&self, keychain: KeychainKind) -> &ExtendedDescriptor { + self.public_descriptor(self.map_keychain(keychain)) + .expect("we mapped it to external if it doesn't exist") + } + + /// The derivation index of this wallet. It will return `None` if it has not derived any addresses. + /// Otherwise, it will return the index of the highest address it has derived. + pub fn derivation_index(&self, keychain: KeychainKind) -> Option { + self.keychain_tracker + .txout_index + .last_revealed_index(&keychain) + } + + /// The index of the next address that you would get if you were to ask the wallet for a new address + pub fn next_derivation_index(&self, keychain: KeychainKind) -> u32 { + self.keychain_tracker.txout_index.next_index(&keychain).0 + } + + /// Informs the wallet that you no longer intend to broadcast a tx that was built from it. + /// + /// This frees up the change address used when creating the tx for use in future transactions. + // TODO: Make this free up reserved utxos when that's implemented + pub fn cancel_tx(&mut self, tx: &Transaction) { + let txout_index = &mut self.keychain_tracker.txout_index; + for txout in &tx.output { + if let Some(&(keychain, index)) = txout_index.index_of_spk(&txout.script_pubkey) { + // NOTE: unmark_used will **not** make something unused if it has actually been used + // by a tx in the tracker. It only removes the superficial marking. + txout_index.unmark_used(&keychain, index); + } + } + } + + fn map_keychain(&self, keychain: KeychainKind) -> KeychainKind { + if keychain == KeychainKind::Internal + && self.public_descriptor(KeychainKind::Internal).is_none() + { + KeychainKind::External + } else { + keychain + } + } + + fn get_descriptor_for_txout(&self, txout: &TxOut) -> Option { + let &(keychain, child) = self + .keychain_tracker + .txout_index + .index_of_spk(&txout.script_pubkey)?; + let descriptor = self.get_descriptor_for_keychain(keychain); + Some(descriptor.at_derivation_index(child)) + } + + fn get_available_utxos(&self) -> Vec<(LocalUtxo, usize)> { + self.list_unspent() + .into_iter() + .map(|utxo| { + let keychain = utxo.keychain; + ( + utxo, + self.get_descriptor_for_keychain(keychain) + .max_satisfaction_weight() + .unwrap(), + ) + }) + .collect() + } + + /// Given the options returns the list of utxos that must be used to form the + /// transaction and any further that may be used if needed. + #[allow(clippy::type_complexity)] + #[allow(clippy::too_many_arguments)] + fn preselect_utxos( + &self, + change_policy: tx_builder::ChangeSpendPolicy, + unspendable: &HashSet, + manually_selected: Vec, + must_use_all_available: bool, + manual_only: bool, + must_only_use_confirmed_tx: bool, + current_height: Option, + ) -> (Vec, Vec) { + // must_spend <- manually selected utxos + // may_spend <- all other available utxos + let mut may_spend = self.get_available_utxos(); + + may_spend.retain(|may_spend| { + !manually_selected + .iter() + .any(|manually_selected| manually_selected.utxo.outpoint() == may_spend.0.outpoint) + }); + let mut must_spend = manually_selected; + + // NOTE: we are intentionally ignoring `unspendable` here. i.e manual + // selection overrides unspendable. + if manual_only { + return (must_spend, vec![]); + } + + let satisfies_confirmed = may_spend + .iter() + .map(|u| { + let txid = u.0.outpoint.txid; + let tx = self.keychain_tracker.chain_graph().get_tx_in_chain(txid); + match tx { + // We don't have the tx in the db for some reason, + // so we can't know for sure if it's mature or not. + // We prefer not to spend it. + None => false, + Some((confirmation_time, tx)) => { + // Whether the UTXO is mature and, if needed, confirmed + let mut spendable = true; + if must_only_use_confirmed_tx && !confirmation_time.is_confirmed() { + return false; + } + if tx.is_coin_base() { + debug_assert!( + confirmation_time.is_confirmed(), + "coinbase must always be confirmed" + ); + if let Some(current_height) = current_height { + match confirmation_time { + ConfirmationTime::Confirmed { height, .. } => { + // https://github.com/bitcoin/bitcoin/blob/c5e67be03bb06a5d7885c55db1f016fbf2333fe3/src/validation.cpp#L373-L375 + spendable &= (current_height.saturating_sub(*height)) + >= COINBASE_MATURITY; + } + ConfirmationTime::Unconfirmed => spendable = false, + } + } + } + spendable + } + } + }) + .collect::>(); + + let mut i = 0; + may_spend.retain(|u| { + let retain = change_policy.is_satisfied_by(&u.0) + && !unspendable.contains(&u.0.outpoint) + && satisfies_confirmed[i]; + i += 1; + retain + }); + + let mut may_spend = may_spend + .into_iter() + .map(|(local_utxo, satisfaction_weight)| WeightedUtxo { + satisfaction_weight, + utxo: Utxo::Local(local_utxo), + }) + .collect(); + + if must_use_all_available { + must_spend.append(&mut may_spend); + } + + (must_spend, may_spend) + } + + fn complete_transaction( + &self, + tx: Transaction, + selected: Vec, + params: TxParams, + ) -> Result { + let mut psbt = psbt::PartiallySignedTransaction::from_unsigned_tx(tx)?; + + if params.add_global_xpubs { + let all_xpubs = self + .keychanins() + .iter() + .flat_map(|(_, desc)| desc.get_extended_keys()) + .collect::>(); + + for xpub in all_xpubs { + let origin = match xpub.origin { + Some(origin) => origin, + None if xpub.xkey.depth == 0 => { + (xpub.root_fingerprint(&self.secp), vec![].into()) + } + _ => return Err(Error::MissingKeyOrigin(xpub.xkey.to_string())), + }; + + psbt.xpub.insert(xpub.xkey, origin); + } + } + + let mut lookup_output = selected + .into_iter() + .map(|utxo| (utxo.outpoint(), utxo)) + .collect::>(); + + // add metadata for the inputs + for (psbt_input, input) in psbt.inputs.iter_mut().zip(psbt.unsigned_tx.input.iter()) { + let utxo = match lookup_output.remove(&input.previous_output) { + Some(utxo) => utxo, + None => continue, + }; + + match utxo { + Utxo::Local(utxo) => { + *psbt_input = + match self.get_psbt_input(utxo, params.sighash, params.only_witness_utxo) { + Ok(psbt_input) => psbt_input, + Err(e) => match e { + Error::UnknownUtxo => psbt::Input { + sighash_type: params.sighash, + ..psbt::Input::default() + }, + _ => return Err(e), + }, + } + } + Utxo::Foreign { + psbt_input: foreign_psbt_input, + outpoint, + } => { + let is_taproot = foreign_psbt_input + .witness_utxo + .as_ref() + .map(|txout| txout.script_pubkey.is_v1_p2tr()) + .unwrap_or(false); + if !is_taproot + && !params.only_witness_utxo + && foreign_psbt_input.non_witness_utxo.is_none() + { + return Err(Error::Generic(format!( + "Missing non_witness_utxo on foreign utxo {}", + outpoint + ))); + } + *psbt_input = *foreign_psbt_input; + } + } + } + + self.update_psbt_with_descriptor(&mut psbt)?; + + Ok(psbt) + } + + /// get the corresponding PSBT Input for a LocalUtxo + pub fn get_psbt_input( + &self, + utxo: LocalUtxo, + sighash_type: Option, + only_witness_utxo: bool, + ) -> Result { + // Try to find the prev_script in our db to figure out if this is internal or external, + // and the derivation index + let &(keychain, child) = self + .keychain_tracker + .txout_index + .index_of_spk(&utxo.txout.script_pubkey) + .ok_or(Error::UnknownUtxo)?; + + let mut psbt_input = psbt::Input { + sighash_type, + ..psbt::Input::default() + }; + + let desc = self.get_descriptor_for_keychain(keychain); + let derived_descriptor = desc.at_derivation_index(child); + + psbt_input + .update_with_descriptor_unchecked(&derived_descriptor) + .map_err(MiniscriptPsbtError::Conversion)?; + + let prev_output = utxo.outpoint; + if let Some(prev_tx) = self.keychain_tracker.graph().get_tx(prev_output.txid) { + if desc.is_witness() || desc.is_taproot() { + psbt_input.witness_utxo = Some(prev_tx.output[prev_output.vout as usize].clone()); + } + if !desc.is_taproot() && (!desc.is_witness() || !only_witness_utxo) { + psbt_input.non_witness_utxo = Some(prev_tx.clone()); + } + } + Ok(psbt_input) + } + + fn update_psbt_with_descriptor( + &self, + psbt: &mut psbt::PartiallySignedTransaction, + ) -> Result<(), Error> { + // We need to borrow `psbt` mutably within the loops, so we have to allocate a vec for all + // the input utxos and outputs + // + // Clippy complains that the collect is not required, but that's wrong + #[allow(clippy::needless_collect)] + let utxos = (0..psbt.inputs.len()) + .filter_map(|i| psbt.get_utxo_for(i).map(|utxo| (true, i, utxo))) + .chain( + psbt.unsigned_tx + .output + .iter() + .enumerate() + .map(|(i, out)| (false, i, out.clone())), + ) + .collect::>(); + + // Try to figure out the keychain and derivation for every input and output + for (is_input, index, out) in utxos.into_iter() { + if let Some(&(keychain, child)) = self + .keychain_tracker + .txout_index + .index_of_spk(&out.script_pubkey) + { + debug!( + "Found descriptor for input #{} {:?}/{}", + index, keychain, child + ); + + let desc = self.get_descriptor_for_keychain(keychain); + let desc = desc.at_derivation_index(child); + + if is_input { + psbt.update_input_with_descriptor(index, &desc) + .map_err(MiniscriptPsbtError::UtxoUpdate)?; + } else { + psbt.update_output_with_descriptor(index, &desc) + .map_err(MiniscriptPsbtError::OutputUpdate)?; + } + } + } + + Ok(()) + } + + /// Return the checksum of the public descriptor associated to `keychain` + /// + /// Internally calls [`Self::get_descriptor_for_keychain`] to fetch the right descriptor + pub fn descriptor_checksum(&self, keychain: KeychainKind) -> String { + self.get_descriptor_for_keychain(keychain) + .to_string() + .split_once('#') + .unwrap() + .1 + .to_string() + } + + /// Applies an update to the wallet and stages the changes (but does not [`commit`] them). + /// + /// Usually you create an `update` by interacting with some blockchain data source and inserting + /// transactions related to your wallet into it. + /// + /// [`commit`]: Self::commit + pub fn apply_udpate(&mut self, update: Update) -> Result<(), UpdateError> + where + D: persist::Backend, + Tx: IntoOwned + Clone, + { + let changeset = self.keychain_tracker.apply_update(update)?; + self.persist.stage(changeset); + Ok(()) + } + + /// Commits all curently [`staged`] changed to the persistence backend returning and error when this fails. + /// + /// [`staged`]: Self::staged + pub fn commit(&mut self) -> Result<(), D::WriteError> + where + D: persist::Backend, + { + self.persist.commit() + } + + /// Returns the changes that will be staged with the next call to [`commit`]. + /// + /// [`commit`]: Self::commit + pub fn staged(&self) -> &ChangeSet { + self.persist.staged() + } +} + +/// Deterministically generate a unique name given the descriptors defining the wallet +/// +/// Compatible with [`wallet_name_from_descriptor`] +pub fn wallet_name_from_descriptor( + descriptor: T, + change_descriptor: Option, + network: Network, + secp: &SecpCtx, +) -> Result +where + T: IntoWalletDescriptor, +{ + //TODO check descriptors contains only public keys + let descriptor = descriptor + .into_wallet_descriptor(secp, network)? + .0 + .to_string(); + let mut wallet_name = calc_checksum(&descriptor[..descriptor.find('#').unwrap()])?; + if let Some(change_descriptor) = change_descriptor { + let change_descriptor = change_descriptor + .into_wallet_descriptor(secp, network)? + .0 + .to_string(); + wallet_name.push_str( + calc_checksum(&change_descriptor[..change_descriptor.find('#').unwrap()])?.as_str(), + ); + } + + Ok(wallet_name) +} + +#[macro_export] +#[doc(hidden)] +/// Macro for getting a wallet for use in a doctest +macro_rules! doctest_wallet { + () => {{ + use $crate::bitcoin::{BlockHash, Transaction, PackedLockTime, TxOut, Network, hashes::Hash}; + use $crate::chain::{ConfirmationTime, BlockId}; + use $crate::wallet::{AddressIndex, Wallet}; + let descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/0/*)"; + let change_descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/1/*)"; + + let mut wallet = Wallet::new_no_persist( + descriptor, + Some(change_descriptor), + Network::Regtest, + ) + .unwrap(); + let address = wallet.get_address(AddressIndex::New).address; + let tx = Transaction { + version: 1, + lock_time: PackedLockTime(0), + input: vec![], + output: vec![TxOut { + value: 500_000, + script_pubkey: address.script_pubkey(), + }], + }; + let _ = wallet.insert_checkpoint(BlockId { height: 1_000, hash: BlockHash::all_zeros() }); + let _ = wallet.insert_tx(tx.clone(), ConfirmationTime::Confirmed { + height: 500, + time: 50_000 + }); + + wallet + }} +} diff --git a/crates/bdk/src/wallet/persist.rs b/crates/bdk/src/wallet/persist.rs new file mode 100644 index 00000000..c6d65ce6 --- /dev/null +++ b/crates/bdk/src/wallet/persist.rs @@ -0,0 +1,120 @@ +//! Persistence for changes made to a [`Wallet`]. +//! +//! BDK's [`Wallet`] needs somewhere to persist changes it makes during operation. +//! Operations like giving out a new address are crucial to persist so that next time the +//! application is loaded it can find transactions related to that address. +//! +//! Note that `Wallet` does not read this persisted data during operation since it always has a copy +//! in memory +use crate::KeychainKind; +use bdk_chain::{keychain::KeychainTracker, ConfirmationTime}; + +/// `Persist` wraps a [`Backend`] to create a convienient staging area for changes before they are +/// persisted. Not all changes made to the [`Wallet`] need to be written to disk right away so you +/// can use [`Persist::stage`] to *stage* it first and then [`Persist::commit`] to finally write it +/// to disk. +#[derive(Debug)] +pub struct Persist

{ + backend: P, + stage: ChangeSet, +} + +impl

Persist

{ + /// Create a new `Persist` from a [`Backend`] + pub fn new(backend: P) -> Self { + Self { + backend, + stage: Default::default(), + } + } + + /// Stage a `changeset` to later persistence with [`commit`]. + /// + /// [`commit`]: Self::commit + pub fn stage(&mut self, changeset: ChangeSet) { + self.stage.append(changeset) + } + + /// Get the changes that haven't been commited yet + pub fn staged(&self) -> &ChangeSet { + &self.stage + } + + /// Commit the staged changes to the underlying persistence backend. + /// + /// Retuns a backend defined error if this fails + pub fn commit(&mut self) -> Result<(), P::WriteError> + where + P: Backend, + { + self.backend.append_changeset(&self.stage)?; + self.stage = Default::default(); + Ok(()) + } +} + +/// A persistence backend for [`Wallet`] +/// +/// [`Wallet`]: crate::Wallet +pub trait Backend { + /// The error the backend returns when it fails to write + type WriteError: core::fmt::Debug; + /// The error the backend returns when it fails to load + type LoadError: core::fmt::Debug; + /// Appends a new changeset to the persistance backend. + /// + /// It is up to the backend what it does with this. It could store every changeset in a list or + /// it insert the actual changes to a more structured database. All it needs to guarantee is + /// that [`load_into_keychain_tracker`] restores a keychain tracker to what it should be if all + /// changesets had been applied sequentially. + /// + /// [`load_into_keychain_tracker`]: Self::load_into_keychain_tracker + fn append_changeset(&mut self, changeset: &ChangeSet) -> Result<(), Self::WriteError>; + + /// Applies all the changesets the backend has received to `tracker`. + fn load_into_keychain_tracker( + &mut self, + tracker: &mut KeychainTracker, + ) -> Result<(), Self::LoadError>; +} + +#[cfg(feature = "file-store")] +mod file_store { + use super::*; + use bdk_chain::file_store::{IterError, KeychainStore}; + + type FileStore = KeychainStore; + + impl Backend for FileStore { + type WriteError = std::io::Error; + type LoadError = IterError; + fn append_changeset(&mut self, changeset: &ChangeSet) -> Result<(), Self::WriteError> { + self.append_changeset(changeset) + } + fn load_into_keychain_tracker( + &mut self, + tracker: &mut KeychainTracker, + ) -> Result<(), Self::LoadError> { + self.load_into_keychain_tracker(tracker) + } + } +} + +impl Backend for () { + type WriteError = (); + type LoadError = (); + fn append_changeset(&mut self, _changeset: &ChangeSet) -> Result<(), Self::WriteError> { + Ok(()) + } + fn load_into_keychain_tracker( + &mut self, + _tracker: &mut KeychainTracker, + ) -> Result<(), Self::LoadError> { + Ok(()) + } +} + +#[cfg(feature = "file-store")] +pub use file_store::*; + +use super::ChangeSet; diff --git a/crates/bdk/src/wallet/signer.rs b/crates/bdk/src/wallet/signer.rs new file mode 100644 index 00000000..68dc4645 --- /dev/null +++ b/crates/bdk/src/wallet/signer.rs @@ -0,0 +1,1140 @@ +// Bitcoin Dev Kit +// Written in 2020 by Alekos Filini +// +// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Generalized signers +//! +//! This module provides the ability to add customized signers to a [`Wallet`](super::Wallet) +//! through the [`Wallet::add_signer`](super::Wallet::add_signer) function. +//! +//! ``` +//! # use alloc::sync::Arc; +//! # use core::str::FromStr; +//! # use bitcoin::secp256k1::{Secp256k1, All}; +//! # use bitcoin::*; +//! # use bitcoin::util::psbt; +//! # use bdk::signer::*; +//! # use bdk::*; +//! # #[derive(Debug)] +//! # struct CustomHSM; +//! # impl CustomHSM { +//! # fn hsm_sign_input(&self, _psbt: &mut psbt::PartiallySignedTransaction, _input: usize) -> Result<(), SignerError> { +//! # Ok(()) +//! # } +//! # fn connect() -> Self { +//! # CustomHSM +//! # } +//! # fn get_id(&self) -> SignerId { +//! # SignerId::Dummy(0) +//! # } +//! # } +//! #[derive(Debug)] +//! struct CustomSigner { +//! device: CustomHSM, +//! } +//! +//! impl CustomSigner { +//! fn connect() -> Self { +//! CustomSigner { device: CustomHSM::connect() } +//! } +//! } +//! +//! impl SignerCommon for CustomSigner { +//! fn id(&self, _secp: &Secp256k1) -> SignerId { +//! self.device.get_id() +//! } +//! } +//! +//! impl InputSigner for CustomSigner { +//! fn sign_input( +//! &self, +//! psbt: &mut psbt::PartiallySignedTransaction, +//! input_index: usize, +//! _sign_options: &SignOptions, +//! _secp: &Secp256k1, +//! ) -> Result<(), SignerError> { +//! self.device.hsm_sign_input(psbt, input_index)?; +//! +//! Ok(()) +//! } +//! } +//! +//! let custom_signer = CustomSigner::connect(); +//! +//! let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)"; +//! let mut wallet = Wallet::new_no_persist(descriptor, None, Network::Testnet)?; +//! wallet.add_signer( +//! KeychainKind::External, +//! SignerOrdering(200), +//! Arc::new(custom_signer) +//! ); +//! +//! # Ok::<_, bdk::Error>(()) +//! ``` + +use crate::collections::BTreeMap; +use alloc::sync::Arc; +use alloc::vec::Vec; +use core::cmp::Ordering; +use core::fmt; +use core::ops::{Bound::Included, Deref}; + +use bitcoin::blockdata::opcodes; +use bitcoin::blockdata::script::Builder as ScriptBuilder; +use bitcoin::hashes::{hash160, Hash}; +use bitcoin::secp256k1::Message; +use bitcoin::util::bip32::{ChildNumber, DerivationPath, ExtendedPrivKey, Fingerprint}; +use bitcoin::util::{ecdsa, psbt, schnorr, sighash, taproot}; +use bitcoin::{secp256k1, XOnlyPublicKey}; +use bitcoin::{EcdsaSighashType, PrivateKey, PublicKey, SchnorrSighashType, Script}; + +use miniscript::descriptor::{ + Descriptor, DescriptorPublicKey, DescriptorSecretKey, DescriptorXKey, KeyMap, SinglePriv, + SinglePubKey, +}; +use miniscript::{Legacy, Segwitv0, SigType, Tap, ToPublicKey}; + +use super::utils::SecpCtx; +use crate::descriptor::{DescriptorMeta, XKeyUtils}; +use crate::psbt::PsbtUtils; + +/// Identifier of a signer in the `SignersContainers`. Used as a key to find the right signer among +/// multiple of them +#[derive(Debug, Clone, Ord, PartialOrd, PartialEq, Eq, Hash)] +pub enum SignerId { + /// Bitcoin HASH160 (RIPEMD160 after SHA256) hash of an ECDSA public key + PkHash(hash160::Hash), + /// The fingerprint of a BIP32 extended key + Fingerprint(Fingerprint), + /// Dummy identifier + Dummy(u64), +} + +impl From for SignerId { + fn from(hash: hash160::Hash) -> SignerId { + SignerId::PkHash(hash) + } +} + +impl From for SignerId { + fn from(fing: Fingerprint) -> SignerId { + SignerId::Fingerprint(fing) + } +} + +/// Signing error +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum SignerError { + /// The private key is missing for the required public key + MissingKey, + /// The private key in use has the right fingerprint but derives differently than expected + InvalidKey, + /// The user canceled the operation + UserCanceled, + /// Input index is out of range + InputIndexOutOfRange, + /// The `non_witness_utxo` field of the transaction is required to sign this input + MissingNonWitnessUtxo, + /// The `non_witness_utxo` specified is invalid + InvalidNonWitnessUtxo, + /// The `witness_utxo` field of the transaction is required to sign this input + MissingWitnessUtxo, + /// The `witness_script` field of the transaction is required to sign this input + MissingWitnessScript, + /// The fingerprint and derivation path are missing from the psbt input + MissingHdKeypath, + /// The psbt contains a non-`SIGHASH_ALL` sighash in one of its input and the user hasn't + /// explicitly allowed them + /// + /// To enable signing transactions with non-standard sighashes set + /// [`SignOptions::allow_all_sighashes`] to `true`. + NonStandardSighash, + /// Invalid SIGHASH for the signing context in use + InvalidSighash, + /// Error while computing the hash to sign + SighashError(sighash::Error), + /// Error while signing using hardware wallets + #[cfg(feature = "hardware-signer")] + HWIError(hwi::error::Error), +} + +#[cfg(feature = "hardware-signer")] +impl From for SignerError { + fn from(e: hwi::error::Error) -> Self { + SignerError::HWIError(e) + } +} + +impl From for SignerError { + fn from(e: sighash::Error) -> Self { + SignerError::SighashError(e) + } +} + +impl fmt::Display for SignerError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::MissingKey => write!(f, "Missing private key"), + Self::InvalidKey => write!(f, "The private key in use has the right fingerprint but derives differently than expected"), + Self::UserCanceled => write!(f, "The user canceled the operation"), + Self::InputIndexOutOfRange => write!(f, "Input index out of range"), + Self::MissingNonWitnessUtxo => write!(f, "Missing non-witness UTXO"), + Self::InvalidNonWitnessUtxo => write!(f, "Invalid non-witness UTXO"), + Self::MissingWitnessUtxo => write!(f, "Missing witness UTXO"), + Self::MissingWitnessScript => write!(f, "Missing witness script"), + Self::MissingHdKeypath => write!(f, "Missing fingerprint and derivation path"), + Self::NonStandardSighash => write!(f, "The psbt contains a non standard sighash"), + Self::InvalidSighash => write!(f, "Invalid SIGHASH for the signing context in use"), + Self::SighashError(err) => write!(f, "Error while computing the hash to sign: {}", err), + #[cfg(feature = "hardware-signer")] + Self::HWIError(err) => write!(f, "Error while signing using hardware wallets: {}", err), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for SignerError {} + +/// Signing context +/// +/// Used by our software signers to determine the type of signatures to make +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SignerContext { + /// Legacy context + Legacy, + /// Segwit v0 context (BIP 143) + Segwitv0, + /// Taproot context (BIP 340) + Tap { + /// Whether the signer can sign for the internal key or not + is_internal_key: bool, + }, +} + +/// Wrapper structure to pair a signer with its context +#[derive(Debug, Clone)] +pub struct SignerWrapper { + signer: S, + ctx: SignerContext, +} + +impl SignerWrapper { + /// Create a wrapped signer from a signer and a context + pub fn new(signer: S, ctx: SignerContext) -> Self { + SignerWrapper { signer, ctx } + } +} + +impl Deref for SignerWrapper { + type Target = S; + + fn deref(&self) -> &Self::Target { + &self.signer + } +} + +/// Common signer methods +pub trait SignerCommon: fmt::Debug + Send + Sync { + /// Return the [`SignerId`] for this signer + /// + /// The [`SignerId`] can be used to lookup a signer in the [`Wallet`](crate::Wallet)'s signers map or to + /// compare two signers. + fn id(&self, secp: &SecpCtx) -> SignerId; + + /// Return the secret key for the signer + /// + /// This is used internally to reconstruct the original descriptor that may contain secrets. + /// External signers that are meant to keep key isolated should just return `None` here (which + /// is the default for this method, if not overridden). + fn descriptor_secret_key(&self) -> Option { + None + } +} + +/// PSBT Input signer +/// +/// This trait can be implemented to provide custom signers to the wallet. If the signer supports signing +/// individual inputs, this trait should be implemented and BDK will provide automatically an implementation +/// for [`TransactionSigner`]. +pub trait InputSigner: SignerCommon { + /// Sign a single psbt input + fn sign_input( + &self, + psbt: &mut psbt::PartiallySignedTransaction, + input_index: usize, + sign_options: &SignOptions, + secp: &SecpCtx, + ) -> Result<(), SignerError>; +} + +/// PSBT signer +/// +/// This trait can be implemented when the signer can't sign inputs individually, but signs the whole transaction +/// at once. +pub trait TransactionSigner: SignerCommon { + /// Sign all the inputs of the psbt + fn sign_transaction( + &self, + psbt: &mut psbt::PartiallySignedTransaction, + sign_options: &SignOptions, + secp: &SecpCtx, + ) -> Result<(), SignerError>; +} + +impl TransactionSigner for T { + fn sign_transaction( + &self, + psbt: &mut psbt::PartiallySignedTransaction, + sign_options: &SignOptions, + secp: &SecpCtx, + ) -> Result<(), SignerError> { + for input_index in 0..psbt.inputs.len() { + self.sign_input(psbt, input_index, sign_options, secp)?; + } + + Ok(()) + } +} + +impl SignerCommon for SignerWrapper> { + fn id(&self, secp: &SecpCtx) -> SignerId { + SignerId::from(self.root_fingerprint(secp)) + } + + fn descriptor_secret_key(&self) -> Option { + Some(DescriptorSecretKey::XPrv(self.signer.clone())) + } +} + +impl InputSigner for SignerWrapper> { + fn sign_input( + &self, + psbt: &mut psbt::PartiallySignedTransaction, + input_index: usize, + sign_options: &SignOptions, + secp: &SecpCtx, + ) -> Result<(), SignerError> { + if input_index >= psbt.inputs.len() { + return Err(SignerError::InputIndexOutOfRange); + } + + if psbt.inputs[input_index].final_script_sig.is_some() + || psbt.inputs[input_index].final_script_witness.is_some() + { + return Ok(()); + } + + let tap_key_origins = psbt.inputs[input_index] + .tap_key_origins + .iter() + .map(|(pk, (_, keysource))| (SinglePubKey::XOnly(*pk), keysource)); + let (public_key, full_path) = match psbt.inputs[input_index] + .bip32_derivation + .iter() + .map(|(pk, keysource)| (SinglePubKey::FullKey(PublicKey::new(*pk)), keysource)) + .chain(tap_key_origins) + .find_map(|(pk, keysource)| { + if self.matches(keysource, secp).is_some() { + Some((pk, keysource.1.clone())) + } else { + None + } + }) { + Some((pk, full_path)) => (pk, full_path), + None => return Ok(()), + }; + + let derived_key = match self.origin.clone() { + Some((_fingerprint, origin_path)) => { + let deriv_path = DerivationPath::from( + &full_path.into_iter().cloned().collect::>() + [origin_path.len()..], + ); + self.xkey.derive_priv(secp, &deriv_path).unwrap() + } + None => self.xkey.derive_priv(secp, &full_path).unwrap(), + }; + + let computed_pk = secp256k1::PublicKey::from_secret_key(secp, &derived_key.private_key); + let valid_key = match public_key { + SinglePubKey::FullKey(pk) if pk.inner == computed_pk => true, + SinglePubKey::XOnly(x_only) if XOnlyPublicKey::from(computed_pk) == x_only => true, + _ => false, + }; + if !valid_key { + Err(SignerError::InvalidKey) + } else { + // HD wallets imply compressed keys + let priv_key = PrivateKey { + compressed: true, + network: self.xkey.network, + inner: derived_key.private_key, + }; + + SignerWrapper::new(priv_key, self.ctx).sign_input(psbt, input_index, sign_options, secp) + } + } +} + +impl SignerCommon for SignerWrapper { + fn id(&self, secp: &SecpCtx) -> SignerId { + SignerId::from(self.public_key(secp).to_pubkeyhash(SigType::Ecdsa)) + } + + fn descriptor_secret_key(&self) -> Option { + Some(DescriptorSecretKey::Single(SinglePriv { + key: self.signer, + origin: None, + })) + } +} + +impl InputSigner for SignerWrapper { + fn sign_input( + &self, + psbt: &mut psbt::PartiallySignedTransaction, + input_index: usize, + sign_options: &SignOptions, + secp: &SecpCtx, + ) -> Result<(), SignerError> { + if input_index >= psbt.inputs.len() || input_index >= psbt.unsigned_tx.input.len() { + return Err(SignerError::InputIndexOutOfRange); + } + + if psbt.inputs[input_index].final_script_sig.is_some() + || psbt.inputs[input_index].final_script_witness.is_some() + { + return Ok(()); + } + + let pubkey = PublicKey::from_private_key(secp, self); + let x_only_pubkey = XOnlyPublicKey::from(pubkey.inner); + + if let SignerContext::Tap { is_internal_key } = self.ctx { + if is_internal_key + && psbt.inputs[input_index].tap_key_sig.is_none() + && sign_options.sign_with_tap_internal_key + { + let (hash, hash_ty) = Tap::sighash(psbt, input_index, None)?; + sign_psbt_schnorr( + &self.inner, + x_only_pubkey, + None, + &mut psbt.inputs[input_index], + hash, + hash_ty, + secp, + ); + } + + if let Some((leaf_hashes, _)) = + psbt.inputs[input_index].tap_key_origins.get(&x_only_pubkey) + { + let leaf_hashes = leaf_hashes + .iter() + .filter(|lh| { + // Removing the leaves we shouldn't sign for + let should_sign = match &sign_options.tap_leaves_options { + TapLeavesOptions::All => true, + TapLeavesOptions::Include(v) => v.contains(lh), + TapLeavesOptions::Exclude(v) => !v.contains(lh), + TapLeavesOptions::None => false, + }; + // Filtering out the leaves without our key + should_sign + && !psbt.inputs[input_index] + .tap_script_sigs + .contains_key(&(x_only_pubkey, **lh)) + }) + .cloned() + .collect::>(); + for lh in leaf_hashes { + let (hash, hash_ty) = Tap::sighash(psbt, input_index, Some(lh))?; + sign_psbt_schnorr( + &self.inner, + x_only_pubkey, + Some(lh), + &mut psbt.inputs[input_index], + hash, + hash_ty, + secp, + ); + } + } + + return Ok(()); + } + + if psbt.inputs[input_index].partial_sigs.contains_key(&pubkey) { + return Ok(()); + } + + let (hash, hash_ty) = match self.ctx { + SignerContext::Segwitv0 => Segwitv0::sighash(psbt, input_index, ())?, + SignerContext::Legacy => Legacy::sighash(psbt, input_index, ())?, + _ => return Ok(()), // handled above + }; + sign_psbt_ecdsa( + &self.inner, + pubkey, + &mut psbt.inputs[input_index], + hash, + hash_ty, + secp, + sign_options.allow_grinding, + ); + + Ok(()) + } +} + +fn sign_psbt_ecdsa( + secret_key: &secp256k1::SecretKey, + pubkey: PublicKey, + psbt_input: &mut psbt::Input, + hash: bitcoin::Sighash, + hash_ty: EcdsaSighashType, + secp: &SecpCtx, + allow_grinding: bool, +) { + let msg = &Message::from_slice(&hash.into_inner()[..]).unwrap(); + let sig = if allow_grinding { + secp.sign_ecdsa_low_r(msg, secret_key) + } else { + secp.sign_ecdsa(msg, secret_key) + }; + secp.verify_ecdsa(msg, &sig, &pubkey.inner) + .expect("invalid or corrupted ecdsa signature"); + + let final_signature = ecdsa::EcdsaSig { sig, hash_ty }; + psbt_input.partial_sigs.insert(pubkey, final_signature); +} + +// Calling this with `leaf_hash` = `None` will sign for key-spend +fn sign_psbt_schnorr( + secret_key: &secp256k1::SecretKey, + pubkey: XOnlyPublicKey, + leaf_hash: Option, + psbt_input: &mut psbt::Input, + hash: taproot::TapSighashHash, + hash_ty: SchnorrSighashType, + secp: &SecpCtx, +) { + use schnorr::TapTweak; + + let keypair = secp256k1::KeyPair::from_seckey_slice(secp, secret_key.as_ref()).unwrap(); + let keypair = match leaf_hash { + None => keypair + .tap_tweak(secp, psbt_input.tap_merkle_root) + .to_inner(), + Some(_) => keypair, // no tweak for script spend + }; + + let msg = &Message::from_slice(&hash.into_inner()[..]).unwrap(); + let sig = secp.sign_schnorr(msg, &keypair); + secp.verify_schnorr(&sig, msg, &XOnlyPublicKey::from_keypair(&keypair).0) + .expect("invalid or corrupted schnorr signature"); + + let final_signature = schnorr::SchnorrSig { sig, hash_ty }; + + if let Some(lh) = leaf_hash { + psbt_input + .tap_script_sigs + .insert((pubkey, lh), final_signature); + } else { + psbt_input.tap_key_sig = Some(final_signature); + } +} + +/// Defines the order in which signers are called +/// +/// The default value is `100`. Signers with an ordering above that will be called later, +/// and they will thus see the partial signatures added to the transaction once they get to sign +/// themselves. +#[derive(Debug, Clone, PartialOrd, PartialEq, Ord, Eq)] +pub struct SignerOrdering(pub usize); + +impl Default for SignerOrdering { + fn default() -> Self { + SignerOrdering(100) + } +} + +#[derive(Debug, Clone)] +struct SignersContainerKey { + id: SignerId, + ordering: SignerOrdering, +} + +impl From<(SignerId, SignerOrdering)> for SignersContainerKey { + fn from(tuple: (SignerId, SignerOrdering)) -> Self { + SignersContainerKey { + id: tuple.0, + ordering: tuple.1, + } + } +} + +/// Container for multiple signers +#[derive(Debug, Default, Clone)] +pub struct SignersContainer(BTreeMap>); + +impl SignersContainer { + /// Create a map of public keys to secret keys + pub fn as_key_map(&self, secp: &SecpCtx) -> KeyMap { + self.0 + .values() + .filter_map(|signer| signer.descriptor_secret_key()) + .filter_map(|secret| secret.to_public(secp).ok().map(|public| (public, secret))) + .collect() + } + + /// Build a new signer container from a [`KeyMap`] + /// + /// Also looks at the corresponding descriptor to determine the [`SignerContext`] to attach to + /// the signers + pub fn build( + keymap: KeyMap, + descriptor: &Descriptor, + secp: &SecpCtx, + ) -> SignersContainer { + let mut container = SignersContainer::new(); + + for (pubkey, secret) in keymap { + let ctx = match descriptor { + Descriptor::Tr(tr) => SignerContext::Tap { + is_internal_key: tr.internal_key() == &pubkey, + }, + _ if descriptor.is_witness() => SignerContext::Segwitv0, + _ => SignerContext::Legacy, + }; + + match secret { + DescriptorSecretKey::Single(private_key) => container.add_external( + SignerId::from( + private_key + .key + .public_key(secp) + .to_pubkeyhash(SigType::Ecdsa), + ), + SignerOrdering::default(), + Arc::new(SignerWrapper::new(private_key.key, ctx)), + ), + DescriptorSecretKey::XPrv(xprv) => container.add_external( + SignerId::from(xprv.root_fingerprint(secp)), + SignerOrdering::default(), + Arc::new(SignerWrapper::new(xprv, ctx)), + ), + }; + } + + container + } +} + +impl SignersContainer { + /// Default constructor + pub fn new() -> Self { + SignersContainer(Default::default()) + } + + /// Adds an external signer to the container for the specified id. Optionally returns the + /// signer that was previously in the container, if any + pub fn add_external( + &mut self, + id: SignerId, + ordering: SignerOrdering, + signer: Arc, + ) -> Option> { + self.0.insert((id, ordering).into(), signer) + } + + /// Removes a signer from the container and returns it + pub fn remove( + &mut self, + id: SignerId, + ordering: SignerOrdering, + ) -> Option> { + self.0.remove(&(id, ordering).into()) + } + + /// Returns the list of identifiers of all the signers in the container + pub fn ids(&self) -> Vec<&SignerId> { + self.0 + .keys() + .map(|SignersContainerKey { id, .. }| id) + .collect() + } + + /// Returns the list of signers in the container, sorted by lowest to highest `ordering` + pub fn signers(&self) -> Vec<&Arc> { + self.0.values().collect() + } + + /// Finds the signer with lowest ordering for a given id in the container. + pub fn find(&self, id: SignerId) -> Option<&Arc> { + self.0 + .range(( + Included(&(id.clone(), SignerOrdering(0)).into()), + Included(&(id.clone(), SignerOrdering(usize::MAX)).into()), + )) + .filter(|(k, _)| k.id == id) + .map(|(_, v)| v) + .next() + } +} + +/// Options for a software signer +/// +/// Adjust the behavior of our software signers and the way a transaction is finalized +#[derive(Debug, Clone)] +pub struct SignOptions { + /// Whether the signer should trust the `witness_utxo`, if the `non_witness_utxo` hasn't been + /// provided + /// + /// Defaults to `false` to mitigate the "SegWit bug" which chould trick the wallet into + /// paying a fee larger than expected. + /// + /// Some wallets, especially if relatively old, might not provide the `non_witness_utxo` for + /// SegWit transactions in the PSBT they generate: in those cases setting this to `true` + /// should correctly produce a signature, at the expense of an increased trust in the creator + /// of the PSBT. + /// + /// For more details see: + pub trust_witness_utxo: bool, + + /// Whether the wallet should assume a specific height has been reached when trying to finalize + /// a transaction + /// + /// The wallet will only "use" a timelock to satisfy the spending policy of an input if the + /// timelock height has already been reached. This option allows overriding the "current height" to let the + /// wallet use timelocks in the future to spend a coin. + pub assume_height: Option, + + /// Whether the signer should use the `sighash_type` set in the PSBT when signing, no matter + /// what its value is + /// + /// Defaults to `false` which will only allow signing using `SIGHASH_ALL`. + pub allow_all_sighashes: bool, + + /// Whether to remove partial signatures from the PSBT inputs while finalizing PSBT. + /// + /// Defaults to `true` which will remove partial signatures during finalization. + pub remove_partial_sigs: bool, + + /// Whether to try finalizing the PSBT after the inputs are signed. + /// + /// Defaults to `true` which will try finalizing PSBT after inputs are signed. + pub try_finalize: bool, + + /// Specifies which Taproot script-spend leaves we should sign for. This option is + /// ignored if we're signing a non-taproot PSBT. + /// + /// Defaults to All, i.e., the wallet will sign all the leaves it has a key for. + pub tap_leaves_options: TapLeavesOptions, + + /// Whether we should try to sign a taproot transaction with the taproot internal key + /// or not. This option is ignored if we're signing a non-taproot PSBT. + /// + /// Defaults to `true`, i.e., we always try to sign with the taproot internal key. + pub sign_with_tap_internal_key: bool, + + /// Whether we should grind ECDSA signature to ensure signing with low r + /// or not. + /// Defaults to `true`, i.e., we always grind ECDSA signature to sign with low r. + pub allow_grinding: bool, +} + +/// Customize which taproot script-path leaves the signer should sign. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TapLeavesOptions { + /// The signer will sign all the leaves it has a key for. + All, + /// The signer won't sign leaves other than the ones specified. Note that it could still ignore + /// some of the specified leaves, if it doesn't have the right key to sign them. + Include(Vec), + /// The signer won't sign the specified leaves. + Exclude(Vec), + /// The signer won't sign any leaf. + None, +} + +impl Default for TapLeavesOptions { + fn default() -> Self { + TapLeavesOptions::All + } +} + +#[allow(clippy::derivable_impls)] +impl Default for SignOptions { + fn default() -> Self { + SignOptions { + trust_witness_utxo: false, + assume_height: None, + allow_all_sighashes: false, + remove_partial_sigs: true, + try_finalize: true, + tap_leaves_options: TapLeavesOptions::default(), + sign_with_tap_internal_key: true, + allow_grinding: true, + } + } +} + +pub(crate) trait ComputeSighash { + type Extra; + type Sighash; + type SighashType; + + fn sighash( + psbt: &psbt::PartiallySignedTransaction, + input_index: usize, + extra: Self::Extra, + ) -> Result<(Self::Sighash, Self::SighashType), SignerError>; +} + +impl ComputeSighash for Legacy { + type Extra = (); + type Sighash = bitcoin::Sighash; + type SighashType = EcdsaSighashType; + + fn sighash( + psbt: &psbt::PartiallySignedTransaction, + input_index: usize, + _extra: (), + ) -> Result<(Self::Sighash, Self::SighashType), SignerError> { + if input_index >= psbt.inputs.len() || input_index >= psbt.unsigned_tx.input.len() { + return Err(SignerError::InputIndexOutOfRange); + } + + let psbt_input = &psbt.inputs[input_index]; + let tx_input = &psbt.unsigned_tx.input[input_index]; + + let sighash = psbt_input + .sighash_type + .unwrap_or_else(|| EcdsaSighashType::All.into()) + .ecdsa_hash_ty() + .map_err(|_| SignerError::InvalidSighash)?; + let script = match psbt_input.redeem_script { + Some(ref redeem_script) => redeem_script.clone(), + None => { + let non_witness_utxo = psbt_input + .non_witness_utxo + .as_ref() + .ok_or(SignerError::MissingNonWitnessUtxo)?; + let prev_out = non_witness_utxo + .output + .get(tx_input.previous_output.vout as usize) + .ok_or(SignerError::InvalidNonWitnessUtxo)?; + + prev_out.script_pubkey.clone() + } + }; + + Ok(( + sighash::SighashCache::new(&psbt.unsigned_tx).legacy_signature_hash( + input_index, + &script, + sighash.to_u32(), + )?, + sighash, + )) + } +} + +fn p2wpkh_script_code(script: &Script) -> Script { + ScriptBuilder::new() + .push_opcode(opcodes::all::OP_DUP) + .push_opcode(opcodes::all::OP_HASH160) + .push_slice(&script[2..]) + .push_opcode(opcodes::all::OP_EQUALVERIFY) + .push_opcode(opcodes::all::OP_CHECKSIG) + .into_script() +} + +impl ComputeSighash for Segwitv0 { + type Extra = (); + type Sighash = bitcoin::Sighash; + type SighashType = EcdsaSighashType; + + fn sighash( + psbt: &psbt::PartiallySignedTransaction, + input_index: usize, + _extra: (), + ) -> Result<(Self::Sighash, Self::SighashType), SignerError> { + if input_index >= psbt.inputs.len() || input_index >= psbt.unsigned_tx.input.len() { + return Err(SignerError::InputIndexOutOfRange); + } + + let psbt_input = &psbt.inputs[input_index]; + let tx_input = &psbt.unsigned_tx.input[input_index]; + + let sighash = psbt_input + .sighash_type + .unwrap_or_else(|| EcdsaSighashType::All.into()) + .ecdsa_hash_ty() + .map_err(|_| SignerError::InvalidSighash)?; + + // Always try first with the non-witness utxo + let utxo = if let Some(prev_tx) = &psbt_input.non_witness_utxo { + // Check the provided prev-tx + if prev_tx.txid() != tx_input.previous_output.txid { + return Err(SignerError::InvalidNonWitnessUtxo); + } + + // The output should be present, if it's missing the `non_witness_utxo` is invalid + prev_tx + .output + .get(tx_input.previous_output.vout as usize) + .ok_or(SignerError::InvalidNonWitnessUtxo)? + } else if let Some(witness_utxo) = &psbt_input.witness_utxo { + // Fallback to the witness_utxo. If we aren't allowed to use it, signing should fail + // before we get to this point + witness_utxo + } else { + // Nothing has been provided + return Err(SignerError::MissingNonWitnessUtxo); + }; + let value = utxo.value; + + let script = match psbt_input.witness_script { + Some(ref witness_script) => witness_script.clone(), + None => { + if utxo.script_pubkey.is_v0_p2wpkh() { + p2wpkh_script_code(&utxo.script_pubkey) + } else if psbt_input + .redeem_script + .as_ref() + .map(Script::is_v0_p2wpkh) + .unwrap_or(false) + { + p2wpkh_script_code(psbt_input.redeem_script.as_ref().unwrap()) + } else { + return Err(SignerError::MissingWitnessScript); + } + } + }; + + Ok(( + sighash::SighashCache::new(&psbt.unsigned_tx).segwit_signature_hash( + input_index, + &script, + value, + sighash, + )?, + sighash, + )) + } +} + +impl ComputeSighash for Tap { + type Extra = Option; + type Sighash = taproot::TapSighashHash; + type SighashType = SchnorrSighashType; + + fn sighash( + psbt: &psbt::PartiallySignedTransaction, + input_index: usize, + extra: Self::Extra, + ) -> Result<(Self::Sighash, SchnorrSighashType), SignerError> { + if input_index >= psbt.inputs.len() || input_index >= psbt.unsigned_tx.input.len() { + return Err(SignerError::InputIndexOutOfRange); + } + + let psbt_input = &psbt.inputs[input_index]; + + let sighash_type = psbt_input + .sighash_type + .unwrap_or_else(|| SchnorrSighashType::Default.into()) + .schnorr_hash_ty() + .map_err(|_| SignerError::InvalidSighash)?; + let witness_utxos = (0..psbt.inputs.len()) + .map(|i| psbt.get_utxo_for(i)) + .collect::>(); + let mut all_witness_utxos = vec![]; + + let mut cache = sighash::SighashCache::new(&psbt.unsigned_tx); + let is_anyone_can_pay = psbt::PsbtSighashType::from(sighash_type).to_u32() & 0x80 != 0; + let prevouts = if is_anyone_can_pay { + sighash::Prevouts::One( + input_index, + witness_utxos[input_index] + .as_ref() + .ok_or(SignerError::MissingWitnessUtxo)?, + ) + } else if witness_utxos.iter().all(Option::is_some) { + all_witness_utxos.extend(witness_utxos.iter().filter_map(|x| x.as_ref())); + sighash::Prevouts::All(&all_witness_utxos) + } else { + return Err(SignerError::MissingWitnessUtxo); + }; + + // Assume no OP_CODESEPARATOR + let extra = extra.map(|leaf_hash| (leaf_hash, 0xFFFFFFFF)); + + Ok(( + cache.taproot_signature_hash(input_index, &prevouts, None, extra, sighash_type)?, + sighash_type, + )) + } +} + +impl PartialOrd for SignersContainerKey { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for SignersContainerKey { + fn cmp(&self, other: &Self) -> Ordering { + self.ordering + .cmp(&other.ordering) + .then(self.id.cmp(&other.id)) + } +} + +impl PartialEq for SignersContainerKey { + fn eq(&self, other: &Self) -> bool { + self.id == other.id && self.ordering == other.ordering + } +} + +impl Eq for SignersContainerKey {} + +#[cfg(test)] +mod signers_container_tests { + use super::*; + use crate::descriptor; + use crate::descriptor::IntoWalletDescriptor; + use crate::keys::{DescriptorKey, IntoDescriptorKey}; + use assert_matches::assert_matches; + use bitcoin::secp256k1::{All, Secp256k1}; + use bitcoin::util::bip32; + use bitcoin::Network; + use core::str::FromStr; + use miniscript::ScriptContext; + + fn is_equal(this: &Arc, that: &Arc) -> bool { + let secp = Secp256k1::new(); + this.id(&secp) == that.id(&secp) + } + + // Signers added with the same ordering (like `Ordering::default`) created from `KeyMap` + // should be preserved and not overwritten. + // This happens usually when a set of signers is created from a descriptor with private keys. + #[test] + fn signers_with_same_ordering() { + let secp = Secp256k1::new(); + + let (prvkey1, _, _) = setup_keys(TPRV0_STR); + let (prvkey2, _, _) = setup_keys(TPRV1_STR); + let desc = descriptor!(sh(multi(2, prvkey1, prvkey2))).unwrap(); + let (wallet_desc, keymap) = desc + .into_wallet_descriptor(&secp, Network::Testnet) + .unwrap(); + + let signers = SignersContainer::build(keymap, &wallet_desc, &secp); + assert_eq!(signers.ids().len(), 2); + + let signers = signers.signers(); + assert_eq!(signers.len(), 2); + } + + #[test] + fn signers_sorted_by_ordering() { + let mut signers = SignersContainer::new(); + let signer1 = Arc::new(DummySigner { number: 1 }); + let signer2 = Arc::new(DummySigner { number: 2 }); + let signer3 = Arc::new(DummySigner { number: 3 }); + + // Mixed order insertions verifies we are not inserting at head or tail. + signers.add_external(SignerId::Dummy(2), SignerOrdering(2), signer2.clone()); + signers.add_external(SignerId::Dummy(1), SignerOrdering(1), signer1.clone()); + signers.add_external(SignerId::Dummy(3), SignerOrdering(3), signer3.clone()); + + // Check that signers are sorted from lowest to highest ordering + let signers = signers.signers(); + + assert!(is_equal(signers[0], &signer1)); + assert!(is_equal(signers[1], &signer2)); + assert!(is_equal(signers[2], &signer3)); + } + + #[test] + fn find_signer_by_id() { + let mut signers = SignersContainer::new(); + let signer1 = Arc::new(DummySigner { number: 1 }); + let signer2 = Arc::new(DummySigner { number: 2 }); + let signer3 = Arc::new(DummySigner { number: 3 }); + let signer4 = Arc::new(DummySigner { number: 3 }); // Same ID as `signer3` but will use lower ordering. + + let id1 = SignerId::Dummy(1); + let id2 = SignerId::Dummy(2); + let id3 = SignerId::Dummy(3); + let id_nonexistent = SignerId::Dummy(999); + + signers.add_external(id1.clone(), SignerOrdering(1), signer1.clone()); + signers.add_external(id2.clone(), SignerOrdering(2), signer2.clone()); + signers.add_external(id3.clone(), SignerOrdering(3), signer3.clone()); + + assert_matches!(signers.find(id1), Some(signer) if is_equal(signer, &signer1)); + assert_matches!(signers.find(id2), Some(signer) if is_equal(signer, &signer2)); + assert_matches!(signers.find(id3.clone()), Some(signer) if is_equal(signer, &signer3)); + + // The `signer4` has the same ID as `signer3` but lower ordering. + // It should be found by `id3` instead of `signer3`. + signers.add_external(id3.clone(), SignerOrdering(2), signer4.clone()); + assert_matches!(signers.find(id3), Some(signer) if is_equal(signer, &signer4)); + + // Can't find anything with ID that doesn't exist + assert_matches!(signers.find(id_nonexistent), None); + } + + #[derive(Debug, Clone, Copy)] + struct DummySigner { + number: u64, + } + + impl SignerCommon for DummySigner { + fn id(&self, _secp: &SecpCtx) -> SignerId { + SignerId::Dummy(self.number) + } + } + + impl TransactionSigner for DummySigner { + fn sign_transaction( + &self, + _psbt: &mut psbt::PartiallySignedTransaction, + _sign_options: &SignOptions, + _secp: &SecpCtx, + ) -> Result<(), SignerError> { + Ok(()) + } + } + + const TPRV0_STR:&str = "tprv8ZgxMBicQKsPdZXrcHNLf5JAJWFAoJ2TrstMRdSKtEggz6PddbuSkvHKM9oKJyFgZV1B7rw8oChspxyYbtmEXYyg1AjfWbL3ho3XHDpHRZf"; + const TPRV1_STR:&str = "tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N"; + + const PATH: &str = "m/44'/1'/0'/0"; + + fn setup_keys( + tprv: &str, + ) -> (DescriptorKey, DescriptorKey, Fingerprint) { + let secp: Secp256k1 = Secp256k1::new(); + let path = bip32::DerivationPath::from_str(PATH).unwrap(); + let tprv = bip32::ExtendedPrivKey::from_str(tprv).unwrap(); + let tpub = bip32::ExtendedPubKey::from_priv(&secp, &tprv); + let fingerprint = tprv.fingerprint(&secp); + let prvkey = (tprv, path.clone()).into_descriptor_key().unwrap(); + let pubkey = (tpub, path).into_descriptor_key().unwrap(); + + (prvkey, pubkey, fingerprint) + } +} diff --git a/crates/bdk/src/wallet/tx_builder.rs b/crates/bdk/src/wallet/tx_builder.rs new file mode 100644 index 00000000..c5492c50 --- /dev/null +++ b/crates/bdk/src/wallet/tx_builder.rs @@ -0,0 +1,943 @@ +// Bitcoin Dev Kit +// Written in 2020 by Alekos Filini +// +// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Transaction builder +//! +//! ## Example +//! +//! ``` +//! # use std::str::FromStr; +//! # use bitcoin::*; +//! # use bdk::*; +//! # use bdk::wallet::tx_builder::CreateTx; +//! # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap(); +//! # let mut wallet = doctest_wallet!(); +//! // create a TxBuilder from a wallet +//! let mut tx_builder = wallet.build_tx(); +//! +//! tx_builder +//! // Create a transaction with one output to `to_address` of 50_000 satoshi +//! .add_recipient(to_address.script_pubkey(), 50_000) +//! // With a custom fee rate of 5.0 satoshi/vbyte +//! .fee_rate(FeeRate::from_sat_per_vb(5.0)) +//! // Only spend non-change outputs +//! .do_not_spend_change() +//! // Turn on RBF signaling +//! .enable_rbf(); +//! let (psbt, tx_details) = tx_builder.finish()?; +//! # Ok::<(), bdk::Error>(()) +//! ``` + +use crate::collections::BTreeMap; +use crate::collections::HashSet; +use alloc::{boxed::Box, rc::Rc, string::String, vec::Vec}; +use core::cell::RefCell; +use core::marker::PhantomData; + +use bitcoin::util::psbt::{self, PartiallySignedTransaction as Psbt}; +use bitcoin::{LockTime, OutPoint, Script, Sequence, Transaction}; + +use super::coin_selection::{CoinSelectionAlgorithm, DefaultCoinSelectionAlgorithm}; +use super::persist; +use crate::{ + types::{FeeRate, KeychainKind, LocalUtxo, WeightedUtxo}, + TransactionDetails, +}; +use crate::{Error, Utxo, Wallet}; +/// Context in which the [`TxBuilder`] is valid +pub trait TxBuilderContext: core::fmt::Debug + Default + Clone {} + +/// Marker type to indicate the [`TxBuilder`] is being used to create a new transaction (as opposed +/// to bumping the fee of an existing one). +#[derive(Debug, Default, Clone)] +pub struct CreateTx; +impl TxBuilderContext for CreateTx {} + +/// Marker type to indicate the [`TxBuilder`] is being used to bump the fee of an existing transaction. +#[derive(Debug, Default, Clone)] +pub struct BumpFee; +impl TxBuilderContext for BumpFee {} + +/// A transaction builder +/// +/// A `TxBuilder` is created by calling [`build_tx`] or [`build_fee_bump`] on a wallet. After +/// assigning it, you set options on it until finally calling [`finish`] to consume the builder and +/// generate the transaction. +/// +/// Each option setting method on `TxBuilder` takes and returns `&mut self` so you can chain calls +/// as in the following example: +/// +/// ``` +/// # use bdk::*; +/// # use bdk::wallet::tx_builder::*; +/// # use bitcoin::*; +/// # use core::str::FromStr; +/// # let mut wallet = doctest_wallet!(); +/// # let addr1 = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap(); +/// # let addr2 = addr1.clone(); +/// // chaining +/// let (psbt1, details) = { +/// let mut builder = wallet.build_tx(); +/// builder +/// .ordering(TxOrdering::Untouched) +/// .add_recipient(addr1.script_pubkey(), 50_000) +/// .add_recipient(addr2.script_pubkey(), 50_000); +/// builder.finish()? +/// }; +/// +/// // non-chaining +/// let (psbt2, details) = { +/// let mut builder = wallet.build_tx(); +/// builder.ordering(TxOrdering::Untouched); +/// for addr in &[addr1, addr2] { +/// builder.add_recipient(addr.script_pubkey(), 50_000); +/// } +/// builder.finish()? +/// }; +/// +/// assert_eq!(psbt1.unsigned_tx.output[..2], psbt2.unsigned_tx.output[..2]); +/// # Ok::<(), bdk::Error>(()) +/// ``` +/// +/// At the moment [`coin_selection`] is an exception to the rule as it consumes `self`. +/// This means it is usually best to call [`coin_selection`] on the return value of `build_tx` before assigning it. +/// +/// For further examples see [this module](super::tx_builder)'s documentation; +/// +/// [`build_tx`]: Wallet::build_tx +/// [`build_fee_bump`]: Wallet::build_fee_bump +/// [`finish`]: Self::finish +/// [`coin_selection`]: Self::coin_selection +#[derive(Debug)] +pub struct TxBuilder<'a, D, Cs, Ctx> { + pub(crate) wallet: Rc>>, + pub(crate) params: TxParams, + pub(crate) coin_selection: Cs, + pub(crate) phantom: PhantomData, +} + +/// The parameters for transaction creation sans coin selection algorithm. +//TODO: TxParams should eventually be exposed publicly. +#[derive(Default, Debug, Clone)] +pub(crate) struct TxParams { + pub(crate) recipients: Vec<(Script, u64)>, + pub(crate) drain_wallet: bool, + pub(crate) drain_to: Option