├── .github ├── FUNDING.yml └── workflows │ ├── build.yml │ ├── codecov.yml │ ├── lint.yml │ ├── mobile.yml │ └── test.yml ├── .gitignore ├── .rustfmt.toml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── LICENSE ├── README.md ├── codecov.yml ├── descriptors ├── Cargo.toml └── src │ ├── deduction.rs │ ├── derive.rs │ ├── descriptor.rs │ ├── input.rs │ ├── lib.rs │ └── templates.rs ├── doc └── assets │ └── comparison.png ├── hd ├── Cargo.toml └── src │ ├── account.rs │ ├── derive.rs │ ├── indexes.rs │ ├── lib.rs │ ├── path.rs │ ├── ranges.rs │ ├── standards.rs │ ├── traits.rs │ ├── unsatisfiable.rs │ ├── xkey.rs │ └── xpubref.rs ├── libbitcoin ├── Cargo.lock ├── Cargo.toml └── src │ ├── helpers.rs │ ├── lib.rs │ └── signer.rs ├── license_header ├── onchain ├── Cargo.toml └── src │ ├── blockchain.rs │ ├── lib.rs │ ├── network.rs │ └── resolvers │ ├── electrum.rs │ └── mod.rs ├── psbt ├── Cargo.toml ├── README.md └── src │ ├── construct │ └── mod.rs │ ├── errors.rs │ ├── global.rs │ ├── input.rs │ ├── lex_order.rs │ ├── lib.rs │ ├── output.rs │ ├── p2c.rs │ ├── proprietary.rs │ └── sign │ ├── inmem.rs │ ├── mod.rs │ └── signer.rs ├── rust-toolchain.toml ├── slip132 ├── Cargo.toml ├── README.md └── src │ └── lib.rs └── src ├── bin ├── btc-cold.rs ├── btc-expl.rs └── btc-hot.rs ├── cli └── mod.rs └── lib.rs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [dr-orlovsky] 4 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | default: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Install rust stable 18 | uses: actions-rs/toolchain@v1 19 | with: 20 | toolchain: stable 21 | override: true 22 | - name: Default build 23 | uses: actions-rs/cargo@v1 24 | with: 25 | command: check 26 | args: --workspace 27 | binary: 28 | runs-on: ubuntu-latest 29 | steps: 30 | - uses: actions/checkout@v2 31 | - name: Install rust stable 32 | uses: actions-rs/toolchain@v1 33 | with: 34 | toolchain: stable 35 | override: true 36 | - name: Default build 37 | uses: actions-rs/cargo@v1 38 | with: 39 | command: build 40 | args: --bin btc-hot --bin btc-cold --features cli,serde,hot,electrum 41 | features: 42 | runs-on: ubuntu-latest 43 | strategy: 44 | fail-fast: false 45 | matrix: 46 | feature: 47 | - miniscript 48 | - compiler 49 | - electrum 50 | - strict_encoding 51 | - keygen 52 | - construct 53 | - sign 54 | - hot 55 | - cli 56 | - serde 57 | steps: 58 | - uses: actions/checkout@v2 59 | - name: Install rust stable 60 | uses: actions-rs/toolchain@v1 61 | with: 62 | toolchain: stable 63 | override: true 64 | - name: Feature ${{ matrix.feature }} 65 | uses: actions-rs/cargo@v1 66 | with: 67 | command: check 68 | args: --no-default-features --features=${{ matrix.feature }} 69 | - name: Defaults + ${{ matrix.feature }} 70 | uses: actions-rs/cargo@v1 71 | with: 72 | command: check 73 | args: --features=${{ matrix.feature }} 74 | platforms: 75 | runs-on: ${{ matrix.os }} 76 | strategy: 77 | fail-fast: false 78 | matrix: 79 | os: [ ubuntu-20.04, ubuntu-22.04, macos-11, macos-12, windows-2019, windows-2022 ] 80 | steps: 81 | - uses: actions/checkout@v2 82 | - name: Install rust stable 83 | uses: actions-rs/toolchain@v1 84 | with: 85 | toolchain: stable 86 | override: true 87 | - name: Build with all features 88 | uses: actions-rs/cargo@v1 89 | with: 90 | command: check 91 | args: --workspace --all-targets --all-features 92 | toolchains: 93 | runs-on: ubuntu-latest 94 | strategy: 95 | fail-fast: false 96 | matrix: 97 | toolchain: [ nightly, beta, stable, 1.70.0 ] 98 | steps: 99 | - uses: actions/checkout@v2 100 | - name: Install rust ${{ matrix.toolchain }} 101 | uses: actions-rs/toolchain@v1 102 | with: 103 | toolchain: ${{ matrix.toolchain }} 104 | override: true 105 | - name: All features 106 | uses: actions-rs/cargo@v1 107 | with: 108 | command: check 109 | args: --workspace --all-targets --all-features 110 | -------------------------------------------------------------------------------- /.github/workflows/codecov.yml: -------------------------------------------------------------------------------- 1 | name: Codecov 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | codecov: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Install latest nightly 18 | uses: actions-rs/toolchain@v1 19 | with: 20 | toolchain: nightly 21 | override: true 22 | - name: Build & test 23 | uses: actions-rs/cargo@v1 24 | with: 25 | command: test 26 | args: --workspace --all-features --no-fail-fast 27 | env: 28 | CARGO_INCREMENTAL: '0' 29 | RUSTFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off' 30 | RUSTDOCFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off' 31 | - id: coverage 32 | name: Generate coverage 33 | uses: actions-rs/grcov@0.2-proto 34 | with: 35 | args: > 36 | -t lcov 37 | --llvm 38 | --ignore-not-existing 39 | --ignore "/*" 40 | -o ./target/lcov.info 41 | ./target/debug/ 42 | - name: Upload coverage to Codecov 43 | uses: codecov/codecov-action@v1 44 | with: 45 | file: ${{ steps.coverage.outputs.report }} 46 | directory: ./coverage/reports/ 47 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lints 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | fmt: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Install rustc nightly 18 | uses: actions-rs/toolchain@v1 19 | with: 20 | toolchain: nightly 21 | override: true 22 | components: rustfmt 23 | - uses: actions-rs/cargo@v1 24 | name: Formatting 25 | with: 26 | command: fmt 27 | args: --all -- --check 28 | clippy: 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@v2 32 | - name: Install rustc stable 33 | uses: actions-rs/toolchain@v1 34 | with: 35 | toolchain: stable 36 | override: true 37 | components: clippy 38 | - uses: actions-rs/cargo@v1 39 | name: Clippy 40 | with: 41 | command: clippy 42 | args: --workspace --all-features 43 | doc: 44 | runs-on: ubuntu-latest 45 | steps: 46 | - uses: actions/checkout@v2 47 | - name: Install rustc nightly 48 | uses: actions-rs/toolchain@v1 49 | with: 50 | toolchain: nightly 51 | override: true 52 | components: rust-docs 53 | - uses: actions-rs/cargo@v1 54 | name: Clippy 55 | with: 56 | command: doc 57 | args: --workspace --all-features 58 | -------------------------------------------------------------------------------- /.github/workflows/mobile.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | android: 14 | runs-on: ubuntu-latest 15 | env: 16 | NDK_VERSION: 20.1.5948944 17 | ANDROID_CLI_ZIP: commandlinetools-linux-6858069_latest.zip 18 | ANDROID_CLI_SHA256: 87f6dcf41d4e642e37ba03cb2e387a542aa0bd73cb689a9e7152aad40a6e7a08 19 | steps: 20 | - uses: actions/checkout@v2 21 | - name: Install rust 22 | uses: actions-rs/toolchain@v1 23 | with: 24 | toolchain: stable 25 | override: true 26 | - name: set environment variables 27 | run: | 28 | ANDROID_SDK_ROOT="$GITHUB_WORKSPACE/sdk" 29 | NDK_HOME="$ANDROID_SDK_ROOT/ndk/$NDK_VERSION" 30 | echo "ANDROID_SDK_ROOT=$ANDROID_SDK_ROOT" >> $GITHUB_ENV 31 | echo "NDK_HOME=$NDK_HOME" >> $GITHUB_ENV 32 | echo "PATH=$PATH:$NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin" >> $GITHUB_ENV 33 | - name: Install NDK 34 | run: | 35 | wget -nv https://dl.google.com/android/repository/$ANDROID_CLI_ZIP 36 | echo "$ANDROID_CLI_SHA256 $ANDROID_CLI_ZIP" > SHA256SUMS 37 | sha256sum -c SHA256SUMS 38 | unzip $ANDROID_CLI_ZIP 39 | mkdir -p $ANDROID_SDK_ROOT/cmdline-tools && mv cmdline-tools $ANDROID_SDK_ROOT/cmdline-tools/3.0 40 | yes 2>/dev/null | $ANDROID_SDK_ROOT/cmdline-tools/3.0/bin/sdkmanager --sdk_root=$ANDROID_SDK_ROOT \ 41 | "build-tools;29.0.3" "platforms;android-29" "ndk;$NDK_VERSION" |grep -v '\[='; true 42 | - name: Add rust targets 43 | run: | 44 | rustup target add aarch64-linux-android x86_64-linux-android armv7-linux-androideabi i686-linux-android 45 | - name: Build for aarch64-linux-android 46 | run: | 47 | export CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="$NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android26-clang" 48 | export CC="aarch64-linux-android21-clang" 49 | export CFLAGS="--sysroot=$NDK_HOME/sysroot -I$NDK_HOME/sysroot/usr/include -I$NDK_HOME/sysroot/usr/include/aarch64-linux-android" 50 | export CXX="aarch64-linux-android21-clang++" 51 | export CXXFLAGS="$CFLAGS -nostdlib++ -I$NDK_HOME/sources/cxx-stl/llvm-libc++/include" 52 | export LDFLAGS="--sysroot=$NDK_HOME/platforms/android-21/arch-arm64" 53 | cargo check --features mobile --target=aarch64-linux-android 54 | - name: Build for x86_64-linux-android 55 | run: | 56 | export CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER="$NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/x86_64-linux-android26-clang" 57 | export CC="x86_64-linux-android21-clang" 58 | export CFLAGS="--sysroot=$NDK_HOME/sysroot -I$NDK_HOME/sysroot/usr/include -I$NDK_HOME/sysroot/usr/include/x86_64-linux-android" 59 | export CXX="x86_64-linux-android21-clang++" 60 | export CXXFLAGS="$CFLAGS -nostdlib++ -I$NDK_HOME/sources/cxx-stl/llvm-libc++/include" 61 | export LDFLAGS="--sysroot=$NDK_HOME/platforms/android-21/arch-x86_64" 62 | cargo check --features mobile --target=x86_64-linux-android 63 | - name: Build for armv7-linux-androideabi 64 | run: | 65 | export CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER="$NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi26-clang" 66 | export CC="armv7a-linux-androideabi21-clang" 67 | export CFLAGS="--sysroot=$NDK_HOME/sysroot -I$NDK_HOME/sysroot/usr/include -I$NDK_HOME/sysroot/usr/include/arm-linux-androideabi" 68 | export CXX="armv7a-linux-androideabi21-clang++" 69 | export CXXFLAGS="$CFLAGS -nostdlib++ -I$NDK_HOME/sources/cxx-stl/llvm-libc++/include" 70 | export LDFLAGS="--sysroot=$NDK_HOME/platforms/android-21/arch-arm -L$NDK_HOME/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a" 71 | cargo check --features mobile --target=armv7-linux-androideabi 72 | - name: Build for i686-linux-android 73 | run: | 74 | export CARGO_TARGET_I686_LINUX_ANDROID_LINKER="$NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/i686-linux-android26-clang" 75 | export CC="i686-linux-android21-clang" 76 | export CFLAGS="--sysroot=$NDK_HOME/sysroot -I$NDK_HOME/sysroot/usr/include -I$NDK_HOME/sysroot/usr/include/i686-linux-android" 77 | export CXX="i686-linux-android21-clang++" 78 | export CXXFLAGS="$CFLAGS -nostdlib++ -I$NDK_HOME/sources/cxx-stl/llvm-libc++/include" 79 | export LDFLAGS="--sysroot=$NDK_HOME/platforms/android-21/arch-x86" 80 | cargo check --features mobile --target=i686-linux-android 81 | ios: 82 | runs-on: macos-latest 83 | steps: 84 | - uses: actions/checkout@v2 85 | - name: Install rust 86 | uses: actions-rs/toolchain@v1 87 | with: 88 | toolchain: stable 89 | override: true 90 | - name: Dependencies and targets 91 | run: | 92 | rustup target add aarch64-apple-ios x86_64-apple-ios 93 | cargo install cargo-lipo 94 | - name: build 95 | uses: actions-rs/cargo@v1 96 | with: 97 | command: lipo 98 | args: --features mobile --verbose 99 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | testing: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Install latest stable 18 | uses: actions-rs/toolchain@v1 19 | with: 20 | toolchain: stable 21 | override: true 22 | - name: Test main lib 23 | uses: actions-rs/cargo@v1 24 | with: 25 | command: test 26 | args: --workspace --all-features --no-fail-fast 27 | - name: Test slip132 28 | uses: actions-rs/cargo@v1 29 | with: 30 | command: test 31 | args: --manifest-path slip132/Cargo.toml --all-features --no-fail-fast 32 | testing-c-lib: 33 | runs-on: ubuntu-latest 34 | steps: 35 | - uses: actions/checkout@v2 36 | - name: Install latest nightly 37 | uses: actions-rs/toolchain@v1 38 | with: 39 | toolchain: nightly 40 | override: true 41 | - name: Build & test 42 | uses: actions-rs/cargo@v1 43 | with: 44 | command: test 45 | args: --manifest-path libbitcoin/Cargo.toml --all-features --no-fail-fast 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | target/ 4 | 5 | # These are backup files generated by rustfmt 6 | **/*.rs.bk 7 | 8 | .idea 9 | .vscode 10 | 11 | *.swp 12 | 13 | /dep_test 14 | 15 | /wallets -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 100 2 | format_code_in_doc_comments = true 3 | fn_single_line = true 4 | format_macro_matchers = true 5 | format_strings = true 6 | merge_derives = false 7 | imports_granularity = "Module" 8 | overflow_delimited_expr = true 9 | group_imports = "StdExternalCrate" 10 | use_field_init_shorthand = true 11 | use_try_shorthand = true 12 | wrap_comments = true 13 | comment_width = 80 14 | unstable_features = true 15 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | We do not apply any code of conduct expectations for contributors and 4 | maintainers in their live and behaviour outside the scope of this project. 5 | We believe that a restriction is the word of sin: free people write code, take 6 | their decisions and act in a way they will, taking responsibility for the 7 | consequences. 8 | 9 | Moreover, we will try to protect the freedom of speech of contributors, and 10 | explicit distance from personal or public life of contributors, as long as 11 | they behave in a civil and productive way when contributing and interacting 12 | within the project, and will go to great lengths to not deny anyone 13 | participation. 14 | 15 | Actions within the technical scope of the project (code quality, spamming etc), 16 | as well as interaction with other maintainers and contributors of course is 17 | a factor of the access to the project development and lifecycle. The decision in 18 | these cases will be made by the project maintainers, with the right of veto or 19 | overriding vote reserved for the original project author, Maxim Orlovsky. 20 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing to LNP/BP projects 2 | =============================== 3 | 4 | :+1::tada: 5 | First and foremost, thanks for taking the time to contribute! 6 | :tada::+1: 7 | 8 | The following is a set of guidelines for contributing to [LNP/BP Standards 9 | Association](https://lnp-bp.org) projects, which are hosted in the GitHub 10 | organizations listed in [readme](https://github.com/LNP-BP#Working-groups). 11 | These are mostly guidelines, not rules. Use your best judgment, and feel free to 12 | propose changes to this document in a pull request. 13 | 14 | #### Table Of Contents 15 | - [General](#general) 16 | - [Communication channels](#communication-channels) 17 | - [Asking questions](#asking-questions) 18 | - [Contribution workflow](#contribution-workflow) 19 | * [Preparing PRs](#preparing-prs) 20 | * [Peer review](#peer-review) 21 | - [Coding conventions](#coding-conventions) 22 | - [Security](#security) 23 | - [Testing](#testing) 24 | - [Going further](#going-further) 25 | 26 | 27 | General 28 | ------- 29 | The LNP/BP projects operate an open contributor model where anyone is welcome to 30 | contribute towards development in the form of peer review, documentation, 31 | testing and patches. 32 | 33 | Anyone is invited to contribute without regard to technical experience, 34 | "expertise", OSS experience, age, or other concern. However, the development of 35 | standards & reference implementations demands a high-level of rigor, adversarial 36 | thinking, thorough testing and risk-minimization. Any bug may cost users real 37 | money. That being said, we deeply welcome people contributing for the first time 38 | to an open source project or pick up Rust while contributing. Don't be shy, 39 | you'll learn. 40 | 41 | Communications Channels 42 | ----------------------- 43 | Communication about LNP/BP standards & implementations happens primarily 44 | on #lnp-pb IRC chat on Freenode with the logs available at 45 | 46 | 47 | Discussion about code base improvements happens in GitHub issues and on pull 48 | requests. 49 | 50 | Major projects are tracked [here](https://github.com/orgs/LNP-BP/projects). 51 | Project roadmap is tracked in each repository GitHub milestones. 52 | 53 | Asking Questions 54 | ---------------- 55 | > **Note:** Please don't file an issue to ask a question. Each repository - or 56 | > GitHub organization has a "Discussions" with Q&A section; please post your 57 | > questions there. You'll get faster results by using this channel. 58 | 59 | Alternatively, we have a dedicated developer channel on IRC, #lnp-bp@libera.chat 60 | where you may get helpful advice if you have questions. 61 | 62 | Contribution Workflow 63 | --------------------- 64 | The codebase is maintained using the "contributor workflow" where everyone 65 | without exception contributes patch proposals using "pull requests". This 66 | facilitates social contribution, easy testing and peer review. 67 | 68 | To contribute a patch, the workflow is a as follows: 69 | 70 | 1. Fork Repository 71 | 2. Create topic branch 72 | 3. Commit patches 73 | 74 | In general commits should be atomic and diffs should be easy to read. For this 75 | reason do not mix any formatting fixes or code moves with actual code changes. 76 | Further, each commit, individually, should compile and pass tests, in order to 77 | ensure git bisect and other automated tools function properly. 78 | 79 | When adding a new feature thought must be given to the long term technical debt. 80 | Every new features should be covered by unit tests. 81 | 82 | When refactoring, structure your PR to make it easy to review and don't hesitate 83 | to split it into multiple small, focused PRs. 84 | 85 | The Minimal Supported Rust Version is nightly for the period of active 86 | development; it is enforced by our Travis. Later we plan to fix to some specific 87 | Rust version after the initial library release. 88 | 89 | Commits should cover both the issue fixed and the solution's rationale. 90 | These [guidelines](https://chris.beams.io/posts/git-commit/) should be kept in 91 | mind. 92 | 93 | To facilitate communication with other contributors, the project is making use 94 | of GitHub's "assignee" field. First check that no one is assigned and then 95 | comment suggesting that you're working on it. If someone is already assigned, 96 | don't hesitate to ask if the assigned party or previous commenters are still 97 | working on it if it has been awhile. 98 | 99 | ### Preparing PRs 100 | 101 | The main library development happens in the `master` branch. This branch must 102 | always compile without errors (using Travis CI). All external contributions are 103 | made within PRs into this branch. 104 | 105 | Prerequisites that a PR must satisfy for merging into the `master` branch: 106 | * the tip of any PR branch must compile and pass unit tests with no errors, with 107 | every feature combination (including compiling the fuzztests) on MSRV, stable 108 | and nightly compilers (this is partially automated with CI, so the rule 109 | is that we will not accept commits which do not pass GitHub CI); 110 | * contain all necessary tests for the introduced functional (either as a part of 111 | commits, or, more preferably, as separate commits, so that it's easy to 112 | reorder them during review and check that the new tests fail without the new 113 | code); 114 | * contain all inline docs for newly introduced API and pass doc tests; 115 | * be based on the recent `master` tip from the original repository at. 116 | 117 | NB: reviewers may run more complex test/CI scripts, thus, satisfying all the 118 | requirements above is just a preliminary, but not necessary sufficient step for 119 | getting the PR accepted as a valid candidate PR for the `master` branch. 120 | 121 | Additionally, to the `master` branch some repositories may have `develop` branch 122 | for any experimental developments. This branch may not compile and should not be 123 | used by any projects depending on the library. 124 | 125 | ### Peer review 126 | 127 | Anyone may participate in peer review which is expressed by comments in the pull 128 | request. Typically reviewers will review the code for obvious errors, as well as 129 | test out the patch set and opine on the technical merits of the patch. PR should 130 | be reviewed first on the conceptual level before focusing on code style or 131 | grammar fixes. 132 | 133 | Coding Conventions 134 | ------------------ 135 | Our CI enforces [clippy's](https://github.com/rust-lang/rust-clippy) 136 | [default linting](https://rust-lang.github.io/rust-clippy/rust-1.52.0/index.html) 137 | and [rustfmt](https://github.com/rust-lang/rustfmt) formatting defined by rules 138 | in [.rustfmt.toml](./.rustfmt.toml). The linter should be run with current 139 | stable rust compiler, while formatter requires nightly version due to the use of 140 | unstable formatting parameters. 141 | 142 | If you use rustup, to lint locally you may run the following instructions: 143 | 144 | ```console 145 | rustup component add clippy 146 | rustup component add fmt 147 | cargo +stable clippy --workspace --all-features 148 | cargo +nightly fmt --all 149 | ``` 150 | 151 | Security 152 | -------- 153 | Security is the primary focus of LNP/BP libraries; disclosure of security 154 | vulnerabilities helps prevent user loss of funds. If you believe a vulnerability 155 | may affect other implementations, please inform them. Guidelines for a 156 | responsible disclosure can be found in [SECURITY.md](./SECURITY.md) file in the 157 | project root. 158 | 159 | Note that some of LNP/BP projects are currently considered "pre-production". 160 | Such projects can be distinguished by the absence of `SECURITY.md`. In such 161 | cases there are no special handling of security issues; please simply open 162 | an issue on GitHub. 163 | 164 | Testing 165 | ------- 166 | Related to the security aspect, LNP/BP developers take testing very seriously. 167 | Due to the modular nature of the project, writing new functional tests is easy 168 | and good test coverage of the codebase is an important goal. 169 | 170 | Fuzzing is heavily encouraged: feel free to add related material under `fuzz/` 171 | 172 | Mutation testing is planned; any contribution there would be warmly welcomed. 173 | 174 | Going further 175 | ------------- 176 | You may be interested in Jon Atack guide on 177 | [How to review Bitcoin Core PRs][Review] and [How to make Bitcoin Core PRs][PR]. 178 | While there are differences between the projects in terms of context and 179 | maturity, many of the suggestions offered apply to this project. 180 | 181 | Overall, have fun :) 182 | 183 | [Review]: https://github.com/jonatack/bitcoin-development/blob/master/how-to-review-bitcoin-core-prs.md 184 | [PR]: https://github.com/jonatack/bitcoin-development/blob/master/how-to-make-bitcoin-core-prs.md 185 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [".", "slip132", "descriptors", "hd", "psbt", "onchain"] 3 | default-members = ["."] 4 | exclude = ["contrib", "libbitcoin"] 5 | 6 | [workspace.package] 7 | version = "0.10.2" 8 | license = "Apache-2.0" 9 | authors = ["Dr. Maxim Orlovsky "] 10 | repository = "https://github.com/BP-WG/descriptor-wallet" 11 | homepage = "https://lnp-bp.org" 12 | categories = ["cryptography::cryptocurrencies", "encoding", "parsing"] 13 | readme = "README.md" 14 | edition = "2021" 15 | rust-version = "1.70.0" # due to clap 16 | 17 | [workspace.dependencies] 18 | amplify = "3.14.2" 19 | strict_encoding = "0.9.0" 20 | secp256k1 = { version = "0.24.3", features = ["global-context"] } 21 | bitcoin = "0.29.2" 22 | bitcoin_scripts = "0.10.0" 23 | bitcoin_blockchain = "0.10.0" 24 | bitcoin_hd = { version = "0.10.2", path = "./hd" } 25 | bitcoin_onchain = { version = "0.10.2", path = "./onchain" } 26 | descriptors = { version = "0.10.2", path = "./descriptors", default-features = false } 27 | psbt = { version = "0.10.2", path = "./psbt", default-features = false } 28 | slip132 = { version = "0.10.0", path = "./slip132" } 29 | miniscript_crate = { package = "miniscript", version = "9.0.1" } 30 | chrono = "0.4.19" 31 | clap = { version = "4.1.13", features = ["derive"] } 32 | 33 | [package] 34 | name = "descriptor-wallet" 35 | version = { workspace = true } 36 | license = { workspace = true } 37 | authors = { workspace = true } 38 | description = "Libraries and command line tool for building descriptor-based bitcoin wallets" 39 | repository = { workspace = true } 40 | homepage = { workspace = true } 41 | keywords = ["bitcoin", "wallet", "cryptocurrency", "descriptor", "miniscript"] 42 | categories = { workspace = true } 43 | readme = { workspace = true } 44 | edition = { workspace = true } 45 | rust-version = { workspace = true } 46 | exclude = [".github", "contrib", "slip132", "libbitcoin", "descriptors", "scripts", "hd", "psbt"] 47 | 48 | [lib] 49 | name = "wallet" 50 | path = "src/lib.rs" 51 | crate-type = ["rlib", "staticlib"] 52 | 53 | [[bin]] 54 | name = "btc-hot" 55 | required-features = ["hot", "cli"] 56 | 57 | [[bin]] 58 | name = "btc-cold" 59 | required-features = ["cli"] 60 | 61 | [[bin]] 62 | name = "btc-expl" 63 | required-features = ["cli", "compiler"] 64 | 65 | [dependencies] 66 | amplify = { workspace = true } 67 | strict_encoding_crate = { package = "strict_encoding", version = "0.9.0", features = ["bitcoin", "derive"], optional = true } 68 | bitcoin = { workspace = true } 69 | bitcoin_scripts = { workspace = true } 70 | bitcoin_blockchain = { workspace = true } 71 | bitcoin_hd = { workspace = true } 72 | bitcoin_onchain = { workspace = true } 73 | descriptors = { workspace = true } 74 | psbt = { workspace = true } 75 | slip132 = { workspace = true } 76 | miniscript_crate = { workspace = true, optional = true } 77 | electrum-client = { version = "0.14.0", optional = true } 78 | serde_crate = { package = "serde", version = "1", features = ["derive"], optional = true } 79 | serde_with = { version = "2.3", features = ["hex"], optional = true } 80 | serde_yaml = { version = "0.9", optional = true } 81 | chrono = { workspace = true } 82 | clap = { workspace = true, optional = true } 83 | bip39 = { version = "2.0.0", optional = true } 84 | aes = { version = "0.8.2", optional = true } 85 | rpassword = { version = "7.2.0", optional = true } 86 | colored = { version = "2", optional = true } 87 | 88 | [dev-dependencies] 89 | bitcoin = { version = "0.29.2", features = ["rand"] } 90 | 91 | [features] 92 | default = [] 93 | all = [ 94 | "mobile", 95 | "miniscript", 96 | "electrum", 97 | "strict_encoding", 98 | "keygen", 99 | "construct", 100 | "compiler", 101 | "sign", 102 | "hot", 103 | "cli", 104 | "serde", 105 | ] 106 | mobile = ["miniscript", "compiler", "electrum", "strict_encoding", "hot", "construct"] 107 | miniscript = [ 108 | "strict_encoding_crate/miniscript", 109 | "bitcoin_hd/miniscript", 110 | "bitcoin_onchain/miniscript_descriptors", 111 | "descriptors/miniscript", 112 | "psbt/miniscript", 113 | ] 114 | compiler = ["miniscript", "miniscript_crate/compiler"] 115 | electrum = [ 116 | "electrum-client", 117 | "bitcoin_onchain/electrum" 118 | ] 119 | strict_encoding = [ 120 | "slip132/strict_encoding" 121 | ] 122 | sign = ["psbt/sign"] 123 | construct = ["psbt/construct"] 124 | hot = [ 125 | "keygen", 126 | "bip39", 127 | "aes", 128 | "rpassword", 129 | "sign" 130 | ] 131 | cli = [ 132 | "electrum", 133 | "construct", 134 | "miniscript", 135 | "miniscript_crate", 136 | "strict_encoding", 137 | "strict_encoding_crate", 138 | "serde", 139 | "colored", 140 | "clap", 141 | "bitcoin_hd/clap", 142 | "serde_yaml", 143 | "bitcoin/base64" 144 | ] 145 | keygen = ["bitcoin/rand", "amplify/rand", "descriptors/rand"] 146 | serde = [ 147 | "slip132/serde", 148 | "bitcoin_onchain/serde", 149 | "bitcoin_hd/serde", 150 | "psbt/serde", 151 | "descriptors/serde" 152 | ] 153 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # This image uses cargo-chef to build the application in order to compile 2 | # the dependencies apart from the main application. This allows the compiled 3 | # dependencies to be cached in the Docker layer and greatly reduce the 4 | # build time when there isn't any dependency changes. 5 | # 6 | # https://github.com/LukeMathWalker/cargo-chef 7 | 8 | ARG SRC_DIR=/usr/local/src/bp-descriptor-wallet 9 | ARG BUILDER_DIR=/srv/bp-descriptor-wallet 10 | 11 | # Base image 12 | FROM rust:1.59.0-slim-bullseye as chef 13 | 14 | ARG SRC_DIR 15 | ARG BUILDER_DIR 16 | 17 | RUN rustup default stable 18 | RUN rustup update 19 | RUN cargo install cargo-chef --locked 20 | 21 | WORKDIR $SRC_DIR 22 | 23 | # Cargo chef step that analyzes the project to determine the minimum subset of 24 | # files (Cargo.lock and Cargo.toml manifests) required to build it and cache 25 | # dependencies 26 | FROM chef AS planner 27 | 28 | COPY . . 29 | RUN cargo chef prepare --recipe-path recipe.json 30 | 31 | FROM chef AS builder 32 | 33 | ARG SRC_DIR 34 | ARG BUILDER_DIR 35 | 36 | COPY --from=planner "${SRC_DIR}/recipe.json" recipe.json 37 | 38 | # Build dependencies - this is the caching Docker layer 39 | RUN cargo chef cook --release --recipe-path recipe.json --target-dir "${BUILDER_DIR}" 40 | 41 | # Copy all files and build application 42 | COPY . . 43 | RUN cargo build --release --target-dir "${BUILDER_DIR}" --bins --all-features 44 | 45 | # Final image with binaries 46 | FROM debian:bullseye-slim as final 47 | 48 | ARG BUILDER_DIR 49 | ARG BIN_DIR=/usr/local/bin 50 | ARG DATA_DIR=/var/lib/bp-descriptor-wallet 51 | ARG USER=bp 52 | 53 | RUN adduser --home "${DATA_DIR}" --shell /bin/bash --disabled-login \ 54 | --gecos "${USER} user" ${USER} 55 | 56 | COPY --from=builder --chown=${USER}:${USER} \ 57 | "${BUILDER_DIR}/release" "${BIN_DIR}" 58 | 59 | WORKDIR "${BIN_DIR}" 60 | 61 | # Remove build artifacts in order to keep only the binaries 62 | RUN rm -rf */ *.d .[^.] .??* 63 | 64 | USER ${USER} 65 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | Copyright (c) 2020-2022 LNP/BP Standards Association 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Descriptor wallet library 2 | 3 | ![Build](https://github.com/BP-WG/descriptor-wallet/workflows/Build/badge.svg) 4 | ![Tests](https://github.com/BP-WG/descriptor-wallet/workflows/Tests/badge.svg) 5 | ![Lints](https://github.com/BP-WG/descriptor-wallet/workflows/Lints/badge.svg) 6 | [![codecov](https://codecov.io/gh/BP-WG/descriptor-wallet/branch/master/graph/badge.svg)](https://codecov.io/gh/BP-WG/descriptor-wallet) 7 | 8 | [![crates.io](https://img.shields.io/crates/v/descriptor-wallet)](https://crates.io/crates/descriptor-wallet) 9 | [![Docs](https://docs.rs/descriptor-wallet/badge.svg)](https://docs.rs/descriptor-wallet) 10 | [![unsafe forbidden](https://img.shields.io/badge/unsafe-forbidden-success.svg)](https://github.com/rust-secure-code/safety-dance/) 11 | [![Apache2 licensed](https://img.shields.io/badge/license-Apache%202-blue)](./LICENSE) 12 | 13 | Library for building descriptor-based bitcoin wallets. Everything a modern 14 | cold and hot bitcoin wallet needs, but which is not (yet) a part of 15 | [rust-bitcoin](https://crates.io/bitcoin) library. 16 | 17 | The library clearly separates parts requiring access to private keys from 18 | those which should operate never touching them. It is advised that wallets 19 | should be designed in a way assuming zero private key access for all of their 20 | operations aside from transaction signing; this part must be separated into 21 | other repository/library and be strictly controlled. One may look after 22 | command-line `btc-hot` and `btc-cold` wallets in [`bin`](bin) directory for an 23 | example of how this can be done. 24 | 25 | Library provides 26 | 27 | - efficient manipulations with BIP-32 derivation paths, separating derivations 28 | requiring private key access from those, which will always operate without; 29 | - miniscript & classical bitcoin descriptors; 30 | - PSBT constructor using input descriptors, which allow to specify custom 31 | information about RBFs, previous public key P2C tweaks and custom hash types 32 | on a per-input basis; 33 | - PSBT signer, supporting RBFs, relative and absolute timelocks, all sighash 34 | types, complex scripts, including witness- and taproot-based; 35 | - script templates allowing embedding extended pubkeys into bitcoin script 36 | assembly; 37 | - lexicographic ordering of transaction & PSBT inputs & oututs; 38 | - script type system; 39 | - helper types for working with hash-lock contracts; 40 | - PSBT utility functions (retrieving previous output, computing fee); 41 | - transaction resolver API on top of Electrum Server API for convenience 42 | computation of already-mined transaction fees etc; 43 | - support for SLIP-32/132 extended pubkey types (`ypub`, `zprv` etc). 44 | 45 | ![Wallet comparison diagram](./doc/assets/comparison.png) 46 | 47 | ## Command-line wallets 48 | 49 | One may install command-line wallets with the following command (requires 50 | rust compiler and `rustup` tools to be already installed on a system): 51 | 52 | ```console 53 | $ rustup default stable 54 | $ rustup update 55 | $ git clone https://github.com/BP-WG/descriptor-wallet 56 | $ cd descriptor-wallet 57 | $ cargo install --path . --locked --all-features 58 | ``` 59 | 60 | This will add `btc-hot` and `btc-cold` commands to the system. 61 | 62 | [bin]: https://github.com/BP-WG/descriptor-wallet/tree/master/src/bin 63 | 64 | ### Install with Docker 65 | 66 | #### Build 67 | 68 | Clone the repository and checkout to the desired version (here `v0.8.0`): 69 | 70 | ```console 71 | $ git clone https://github.com/BP-WG/descriptor-wallet 72 | $ cd descriptor-wallet 73 | $ git checkout v0.8.0 74 | ``` 75 | 76 | Build and tag the Docker image: 77 | 78 | ```console 79 | $ docker build -t descriptor-wallet:v0.8.0 . 80 | ``` 81 | 82 | #### Usage 83 | 84 | ```console 85 | $ docker run descriptor-wallet:v0.8.0 btc-hot --help 86 | $ docker run descriptor-wallet:v0.8.0 btc-cold --help 87 | ``` 88 | 89 | #### Examples with files 90 | 91 | ```console 92 | $ docker run -v $PWD/data:/data descriptor-wallet:v0.8.0 btc-hot seed /data/testnet.seed 93 | $ docker run -v $PWD/data:/data descriptor-wallet:v0.8.0 btc-hot derive --testnet /data/testnet.seed /data/testnet 94 | ``` 95 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | require_ci_to_pass: no 3 | 4 | coverage: 5 | precision: 1 6 | round: nearest 7 | range: "0...95" 8 | # TODO: Update codecov default requirements after v1.0 release 9 | status: 10 | project: 11 | default: 12 | target: 0% 13 | threshold: 1% 14 | patch: 15 | default: 16 | target: 0% 17 | threshold: 1% -------------------------------------------------------------------------------- /descriptors/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "descriptors" 3 | version = { workspace = true } 4 | license = { workspace = true } 5 | authors = { workspace = true } 6 | description = "Bitcoin descriptors library (part of descriptor-wallet)" 7 | repository = { workspace = true } 8 | homepage = { workspace = true } 9 | keywords = ["bitcoin", "wallet", "cryptocurrency", "descriptor", "bip32"] 10 | categories = { workspace = true } 11 | readme = { workspace = true } 12 | edition = { workspace = true } 13 | rust-version = { workspace = true } 14 | exclude = [] 15 | 16 | [dependencies] 17 | amplify = { workspace = true } 18 | strict_encoding = { workspace = true } 19 | bitcoin = { workspace = true } 20 | bitcoin_scripts = { workspace = true } 21 | bitcoin_blockchain = { workspace = true } 22 | bitcoin_hd = { workspace = true } 23 | miniscript_crate = { workspace = true, features = ["compiler"], optional = true } 24 | chrono = { workspace = true } 25 | serde_crate = { package = "serde", version = "1", optional = true } 26 | serde_with = { version = "2.3", features = ["hex"], optional = true } 27 | 28 | [features] 29 | all = [ 30 | "rand", 31 | "miniscript", 32 | "serde" 33 | ] 34 | default = [] 35 | rand = [ 36 | "bitcoin/rand", 37 | "amplify/rand" 38 | ] 39 | miniscript = [ 40 | "miniscript_crate", 41 | "bitcoin_hd/miniscript" 42 | ] 43 | serde = [ 44 | "serde_crate", 45 | "serde_with", 46 | "bitcoin_scripts/serde" 47 | ] 48 | -------------------------------------------------------------------------------- /descriptors/src/deduction.rs: -------------------------------------------------------------------------------- 1 | // Wallet-level libraries for bitcoin protocol by LNP/BP Association 2 | // 3 | // Written in 2020-2022 by 4 | // Dr. Maxim Orlovsky 5 | // 6 | // This software is distributed without any warranty. 7 | // 8 | // You should have received a copy of the Apache-2.0 License 9 | // along with this software. 10 | // If not, see . 11 | 12 | use bitcoin::util::address::WitnessVersion; 13 | use bitcoin_scripts::{PubkeyScript, RedeemScript}; 14 | 15 | use crate::CompositeDescrType; 16 | 17 | /// Errors that happens during deduction process 18 | #[derive( 19 | Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Display, Error 20 | )] 21 | #[display(doc_comments)] 22 | pub enum DeductionError { 23 | /// input spends non-taproot witness version 1 24 | NonTaprootV1, 25 | 26 | /// input spends future witness version {0} 27 | UnsupportedWitnessVersion(WitnessVersion), 28 | 29 | /// input spends P2SH output, but no `redeedScript` is present in the PSBT 30 | /// input data 31 | P2shWithoutRedeemScript, 32 | 33 | /// redeem script is invalid in context of nested (legacy) P2W*-in-P2SH 34 | /// spending 35 | InvalidRedeemScript, 36 | } 37 | 38 | impl CompositeDescrType { 39 | /// Deduction of a descriptor type from a `scriptPubkey` data and data 40 | /// inside redeem script and witness scripts. 41 | /// 42 | /// # Errors 43 | /// 44 | /// The function may [`DeductionError`] 45 | /// 46 | /// # Panics 47 | /// 48 | /// Panics if PSBT integrity is broken and current input does not have an 49 | /// associated previous output data or these data are incorrect. 50 | pub fn deduce( 51 | spk: &PubkeyScript, 52 | redeem_script: Option<&RedeemScript>, 53 | witness_script_known: bool, 54 | ) -> Result { 55 | let witness_version = spk.witness_version(); 56 | match (spk, witness_version) { 57 | (spk, _) if spk.is_p2pk() => Ok(CompositeDescrType::Pk), 58 | (spk, _) if spk.is_p2pkh() => Ok(CompositeDescrType::Pkh), 59 | (spk, _) if spk.is_v0_p2wpkh() => Ok(CompositeDescrType::Wpkh), 60 | (spk, _) if spk.is_v0_p2wsh() => Ok(CompositeDescrType::Wsh), 61 | (spk, _) if spk.is_v1_p2tr() => Ok(CompositeDescrType::Tr), 62 | (spk, _) if spk.is_p2sh() => { 63 | let redeem_script = if let Some(redeem_script) = redeem_script { 64 | redeem_script 65 | } else { 66 | return Err(DeductionError::P2shWithoutRedeemScript); 67 | }; 68 | if witness_script_known { 69 | if redeem_script.is_v0_p2wpkh() { 70 | Ok(CompositeDescrType::ShWpkh) 71 | } else if redeem_script.is_v0_p2wsh() { 72 | Ok(CompositeDescrType::ShWsh) 73 | } else { 74 | Err(DeductionError::InvalidRedeemScript) 75 | } 76 | } else { 77 | Ok(CompositeDescrType::Sh) 78 | } 79 | } 80 | (_, Some(WitnessVersion::V1)) => Err(DeductionError::NonTaprootV1), 81 | (_, Some(version)) => Err(DeductionError::UnsupportedWitnessVersion(version)), 82 | (_, None) => Ok(CompositeDescrType::Bare), 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /descriptors/src/derive.rs: -------------------------------------------------------------------------------- 1 | // Wallet-level libraries for bitcoin protocol by LNP/BP Association 2 | // 3 | // Written in 2020-2022 by 4 | // Dr. Maxim Orlovsky 5 | // 6 | // This software is distributed without any warranty. 7 | // 8 | // You should have received a copy of the Apache-2.0 License 9 | // along with this software. 10 | // If not, see . 11 | 12 | use bitcoin::secp256k1::{Secp256k1, Verification}; 13 | use bitcoin::{Network, Script}; 14 | #[cfg(feature = "miniscript")] 15 | use bitcoin_hd::{DerivationAccount, DerivePatternError}; 16 | use bitcoin_hd::{DeriveError, UnhardenedIndex}; 17 | use bitcoin_scripts::address::AddressCompat; 18 | 19 | #[cfg(not(feature = "miniscript"))] 20 | pub mod miniscript { 21 | #[derive( 22 | Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Display, Error 23 | )] 24 | #[display(Debug)] 25 | pub enum Error {} 26 | } 27 | 28 | /// Methods for deriving from output descriptor. 29 | pub trait DeriveDescriptor { 30 | /// Generated descriptor type as an output from 31 | /// [`DeriveDescriptor::derive_descriptor`]. 32 | type Output; 33 | 34 | /// Translates descriptor to a specifically-derived form. 35 | fn derive_descriptor( 36 | &self, 37 | secp: &Secp256k1, 38 | pat: impl AsRef<[UnhardenedIndex]>, 39 | ) -> Result; 40 | } 41 | 42 | /// Standard methods which should be supported by descriptors of different 43 | /// sorts. 44 | pub trait Descriptor { 45 | /// Checks sanity of the output descriptor (see [`DeriveError`] for the list 46 | /// of possible inconsistencies). 47 | fn check_sanity(&self) -> Result<(), DeriveError>; 48 | 49 | /// Measures length of the derivation wildcard pattern accross all keys 50 | /// participating descriptor 51 | fn derive_pattern_len(&self) -> Result; 52 | 53 | /// Detects bitcoin network which should be used with the provided 54 | /// descriptor 55 | fn network(&self, regtest: bool) -> Result; 56 | 57 | /// Generates address from the descriptor for specific derive pattern 58 | fn address( 59 | &self, 60 | secp: &Secp256k1, 61 | pat: impl AsRef<[UnhardenedIndex]>, 62 | regtest: bool, 63 | ) -> Result; 64 | 65 | /// Creates scriptPubkey for specific derive pattern in pre-taproot 66 | /// descriptors 67 | fn script_pubkey_pretr( 68 | &self, 69 | secp: &Secp256k1, 70 | pat: impl AsRef<[UnhardenedIndex]>, 71 | ) -> Result; 72 | 73 | /// Creates scriptPubkey for specific derive pattern in taproot descriptors 74 | fn script_pubkey_tr( 75 | &self, 76 | secp: &Secp256k1, 77 | pat: impl AsRef<[UnhardenedIndex]>, 78 | ) -> Result; 79 | } 80 | 81 | #[cfg(feature = "miniscript")] 82 | mod ms { 83 | use std::cell::Cell; 84 | 85 | use bitcoin::XOnlyPublicKey; 86 | use bitcoin_hd::account::DerivePublicKey; 87 | use bitcoin_hd::SegmentIndexes; 88 | use bitcoin_scripts::address::AddressNetwork; 89 | use miniscript::{translate_hash_fail, ForEachKey, TranslatePk, Translator}; 90 | 91 | use super::*; 92 | 93 | struct KeyTranslator<'a, C: Verification> { 94 | secp: &'a Secp256k1, 95 | pat: &'a [UnhardenedIndex], 96 | } 97 | 98 | impl<'a, C> Translator 99 | for KeyTranslator<'a, C> 100 | where 101 | C: Verification, 102 | { 103 | fn pk(&mut self, pk: &DerivationAccount) -> Result { 104 | pk.derive_public_key(self.secp, self.pat) 105 | .map(bitcoin::PublicKey::new) 106 | } 107 | 108 | translate_hash_fail!(DerivationAccount, bitcoin::PublicKey, DerivePatternError); 109 | } 110 | 111 | impl<'a, C> Translator 112 | for KeyTranslator<'a, C> 113 | where 114 | C: Verification, 115 | { 116 | fn pk(&mut self, pk: &DerivationAccount) -> Result { 117 | pk.derive_public_key(self.secp, self.pat) 118 | .map(XOnlyPublicKey::from) 119 | } 120 | 121 | translate_hash_fail!(DerivationAccount, XOnlyPublicKey, DerivePatternError); 122 | } 123 | 124 | impl DeriveDescriptor for miniscript::Descriptor 125 | where 126 | Self: TranslatePk, 127 | { 128 | type Output = miniscript::Descriptor; 129 | 130 | fn derive_descriptor( 131 | &self, 132 | secp: &Secp256k1, 133 | pat: impl AsRef<[UnhardenedIndex]>, 134 | ) -> Result, DeriveError> { 135 | let pat = pat.as_ref(); 136 | if pat.len() != self.derive_pattern_len()? { 137 | return Err(DeriveError::DerivePatternMismatch); 138 | } 139 | let mut translator = KeyTranslator { secp, pat }; 140 | as TranslatePk<_, bitcoin::PublicKey>>::translate_pk(self, &mut translator) 141 | .map_err(DeriveError::from) 142 | } 143 | } 144 | 145 | impl DeriveDescriptor for miniscript::Descriptor 146 | where 147 | Self: TranslatePk, 148 | { 149 | type Output = miniscript::Descriptor; 150 | 151 | fn derive_descriptor( 152 | &self, 153 | secp: &Secp256k1, 154 | pat: impl AsRef<[UnhardenedIndex]>, 155 | ) -> Result, DeriveError> { 156 | let pat = pat.as_ref(); 157 | if pat.len() != self.derive_pattern_len()? { 158 | return Err(DeriveError::DerivePatternMismatch); 159 | } 160 | let mut translator = KeyTranslator { secp, pat }; 161 | as TranslatePk<_, XOnlyPublicKey>>::translate_pk(self, &mut translator) 162 | .map_err(DeriveError::from) 163 | } 164 | } 165 | 166 | impl Descriptor for miniscript::Descriptor { 167 | #[inline] 168 | fn check_sanity(&self) -> Result<(), DeriveError> { 169 | self.derive_pattern_len()?; 170 | self.network(false)?; 171 | Ok(()) 172 | } 173 | 174 | fn derive_pattern_len(&self) -> Result { 175 | let len = Cell::new(None); 176 | self.for_each_key(|key| { 177 | let c = key 178 | .terminal_path 179 | .iter() 180 | .filter(|step| step.count() > 1) 181 | .count(); 182 | match (len.get(), c) { 183 | (None, c) => { 184 | len.set(Some(c)); 185 | true 186 | } 187 | (Some(c1), c2) if c1 != c2 => false, 188 | _ => true, 189 | } 190 | }); 191 | len.get().ok_or(DeriveError::NoKeys) 192 | } 193 | 194 | fn network(&self, regtest: bool) -> Result { 195 | let network = Cell::new(None); 196 | self.for_each_key(|key| match (network.get(), key.account_xpub.network) { 197 | (None, net) => { 198 | network.set(Some(net)); 199 | true 200 | } 201 | (Some(net1), net2) if net1 != net2 => false, 202 | _ => true, 203 | }); 204 | let network = network.get().ok_or(DeriveError::NoKeys)?; 205 | match (network, regtest) { 206 | (network, false) => Ok(network), 207 | (Network::Testnet | Network::Signet | Network::Regtest, true) if regtest => { 208 | Ok(Network::Regtest) 209 | } 210 | _ => Err(DeriveError::InconsistentKeyNetwork), 211 | } 212 | } 213 | 214 | #[inline] 215 | fn address( 216 | &self, 217 | secp: &Secp256k1, 218 | pat: impl AsRef<[UnhardenedIndex]>, 219 | regtest: bool, 220 | ) -> Result { 221 | let network = AddressNetwork::from(self.network(regtest)?); 222 | let spk = Descriptor::script_pubkey_pretr(self, secp, pat)?; 223 | AddressCompat::from_script(&spk.into(), network) 224 | .ok_or(DeriveError::NoAddressForDescriptor) 225 | } 226 | 227 | #[inline] 228 | fn script_pubkey_pretr( 229 | &self, 230 | secp: &Secp256k1, 231 | pat: impl AsRef<[UnhardenedIndex]>, 232 | ) -> Result { 233 | let d = 234 | >::derive_descriptor(self, secp, pat)?; 235 | Ok(d.script_pubkey()) 236 | } 237 | 238 | #[inline] 239 | fn script_pubkey_tr( 240 | &self, 241 | secp: &Secp256k1, 242 | pat: impl AsRef<[UnhardenedIndex]>, 243 | ) -> Result { 244 | let d = >::derive_descriptor(self, secp, pat)?; 245 | Ok(d.script_pubkey()) 246 | } 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /descriptors/src/input.rs: -------------------------------------------------------------------------------- 1 | // Wallet-level libraries for bitcoin protocol by LNP/BP Association 2 | // 3 | // Written in 2020-2022 by 4 | // Dr. Maxim Orlovsky 5 | // 6 | // This software is distributed without any warranty. 7 | // 8 | // You should have received a copy of the Apache-2.0 License 9 | // along with this software. 10 | // If not, see . 11 | 12 | use core::fmt::{self, Display, Formatter}; 13 | use core::str::FromStr; 14 | 15 | use bitcoin::blockdata::transaction::ParseOutPointError; 16 | use bitcoin::hashes::sha256; 17 | use bitcoin::util::bip32; 18 | use bitcoin::util::bip32::Fingerprint; 19 | use bitcoin::{EcdsaSighashType as SighashType, OutPoint}; 20 | use bitcoin_blockchain::locks::{self, SeqNo}; 21 | use bitcoin_hd::{DerivationSubpath, UnhardenedIndex}; 22 | 23 | #[derive(Clone, PartialEq, Eq, Debug)] 24 | #[derive(StrictEncode, StrictDecode)] 25 | pub struct InputDescriptor { 26 | pub outpoint: OutPoint, 27 | pub terminal: DerivationSubpath, 28 | pub seq_no: SeqNo, 29 | pub tweak: Option<(Fingerprint, sha256::Hash)>, 30 | pub sighash_type: SighashType, 31 | } 32 | 33 | impl Display for InputDescriptor { 34 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 35 | Display::fmt(&self.outpoint, f)?; 36 | f.write_str(" ")?; 37 | Display::fmt(&self.terminal, f)?; 38 | if let Some((fingerprint, tweak)) = self.tweak { 39 | f.write_str(" ")?; 40 | Display::fmt(&fingerprint, f)?; 41 | Display::fmt(&tweak, f)?; 42 | } 43 | if self.seq_no != SeqNo::unencumbered(true) { 44 | f.write_str(" ")?; 45 | Display::fmt(&self.seq_no, f)?; 46 | } 47 | if self.sighash_type != SighashType::All { 48 | f.write_str(" ")?; 49 | Display::fmt(&self.sighash_type, f)?; 50 | } 51 | Ok(()) 52 | } 53 | } 54 | 55 | #[derive(Clone, PartialEq, Eq, Debug, Display, From)] 56 | #[display(doc_comments)] 57 | pub enum ParseError { 58 | /// invalid sequence number in input descriptor 59 | #[from] 60 | InvalidSeqNo(locks::ParseError), 61 | 62 | /// invalid signature hash type in input descriptor 63 | InvalidSighash(String), 64 | 65 | /// invalid key derivation in input descriptor 66 | #[from] 67 | InvalidDerivation(bip32::Error), 68 | 69 | /// invalid hexadecimal P2C tweak representation in input descriptor 70 | #[from] 71 | InvalidTweak(bitcoin::hashes::hex::Error), 72 | 73 | /// invalid input outpoint 74 | #[from] 75 | InvalidOutpoint(ParseOutPointError), 76 | 77 | /// invalid tweak descriptor format `{0}`; tweak must consists of account 78 | /// xpub fingerprint and 256-bit number, separated by `:` 79 | InvalidTweakFormat(String), 80 | 81 | /// invalid input descriptor: outpoint information is required 82 | NoOutpoint, 83 | 84 | /// invalid input descriptor: terminal derivation information is required 85 | NoDerivation, 86 | 87 | /// unrecognized input descriptor fragment `{0}` 88 | UnrecognizedFragment(String), 89 | } 90 | 91 | impl std::error::Error for ParseError { 92 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 93 | match self { 94 | ParseError::InvalidSeqNo(err) => Some(err), 95 | ParseError::InvalidSighash(_) => None, 96 | ParseError::InvalidDerivation(err) => Some(err), 97 | ParseError::InvalidTweak(err) => Some(err), 98 | ParseError::InvalidOutpoint(err) => Some(err), 99 | ParseError::InvalidTweakFormat(_) => None, 100 | ParseError::NoOutpoint => None, 101 | ParseError::NoDerivation => None, 102 | ParseError::UnrecognizedFragment(_) => None, 103 | } 104 | } 105 | } 106 | 107 | impl FromStr for InputDescriptor { 108 | type Err = ParseError; 109 | 110 | fn from_str(s: &str) -> Result { 111 | let mut split = s.split_whitespace(); 112 | let outpoint = split.next().ok_or(ParseError::NoOutpoint)?; 113 | let derivation = split.next().ok_or(ParseError::NoDerivation)?; 114 | 115 | let mut d = InputDescriptor { 116 | outpoint: outpoint.parse()?, 117 | terminal: derivation.parse()?, 118 | seq_no: none!(), 119 | tweak: None, 120 | sighash_type: SighashType::All, 121 | }; 122 | 123 | for fragment in split { 124 | if let Ok(seq_no) = SeqNo::from_str(fragment) { 125 | d.seq_no = seq_no; 126 | } else if let Ok(sighash_type) = SighashType::from_str(fragment) { 127 | d.sighash_type = sighash_type; 128 | } else if fragment.contains(':') { 129 | let mut split = fragment.split(':'); 130 | d.tweak = match (split.next(), split.next(), split.next()) { 131 | (Some(""), _, _) => None, 132 | (Some(fingerprint), Some(tweak), None) => { 133 | Some((fingerprint.parse()?, tweak.parse()?)) 134 | } 135 | (_, _, _) => return Err(ParseError::InvalidTweakFormat(fragment.to_owned())), 136 | } 137 | } else { 138 | return Err(ParseError::UnrecognizedFragment(fragment.to_owned())); 139 | } 140 | } 141 | 142 | Ok(d) 143 | } 144 | } 145 | 146 | #[cfg(test)] 147 | mod test { 148 | use super::*; 149 | 150 | #[test] 151 | fn display_from_str() { 152 | let input = InputDescriptor { 153 | outpoint: "9a035b0e6e9d07065a31c49884cb1c2d8953636346e91948df75b20e27f50f24:8" 154 | .parse() 155 | .unwrap(), 156 | terminal: "/1/167".parse().unwrap(), 157 | seq_no: "rbf(1)".parse().unwrap(), 158 | tweak: None, 159 | sighash_type: SighashType::AllPlusAnyoneCanPay, 160 | }; 161 | 162 | assert_eq!( 163 | input.to_string(), 164 | "9a035b0e6e9d07065a31c49884cb1c2d8953636346e91948df75b20e27f50f24:8 /1/167 rbf(1) \ 165 | SIGHASH_ALL|SIGHASH_ANYONECANPAY" 166 | ); 167 | assert_eq!( 168 | input, 169 | "9a035b0e6e9d07065a31c49884cb1c2d8953636346e91948df75b20e27f50f24:8 /1/167 rbf(1) \ 170 | SIGHASH_ALL|SIGHASH_ANYONECANPAY" 171 | .parse() 172 | .unwrap() 173 | ); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /descriptors/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Wallet-level libraries for bitcoin protocol by LNP/BP Association 2 | // 3 | // Written in 2020-2022 by 4 | // Dr. Maxim Orlovsky 5 | // 6 | // This software is distributed without any warranty. 7 | // 8 | // You should have received a copy of the Apache-2.0 License 9 | // along with this software. 10 | // If not, see . 11 | 12 | // Coding conventions 13 | #![recursion_limit = "256"] 14 | #![deny(dead_code, /* missing_docs, */ warnings)] 15 | 16 | //! General workflow for working with ScriptPubkey* types: 17 | //! ```text 18 | //! Template -> Descriptor -> Structure -> PubkeyScript -> TxOut 19 | //! 20 | //! TxOut -> PubkeyScript -> Descriptor -> Structure -> Format 21 | //! ``` 22 | 23 | #[macro_use] 24 | extern crate amplify; 25 | #[macro_use] 26 | extern crate strict_encoding; 27 | #[cfg(feature = "miniscript")] 28 | extern crate miniscript_crate as miniscript; 29 | #[cfg(feature = "serde")] 30 | #[macro_use] 31 | extern crate serde_crate as serde; 32 | 33 | mod deduction; 34 | pub mod derive; 35 | mod descriptor; 36 | mod input; 37 | #[cfg(feature = "miniscript")] 38 | mod templates; 39 | 40 | pub use deduction::DeductionError; 41 | pub use descriptor::{ 42 | BareDescriptor, CompositeDescrType, DescrVariants, DescriptorClass, Error, InnerDescrType, 43 | OuterDescrType, ParseError, ScriptPubkeyDescr, SpkClass, UnsupportedScriptPubkey, 44 | }; 45 | pub use input::InputDescriptor; 46 | #[cfg(feature = "miniscript")] 47 | pub use templates::ScriptTemplate; 48 | -------------------------------------------------------------------------------- /descriptors/src/templates.rs: -------------------------------------------------------------------------------- 1 | // Wallet-level libraries for bitcoin protocol by LNP/BP Association 2 | // 3 | // Written in 2020-2022 by 4 | // Dr. Maxim Orlovsky 5 | // 6 | // This software is distributed without any warranty. 7 | // 8 | // You should have received a copy of the Apache-2.0 License 9 | // along with this software. 10 | // If not, see . 11 | 12 | use std::fmt::{self, Display, Formatter}; 13 | use std::str::FromStr; 14 | 15 | use amplify::Wrapper; 16 | use bitcoin::blockdata::opcodes; 17 | use bitcoin::blockdata::script::Builder; 18 | use bitcoin::secp256k1::{Secp256k1, Verification}; 19 | use bitcoin::Script; 20 | use bitcoin_hd::account::DerivePublicKey; 21 | use bitcoin_hd::{DerivePatternError, UnhardenedIndex}; 22 | use miniscript::MiniscriptKey; 23 | #[cfg(feature = "serde")] 24 | use serde_with::{hex::Hex, As, DisplayFromStr}; 25 | use strict_encoding::{StrictDecode, StrictEncode}; 26 | 27 | /// Allows creating templates for native bitcoin scripts with embedded 28 | /// key generator templates. May be useful for creating descriptors in 29 | /// situations where target script can't be deterministically represented by 30 | /// miniscript, for instance for Lightning network-specific transaction outputs 31 | #[cfg_attr( 32 | feature = "serde", 33 | derive(Serialize, Deserialize), 34 | serde(crate = "serde_crate", rename = "lowercase") 35 | )] 36 | #[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Debug, Hash, Display)] 37 | #[derive(StrictEncode, StrictDecode)] 38 | pub enum OpcodeTemplate 39 | where 40 | Pk: MiniscriptKey + StrictEncode + StrictDecode + FromStr, 41 | ::Err: Display, 42 | { 43 | /// Normal script command (OP_CODE) 44 | #[display("opcode({0})")] 45 | OpCode(u8), 46 | 47 | /// Binary data (follows push commands) 48 | #[display("data({0:#x?})")] 49 | Data(#[cfg_attr(feature = "serde", serde(with = "As::"))] Box<[u8]>), 50 | 51 | /// Key template 52 | #[display("key({0})")] 53 | Key(#[cfg_attr(feature = "serde", serde(with = "As::"))] Pk), 54 | } 55 | 56 | impl OpcodeTemplate 57 | where 58 | Pk: MiniscriptKey + DerivePublicKey + StrictEncode + StrictDecode + FromStr, 59 | ::Err: Display, 60 | { 61 | fn translate_pk( 62 | &self, 63 | ctx: &Secp256k1, 64 | pat: impl IntoIterator>, 65 | ) -> Result, DerivePatternError> { 66 | Ok(match self { 67 | OpcodeTemplate::OpCode(code) => OpcodeTemplate::OpCode(*code), 68 | OpcodeTemplate::Data(data) => OpcodeTemplate::Data(data.clone()), 69 | OpcodeTemplate::Key(key) => { 70 | OpcodeTemplate::Key(bitcoin::PublicKey::new(key.derive_public_key(ctx, pat)?)) 71 | } 72 | }) 73 | } 74 | } 75 | 76 | /// Allows creating templates for native bitcoin scripts with embedded 77 | /// key generator templates. May be useful for creating descriptors in 78 | /// situations where target script can't be deterministically represented by 79 | /// miniscript, for instance for Lightning network-specific transaction outputs 80 | #[cfg_attr( 81 | feature = "serde", 82 | derive(Serialize, Deserialize), 83 | serde(crate = "serde_crate", transparent) 84 | )] 85 | #[derive(Wrapper, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, From)] 86 | #[derive(StrictEncode, StrictDecode)] 87 | #[wrap(Index, IndexMut, IndexFull, IndexFrom, IndexTo, IndexInclusive)] 88 | pub struct ScriptTemplate(Vec>) 89 | where 90 | Pk: MiniscriptKey + StrictEncode + StrictDecode + FromStr, 91 | ::Err: Display; 92 | 93 | impl Display for ScriptTemplate 94 | where 95 | Pk: MiniscriptKey + StrictEncode + StrictDecode + FromStr, 96 | ::Err: Display, 97 | { 98 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 99 | for instruction in &self.0 { 100 | Display::fmt(instruction, f)?; 101 | } 102 | Ok(()) 103 | } 104 | } 105 | 106 | impl ScriptTemplate 107 | where 108 | Pk: MiniscriptKey + DerivePublicKey + StrictEncode + StrictDecode + FromStr, 109 | ::Err: Display, 110 | { 111 | pub fn translate_pk( 112 | &self, 113 | ctx: &Secp256k1, 114 | pat: impl AsRef<[UnhardenedIndex]>, 115 | ) -> Result, DerivePatternError> { 116 | let pat = pat.as_ref(); 117 | Ok(self 118 | .0 119 | .iter() 120 | .map(|op| op.translate_pk(ctx, pat)) 121 | .collect::, _>>()? 122 | .into()) 123 | } 124 | } 125 | 126 | impl From> for Script { 127 | fn from(template: ScriptTemplate) -> Self { 128 | let mut builder = Builder::new(); 129 | for op in template.into_inner() { 130 | builder = match op { 131 | OpcodeTemplate::OpCode(code) => builder.push_opcode(opcodes::All::from(code)), 132 | OpcodeTemplate::Data(data) => builder.push_slice(&data), 133 | OpcodeTemplate::Key(key) => builder.push_key(&key), 134 | }; 135 | } 136 | builder.into_script() 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /doc/assets/comparison.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BP-WG/descriptor-wallet/cef0bb63c4aa90e843c0601fac0ac5f87b40ec54/doc/assets/comparison.png -------------------------------------------------------------------------------- /hd/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bitcoin_hd" 3 | version = { workspace = true } 4 | license = { workspace = true } 5 | authors = { workspace = true } 6 | description = "Bitcoin hierarchical deterministic derivation library" 7 | repository = { workspace = true } 8 | homepage = { workspace = true } 9 | keywords = ["bitcoin", "wallet", "cryptocurrency", "cryptography", "bip32"] 10 | categories = { workspace = true } 11 | readme = { workspace = true } 12 | edition = { workspace = true } 13 | rust-version = { workspace = true } 14 | exclude = [] 15 | 16 | [dependencies] 17 | amplify = { workspace = true } 18 | strict_encoding = { workspace = true } 19 | bitcoin = { workspace = true } 20 | secp256k1 = { workspace = true } 21 | miniscript_crate = { workspace = true, optional = true } 22 | slip132 = { workspace = true } 23 | clap = { workspace = true, optional = true } 24 | serde_crate = { package = "serde", version = "1", features = ["derive"], optional = true } 25 | 26 | [features] 27 | default = [] 28 | all = ["serde", "miniscript", "clap"] 29 | serde = ["serde_crate", "bitcoin/serde"] 30 | miniscript = ["miniscript_crate"] 31 | -------------------------------------------------------------------------------- /hd/src/derive.rs: -------------------------------------------------------------------------------- 1 | // Wallet-level libraries for bitcoin protocol by LNP/BP Association 2 | // 3 | // Written in 2020-2022 by 4 | // Dr. Maxim Orlovsky 5 | // 6 | // This software is distributed without any warranty. 7 | // 8 | // You should have received a copy of the Apache-2.0 License 9 | // along with this software. 10 | // If not, see . 11 | 12 | /// the provided derive pattern does not match descriptor derivation 13 | /// wildcard 14 | #[derive( 15 | Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, Display, Error 16 | )] 17 | #[display(doc_comments)] 18 | pub struct DerivePatternError; 19 | 20 | /// Errors during descriptor derivation 21 | #[derive(Debug, Display, From)] 22 | #[display(doc_comments)] 23 | pub enum DeriveError { 24 | /// account-level extended public in the descriptor has different network 25 | /// requirements 26 | InconsistentKeyNetwork, 27 | 28 | /// key derivation in the descriptor uses inconsistent wildcard pattern 29 | InconsistentKeyDerivePattern, 30 | 31 | /// the provided derive pattern does not match descriptor derivation 32 | /// wildcard 33 | #[from(DerivePatternError)] 34 | DerivePatternMismatch, 35 | 36 | /// descriptor contains no keys; corresponding outputs will be 37 | /// "anyone-can-sped" 38 | NoKeys, 39 | 40 | /// descriptor does not support address generation 41 | NoAddressForDescriptor, 42 | 43 | /// unable to derive script public key for the descriptor; possible 44 | /// incorrect miniscript for the descriptor context 45 | DescriptorFailure, 46 | } 47 | 48 | impl std::error::Error for DeriveError { 49 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 50 | match self { 51 | DeriveError::InconsistentKeyNetwork => None, 52 | DeriveError::InconsistentKeyDerivePattern => None, 53 | DeriveError::DerivePatternMismatch => None, 54 | DeriveError::NoKeys => None, 55 | DeriveError::NoAddressForDescriptor => None, 56 | DeriveError::DescriptorFailure => None, 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /hd/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Wallet-level libraries for bitcoin protocol by LNP/BP Association 2 | // 3 | // Written in 2020-2022 by 4 | // Dr. Maxim Orlovsky 5 | // 6 | // This software is distributed without any warranty. 7 | // 8 | // You should have received a copy of the Apache-2.0 License 9 | // along with this software. 10 | // If not, see . 11 | 12 | //! Library with extended support of hierarchival deterministic wallet 13 | //! functions. 14 | //! 15 | //! Includes advanced derivation paths functionality and operations. 16 | 17 | // Coding conventions 18 | #![recursion_limit = "256"] 19 | #![deny(dead_code, missing_docs, warnings)] 20 | 21 | #[macro_use] 22 | extern crate amplify; 23 | #[macro_use] 24 | extern crate strict_encoding; 25 | 26 | #[cfg(feature = "serde")] 27 | #[macro_use] 28 | extern crate serde_crate as serde; 29 | #[cfg(feature = "miniscript")] 30 | extern crate miniscript_crate as miniscript; 31 | 32 | pub mod account; 33 | mod derive; 34 | mod indexes; 35 | mod path; 36 | mod ranges; 37 | pub mod standards; 38 | mod traits; 39 | mod unsatisfiable; 40 | mod xkey; 41 | mod xpubref; 42 | 43 | pub use account::DerivationAccount; 44 | pub use derive::{DeriveError, DerivePatternError}; 45 | pub use indexes::{ 46 | AccountStep, HardenedIndex, HardenedIndexExpected, SegmentIndexes, TerminalStep, 47 | UnhardenedIndex, UnhardenedIndexExpected, 48 | }; 49 | pub use path::DerivationSubpath; 50 | pub use ranges::{IndexRange, IndexRangeList}; 51 | pub use standards::{Bip43, DerivationStandard, DescriptorType}; 52 | pub use traits::{DerivationPathMaster, HardenedNormalSplit}; 53 | pub use unsatisfiable::UnsatisfiableKey; 54 | pub use xkey::{ 55 | NonStandardDerivation, XpubDescriptor, XpubOrigin, XpubParseError, XpubRequirementError, 56 | XpubkeyCore, 57 | }; 58 | pub use xpubref::XpubRef; 59 | 60 | /// Constant determining BIP32 boundary for u32 values after which index 61 | /// is treated as hardened 62 | pub const HARDENED_INDEX_BOUNDARY: u32 = 1 << 31; 63 | 64 | // TODO: Replace bip32::Error with more efficient local error type 65 | -------------------------------------------------------------------------------- /hd/src/path.rs: -------------------------------------------------------------------------------- 1 | // Wallet-level libraries for bitcoin protocol by LNP/BP Association 2 | // 3 | // Written in 2020-2022 by 4 | // Dr. Maxim Orlovsky 5 | // 6 | // This software is distributed without any warranty. 7 | // 8 | // You should have received a copy of the Apache-2.0 License 9 | // along with this software. 10 | // If not, see . 11 | 12 | use core::fmt::{self, Display, Formatter}; 13 | use core::str::FromStr; 14 | use std::borrow::{Borrow, BorrowMut}; 15 | use std::io; 16 | use std::ops::{Deref, DerefMut}; 17 | 18 | use bitcoin::util::bip32; 19 | use strict_encoding::{StrictDecode, StrictEncode}; 20 | 21 | use crate::SegmentIndexes; 22 | 23 | /// Derivation path that consisting only of single type of segments. 24 | /// 25 | /// Useful in specifying concrete derivation from a provided extended public key 26 | /// without extended private key accessible. 27 | /// 28 | /// Type guarantees that the number of derivation path segments is non-zero. 29 | #[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, From)] 30 | #[cfg_attr( 31 | feature = "serde", 32 | derive(Serialize, Deserialize), 33 | serde(crate = "serde_crate") 34 | )] 35 | pub struct DerivationSubpath(Vec) 36 | where 37 | Segment: SegmentIndexes; 38 | 39 | // This is needed to get methods line `len()` and `is_empty()` working. 40 | impl Deref for DerivationSubpath 41 | where 42 | Segment: SegmentIndexes, 43 | { 44 | type Target = Vec; 45 | 46 | fn deref(&self) -> &Self::Target { &self.0 } 47 | } 48 | 49 | impl DerefMut for DerivationSubpath 50 | where 51 | Segment: SegmentIndexes, 52 | { 53 | fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } 54 | } 55 | 56 | impl Default for DerivationSubpath 57 | where 58 | Segment: SegmentIndexes, 59 | { 60 | fn default() -> Self { Self(vec![]) } 61 | } 62 | 63 | impl From<&[Segment]> for DerivationSubpath 64 | where 65 | Segment: SegmentIndexes, 66 | { 67 | fn from(path: &[Segment]) -> Self { Self(path.to_vec()) } 68 | } 69 | 70 | impl AsRef<[Segment]> for DerivationSubpath 71 | where 72 | Segment: SegmentIndexes, 73 | { 74 | #[inline] 75 | fn as_ref(&self) -> &[Segment] { &self.0 } 76 | } 77 | 78 | impl AsMut> for DerivationSubpath 79 | where 80 | Segment: SegmentIndexes, 81 | { 82 | #[inline] 83 | fn as_mut(&mut self) -> &mut Vec { &mut self.0 } 84 | } 85 | 86 | impl Borrow<[Segment]> for DerivationSubpath 87 | where 88 | Segment: SegmentIndexes, 89 | { 90 | #[inline] 91 | fn borrow(&self) -> &[Segment] { &self.0 } 92 | } 93 | 94 | impl BorrowMut<[Segment]> for DerivationSubpath 95 | where 96 | Segment: SegmentIndexes, 97 | { 98 | #[inline] 99 | fn borrow_mut(&mut self) -> &mut [Segment] { &mut self.0 } 100 | } 101 | 102 | impl StrictEncode for DerivationSubpath 103 | where 104 | Segment: SegmentIndexes + StrictEncode, 105 | { 106 | #[inline] 107 | fn strict_encode(&self, e: E) -> Result { 108 | self.0.strict_encode(e) 109 | } 110 | } 111 | 112 | impl StrictDecode for DerivationSubpath 113 | where 114 | Segment: SegmentIndexes + StrictDecode, 115 | { 116 | #[inline] 117 | fn strict_decode(d: D) -> Result { 118 | Ok(Self(Vec::strict_decode(d)?)) 119 | } 120 | } 121 | 122 | impl Display for DerivationSubpath 123 | where 124 | Segment: SegmentIndexes + Display, 125 | { 126 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 127 | for segment in &self.0 { 128 | f.write_str("/")?; 129 | Display::fmt(segment, f)?; 130 | } 131 | Ok(()) 132 | } 133 | } 134 | 135 | impl FromStr for DerivationSubpath 136 | where 137 | Segment: SegmentIndexes + FromStr, 138 | bip32::Error: From<::Err>, 139 | { 140 | type Err = bip32::Error; 141 | 142 | fn from_str(s: &str) -> Result { 143 | if !s.starts_with('/') { 144 | return Err(bip32::Error::InvalidDerivationPathFormat); 145 | } 146 | let inner = s[1..] 147 | .split('/') 148 | .map(Segment::from_str) 149 | .collect::, Segment::Err>>()?; 150 | if inner.is_empty() { 151 | return Err(bip32::Error::InvalidDerivationPathFormat); 152 | } 153 | Ok(Self(inner)) 154 | } 155 | } 156 | 157 | impl IntoIterator for DerivationSubpath 158 | where 159 | Segment: SegmentIndexes, 160 | { 161 | type Item = Segment; 162 | type IntoIter = std::vec::IntoIter; 163 | 164 | fn into_iter(self) -> Self::IntoIter { self.0.into_iter() } 165 | } 166 | 167 | impl<'path, Segment> IntoIterator for &'path DerivationSubpath 168 | where 169 | Segment: SegmentIndexes + Copy, 170 | { 171 | type Item = Segment; 172 | type IntoIter = std::iter::Copied>; 173 | 174 | fn into_iter(self) -> Self::IntoIter { self.0.iter().copied() } 175 | } 176 | 177 | impl FromIterator for DerivationSubpath 178 | where 179 | Segment: SegmentIndexes, 180 | { 181 | fn from_iter>(iter: T) -> Self { 182 | Self(iter.into_iter().collect()) 183 | } 184 | } 185 | 186 | impl DerivationSubpath 187 | where 188 | Segment: SegmentIndexes, 189 | { 190 | /// Constructs empty derivation path. 191 | pub fn new() -> Self { Self::default() } 192 | } 193 | -------------------------------------------------------------------------------- /hd/src/ranges.rs: -------------------------------------------------------------------------------- 1 | // Wallet-level libraries for bitcoin protocol by LNP/BP Association 2 | // 3 | // Written in 2020-2022 by 4 | // Dr. Maxim Orlovsky 5 | // 6 | // This software is distributed without any warranty. 7 | // 8 | // You should have received a copy of the Apache-2.0 License 9 | // along with this software. 10 | // If not, see . 11 | 12 | use std::cmp::Ordering; 13 | use std::collections::BTreeSet; 14 | use std::fmt::{self, Display, Formatter}; 15 | use std::io; 16 | use std::ops::RangeInclusive; 17 | use std::str::FromStr; 18 | 19 | use amplify::Wrapper; 20 | use bitcoin::util::bip32; 21 | use strict_encoding::{StrictDecode, StrictEncode}; 22 | 23 | use crate::SegmentIndexes; 24 | 25 | // TODO: Implement iterator methods 26 | 27 | /// Multiple index ranges (in form `a..b, c..d`) as it can be present in the 28 | /// derivation path segment according to BOP-88 and LNPBP-32. The range is 29 | /// always inclusive. 30 | /// 31 | /// The type is guaranteed to have at least one index in the range and at least 32 | /// one range element. It also guarantees that all individual ranges are 33 | /// disjoint. 34 | // TODO: Remove serde impl and use FromStrDisplay on top instead 35 | #[cfg_attr( 36 | feature = "serde", 37 | derive(Serialize, Deserialize), 38 | serde(crate = "serde_crate", transparent) 39 | )] 40 | #[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, From)] 41 | pub struct IndexRangeList(#[from] BTreeSet>) 42 | where 43 | Index: SegmentIndexes; 44 | 45 | impl IndexRangeList 46 | where 47 | Index: SegmentIndexes, 48 | { 49 | /// Constructs derivation range list from a iterator over index ranges. 50 | /// Errors if any of the ranges are not disjoint. 51 | pub fn with(iter: impl IntoIterator>) -> Result { 52 | let mut list = IndexRangeList(bset![]); 53 | for elem in iter.into_iter() { 54 | list.insert(elem)?; 55 | } 56 | if list.0.is_empty() { 57 | return Err(bip32::Error::InvalidDerivationPathFormat); 58 | } 59 | Ok(list) 60 | } 61 | 62 | /// Adds new index range to the list of index ranges present at some 63 | /// derivation path segment. 64 | /// 65 | /// Checks if the added range at least partially intersects with other 66 | /// existing ranges and errors in this case. 67 | pub fn insert(&mut self, range: IndexRange) -> Result<(), bip32::Error> { 68 | for elem in &self.0 { 69 | if elem.does_intersect(&range) { 70 | return Err(bip32::Error::InvalidDerivationPathFormat); 71 | } 72 | } 73 | self.0.insert(range); 74 | Ok(()) 75 | } 76 | 77 | /// Remove index range from the list; returning `true` if the range was 78 | /// present in the list. Removes only full ranges and not 79 | /// partially-intersected range. 80 | #[inline] 81 | pub fn remove(&mut self, range: &IndexRange) -> bool { self.0.remove(range) } 82 | 83 | /// Counts number of disjoint ranges withing the list 84 | #[inline] 85 | pub fn range_count(&self) -> usize { self.0.len() } 86 | 87 | /// Returns the first range from the list of ranges. 88 | #[inline] 89 | pub fn first_range(&self) -> &IndexRange { 90 | self.0 91 | .iter() 92 | .next() 93 | .expect("IndexRangeList guarantees are broken") 94 | } 95 | 96 | /// Returns the last range from the list of ranges. If the list contain only 97 | /// one range the function will return the same value as 98 | /// [`IndexRangeList::first_range`] 99 | #[inline] 100 | pub fn last_range(&self) -> &IndexRange { 101 | self.0 102 | .iter() 103 | .last() 104 | .expect("IndexRangeList guarantees are broken") 105 | } 106 | } 107 | 108 | impl SegmentIndexes for IndexRangeList 109 | where 110 | Index: SegmentIndexes, 111 | { 112 | #[inline] 113 | fn zero() -> Self { Self(bset![IndexRange::zero()]) } 114 | 115 | #[inline] 116 | fn one() -> Self { Self(bset![IndexRange::one()]) } 117 | 118 | #[inline] 119 | fn largest() -> Self { Self(bset![IndexRange::largest()]) } 120 | 121 | #[inline] 122 | fn count(&self) -> usize { self.0.iter().map(IndexRange::count).sum() } 123 | 124 | #[inline] 125 | fn contains(&self, index: u32) -> bool { self.0.iter().any(|i| i.contains(index)) } 126 | 127 | #[inline] 128 | fn from_index(index: impl Into) -> Result { 129 | Ok(Self(bset![IndexRange::from_index(index)?])) 130 | } 131 | 132 | #[inline] 133 | fn first_index(&self) -> u32 { self.first_range().first_index() } 134 | 135 | #[inline] 136 | fn last_index(&self) -> u32 { self.last_range().last_index() } 137 | 138 | #[inline] 139 | fn from_derivation_value(value: u32) -> Result { 140 | Ok(Self(bset![IndexRange::from_derivation_value(value)?])) 141 | } 142 | 143 | #[inline] 144 | fn first_derivation_value(&self) -> u32 { self.first_range().first_derivation_value() } 145 | 146 | #[inline] 147 | fn last_derivation_value(&self) -> u32 { self.last_range().last_derivation_value() } 148 | 149 | #[inline] 150 | fn checked_add_assign(&mut self, _: impl Into) -> Option { None } 151 | 152 | #[inline] 153 | fn checked_sub_assign(&mut self, _: impl Into) -> Option { None } 154 | 155 | #[inline] 156 | fn is_hardened(&self) -> bool { self.first_range().is_hardened() } 157 | } 158 | 159 | impl StrictEncode for IndexRangeList 160 | where 161 | Index: SegmentIndexes + StrictEncode, 162 | BTreeSet>: StrictEncode, 163 | { 164 | #[inline] 165 | fn strict_encode(&self, e: E) -> Result { 166 | self.0.strict_encode(e) 167 | } 168 | } 169 | 170 | impl StrictDecode for IndexRangeList 171 | where 172 | Index: SegmentIndexes + StrictDecode, 173 | BTreeSet>: StrictDecode, 174 | { 175 | fn strict_decode(d: D) -> Result { 176 | let set = BTreeSet::>::strict_decode(d)?; 177 | if set.is_empty() { 178 | return Err(strict_encoding::Error::DataIntegrityError(s!( 179 | "IndexRangeList when deserialized must has at least one element" 180 | ))); 181 | } 182 | Self::with(set).map_err(|_| { 183 | strict_encoding::Error::DataIntegrityError(s!( 184 | "IndexRangeList elements must be disjoint ranges" 185 | )) 186 | }) 187 | } 188 | } 189 | 190 | impl From> for IndexRangeList 191 | where 192 | Index: SegmentIndexes, 193 | { 194 | fn from(range: IndexRange) -> Self { Self(bset![range]) } 195 | } 196 | 197 | impl Display for IndexRangeList 198 | where 199 | Index: SegmentIndexes + Display, 200 | { 201 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 202 | if !f.alternate() { 203 | // Use Sparrow formatting 204 | f.write_str("<")?; 205 | for (index, range) in self.0.iter().enumerate() { 206 | Display::fmt(range, f)?; 207 | if index < self.0.len() - 1 { 208 | f.write_str(";")?; 209 | } 210 | } 211 | f.write_str(">") 212 | } else { 213 | // Use BIP-88 formatting 214 | let mut s = String::new(); 215 | for (index, range) in self.0.iter().enumerate() { 216 | s.extend(format!("{}", range).chars()); 217 | if index < self.0.len() - 1 { 218 | s.push(','); 219 | } 220 | } 221 | let sp = s.replace(&['\'', 'h'][..], ""); 222 | write!(f, "{{{}}}{}", sp, if sp != s { "h" } else { "" }) 223 | } 224 | } 225 | } 226 | 227 | impl FromStr for IndexRangeList 228 | where 229 | Index: SegmentIndexes + FromStr, 230 | bip32::Error: From<::Err>, 231 | { 232 | type Err = bip32::Error; 233 | 234 | fn from_str(s: &str) -> Result { 235 | let mut list = Self(bset![]); 236 | let s = if s.ends_with(&['h', '\''][..]) { 237 | let mut s = s 238 | .trim_end_matches(&['h', '\''][..]) 239 | .replace(',', "h,") 240 | .replace(';', "h;") 241 | .replace('-', "h-"); 242 | s.push('h'); 243 | s 244 | } else { 245 | s.to_owned() 246 | }; 247 | let s = s 248 | .trim_start_matches(&['<', '{'][..]) 249 | .trim_end_matches(&['>', '}'][..]); 250 | for item in s.split(&[',', ';'][..]) { 251 | list.insert(IndexRange::from_str(item)?)?; 252 | } 253 | Ok(list) 254 | } 255 | } 256 | 257 | /// Range of derivation indexes (in form `n..m`) as it can be present in the 258 | /// derivation path terminal segment according to BIP-88 and LNPBP-32. The range 259 | /// is always inclusive. 260 | /// 261 | /// The type is guaranteed to have at least one index in the range. 262 | // TODO: Remove serde impl and use FromStrDisplay on top instead 263 | #[cfg_attr( 264 | feature = "serde", 265 | derive(Serialize, Deserialize), 266 | serde(crate = "serde_crate", transparent) 267 | )] 268 | #[derive(Wrapper, Clone, PartialEq, Eq, Hash, Debug, From)] 269 | pub struct IndexRange(RangeInclusive) 270 | where 271 | Index: SegmentIndexes; 272 | 273 | impl PartialOrd for IndexRange 274 | where 275 | Index: SegmentIndexes, 276 | { 277 | fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } 278 | } 279 | 280 | impl Ord for IndexRange 281 | where 282 | Index: SegmentIndexes, 283 | { 284 | fn cmp(&self, other: &Self) -> Ordering { 285 | match self.first_index().cmp(&other.first_index()) { 286 | Ordering::Equal => self.last_index().cmp(&other.last_index()), 287 | other => other, 288 | } 289 | } 290 | } 291 | 292 | impl IndexRange 293 | where 294 | Index: SegmentIndexes, 295 | { 296 | /// Constructs index range from a single index. 297 | pub fn new(index: impl Into) -> Self { 298 | let index = index.into(); 299 | Self(RangeInclusive::new(index.clone(), index)) 300 | } 301 | 302 | /// Constructs index range from two indexes. If `end` < `start` the order 303 | /// of indexes is reversed 304 | pub fn with(start: impl Into, end: impl Into) -> Self { 305 | let start = start.into(); 306 | let end = end.into(); 307 | if end < start { 308 | Self(RangeInclusive::new(end, start)) 309 | } else { 310 | Self(RangeInclusive::new(start, end)) 311 | } 312 | } 313 | 314 | /// Detects whether two index ranges share common indexes (i.e. intersect) 315 | #[inline] 316 | pub fn does_intersect(&self, other: &IndexRange) -> bool { 317 | self.first_index() <= other.last_index() && other.first_index() <= self.last_index() 318 | } 319 | } 320 | 321 | impl SegmentIndexes for IndexRange 322 | where 323 | Index: SegmentIndexes, 324 | { 325 | #[inline] 326 | fn zero() -> Self { IndexRange(Index::zero()..=Index::zero()) } 327 | 328 | #[inline] 329 | fn one() -> Self { IndexRange(Index::one()..=Index::one()) } 330 | 331 | #[inline] 332 | fn largest() -> Self { IndexRange(Index::largest()..=Index::largest()) } 333 | 334 | #[inline] 335 | fn count(&self) -> usize { 336 | self.0.end().last_index() as usize - self.0.start().first_index() as usize + 1 337 | } 338 | 339 | #[inline] 340 | fn contains(&self, index: u32) -> bool { 341 | self.0.start().first_index() <= index && self.0.end().last_index() >= index 342 | } 343 | 344 | #[inline] 345 | fn from_index(index: impl Into) -> Result { 346 | let index = index.into(); 347 | Ok(IndexRange( 348 | Index::from_index(index)?..=Index::from_index(index)?, 349 | )) 350 | } 351 | 352 | #[inline] 353 | fn first_index(&self) -> u32 { self.0.start().first_index() } 354 | 355 | #[inline] 356 | fn last_index(&self) -> u32 { self.0.end().last_index() } 357 | 358 | #[inline] 359 | fn from_derivation_value(value: u32) -> Result { 360 | Ok(IndexRange( 361 | Index::from_derivation_value(value)?..=Index::from_derivation_value(value)?, 362 | )) 363 | } 364 | 365 | #[inline] 366 | fn first_derivation_value(&self) -> u32 { self.0.start().first_derivation_value() } 367 | 368 | #[inline] 369 | fn last_derivation_value(&self) -> u32 { self.0.end().last_derivation_value() } 370 | 371 | #[inline] 372 | fn checked_add_assign(&mut self, _: impl Into) -> Option { None } 373 | 374 | #[inline] 375 | fn checked_sub_assign(&mut self, _: impl Into) -> Option { None } 376 | 377 | #[inline] 378 | fn is_hardened(&self) -> bool { self.0.start().is_hardened() } 379 | } 380 | 381 | impl Display for IndexRange 382 | where 383 | Index: SegmentIndexes + Display, 384 | { 385 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 386 | let inner = self.as_inner(); 387 | if inner.start() == inner.end() { 388 | Display::fmt(inner.start(), f) 389 | } else { 390 | Display::fmt(inner.start(), f)?; 391 | if f.alternate() { 392 | f.write_str("-")?; 393 | Display::fmt(inner.end(), f) 394 | } else { 395 | for index in (self.start().first_index()..=self.end().first_index()).skip(1) { 396 | write!(f, ";{index}")?; 397 | } 398 | Ok(()) 399 | } 400 | } 401 | } 402 | } 403 | 404 | impl FromStr for IndexRange 405 | where 406 | Index: SegmentIndexes + FromStr, 407 | bip32::Error: From<::Err>, 408 | { 409 | type Err = bip32::Error; 410 | 411 | fn from_str(s: &str) -> Result { 412 | let mut split = s.split('-'); 413 | Ok(match (split.next(), split.next()) { 414 | (Some(start), Some(end)) => { 415 | IndexRange::with(Index::from_str(start)?, Index::from_str(end)?) 416 | } 417 | (Some(start), None) => IndexRange::new(Index::from_str(start)?), 418 | _ => unreachable!(), 419 | }) 420 | } 421 | } 422 | 423 | impl StrictEncode for IndexRange 424 | where 425 | Index: SegmentIndexes + StrictEncode, 426 | { 427 | fn strict_encode(&self, mut e: E) -> Result { 428 | Ok(strict_encode_list!(e; self.first_index(), self.last_index())) 429 | } 430 | } 431 | 432 | impl StrictDecode for IndexRange 433 | where 434 | Index: SegmentIndexes + StrictDecode, 435 | { 436 | fn strict_decode(mut d: D) -> Result { 437 | Ok(Self::from_inner(RangeInclusive::new( 438 | Index::strict_decode(&mut d)?, 439 | Index::strict_decode(&mut d)?, 440 | ))) 441 | } 442 | } 443 | -------------------------------------------------------------------------------- /hd/src/traits.rs: -------------------------------------------------------------------------------- 1 | // Wallet-level libraries for bitcoin protocol by LNP/BP Association 2 | // 3 | // Written in 2020-2022 by 4 | // Dr. Maxim Orlovsky 5 | // 6 | // This software is distributed without any warranty. 7 | // 8 | // You should have received a copy of the Apache-2.0 License 9 | // along with this software. 10 | // If not, see . 11 | 12 | use bitcoin::util::bip32::{ChildNumber, DerivationPath}; 13 | 14 | use crate::{AccountStep, SegmentIndexes, TerminalStep}; 15 | 16 | /// Extension trait allowing to add more methods to [`DerivationPath`] type 17 | pub trait DerivationPathMaster { 18 | /// Returns derivation path for a master key (i.e. empty derivation path) 19 | fn master() -> Self; 20 | 21 | /// Returns whether derivation path represents master key (i.e. it's length 22 | /// is empty). True for `m` path. 23 | fn is_master(&self) -> bool; 24 | } 25 | 26 | impl DerivationPathMaster for DerivationPath { 27 | fn master() -> DerivationPath { vec![].into() } 28 | fn is_master(&self) -> bool { self.into_iter().len() == 0 } 29 | } 30 | 31 | /// Extension trait allowing splitting derivation paths into hardened and 32 | /// unhardened components 33 | pub trait HardenedNormalSplit { 34 | /// Splits [`DerivationPath`] into hardened and unhardened parts 35 | fn hardened_normal_split(&self) -> (Vec, Vec); 36 | } 37 | 38 | impl HardenedNormalSplit for DerivationPath { 39 | fn hardened_normal_split(&self) -> (Vec, Vec) { 40 | let mut terminal_path = vec![]; 41 | let account_path = self 42 | .into_iter() 43 | .rev() 44 | .by_ref() 45 | .skip_while(|child| { 46 | if let ChildNumber::Normal { index } = child { 47 | terminal_path.push( 48 | TerminalStep::from_index(*index) 49 | .expect("ChildNumber::Normal contains hardened index"), 50 | ); 51 | true 52 | } else { 53 | false 54 | } 55 | }) 56 | .cloned() 57 | .map(AccountStep::try_from) 58 | .collect::, _>>() 59 | .expect("ChildNumber indexes are broken"); 60 | let account_path = account_path.into_iter().rev().collect(); 61 | let terminal_path = terminal_path.into_iter().rev().collect(); 62 | (account_path, terminal_path) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /hd/src/unsatisfiable.rs: -------------------------------------------------------------------------------- 1 | // Wallet-level libraries for bitcoin protocol by LNP/BP Association 2 | // 3 | // Written in 2020-2022 by 4 | // Dr. Maxim Orlovsky 5 | // 6 | // This software is distributed without any warranty. 7 | // 8 | // You should have received a copy of the Apache-2.0 License 9 | // along with this software. 10 | // If not, see . 11 | 12 | use bitcoin::hashes::{sha256, Hash}; 13 | use bitcoin::util::bip32::ExtendedPubKey; 14 | use secp256k1::{PublicKey, SECP256K1}; 15 | 16 | use crate::{DerivationAccount, DerivationSubpath, TerminalStep, XpubRef}; 17 | 18 | /// Extension trait for types containing EC keys, which can be made provably 19 | /// unspendable 20 | pub trait UnsatisfiableKey { 21 | /// A parameter supplied to [`UnsatisfiableKey::unsatisfiable_key`], like an 22 | /// information on the use of testnet for extended keys, or derivation path 23 | /// for key templates. 24 | type Param; 25 | 26 | /// Generates provably unspendable key version 27 | fn unsatisfiable_key(_: Self::Param) -> Self; 28 | } 29 | 30 | impl UnsatisfiableKey for PublicKey { 31 | type Param = (); 32 | 33 | fn unsatisfiable_key(_: Self::Param) -> Self { 34 | let unspendable_key = PublicKey::from_secret_key(SECP256K1, &secp256k1::ONE_KEY); 35 | let hash = &sha256::Hash::hash(&unspendable_key.serialize()); 36 | let tweak = 37 | secp256k1::Scalar::from_be_bytes(hash.into_inner()).expect("negligible probability"); 38 | unspendable_key 39 | .add_exp_tweak(SECP256K1, &tweak) 40 | .expect("negligible probability") 41 | } 42 | } 43 | 44 | impl UnsatisfiableKey for ExtendedPubKey { 45 | type Param = bool; 46 | 47 | fn unsatisfiable_key(testnet: Self::Param) -> Self { 48 | let unspendable_key = PublicKey::unsatisfiable_key(()); 49 | let mut buf = Vec::with_capacity(78); 50 | buf.extend(if testnet { 51 | [0x04u8, 0x35, 0x87, 0xCF] 52 | } else { 53 | [0x04u8, 0x88, 0xB2, 0x1E] 54 | }); 55 | buf.extend([0u8; 5]); // depth + fingerprint 56 | buf.extend([0u8; 4]); // child no 57 | buf.extend(&unspendable_key.serialize()[1..]); 58 | buf.extend(unspendable_key.serialize()); 59 | ExtendedPubKey::decode(&buf).expect("broken unspendable key construction") 60 | } 61 | } 62 | 63 | impl UnsatisfiableKey for DerivationAccount { 64 | type Param = (bool, DerivationSubpath); 65 | 66 | fn unsatisfiable_key(param: Self::Param) -> Self { 67 | let (testnet, terminal_path) = param; 68 | DerivationAccount { 69 | master: XpubRef::Unknown, 70 | account_path: empty!(), 71 | account_xpub: ExtendedPubKey::unsatisfiable_key(testnet), 72 | revocation_seal: None, 73 | terminal_path, 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /hd/src/xpubref.rs: -------------------------------------------------------------------------------- 1 | // Wallet-level libraries for bitcoin protocol by LNP/BP Association 2 | // 3 | // Written in 2020-2022 by 4 | // Dr. Maxim Orlovsky 5 | // 6 | // This software is distributed without any warranty. 7 | // 8 | // You should have received a copy of the Apache-2.0 License 9 | // along with this software. 10 | // If not, see . 11 | 12 | use std::str::FromStr; 13 | 14 | use bitcoin::util::bip32::{self, ExtendedPubKey, Fingerprint}; 15 | use bitcoin::XpubIdentifier; 16 | 17 | /// A reference to the used extended public key at some level of a derivation 18 | /// path. 19 | #[derive( 20 | Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Default, Debug, Display, From 21 | )] 22 | #[derive(StrictEncode, StrictDecode)] 23 | #[cfg_attr( 24 | feature = "serde", 25 | derive(Serialize, Deserialize), 26 | serde(crate = "serde_crate", rename_all = "camelCase", untagged) 27 | )] 28 | #[display("[{0}]", alt = "[{0:#}]")] 29 | pub enum XpubRef { 30 | /// Extended public key reference is not present 31 | #[display("")] 32 | #[default] 33 | Unknown, 34 | 35 | /// Extended public key reference using its [`Fingerprint`] 36 | #[from] 37 | Fingerprint(Fingerprint), 38 | 39 | /// Extended public key reference using [`XpubIdentifier`] 40 | #[from] 41 | XpubIdentifier(XpubIdentifier), 42 | 43 | /// Extended public key reference using full [`ExtendedPubKey`] data 44 | #[from] 45 | Xpub(ExtendedPubKey), 46 | } 47 | 48 | impl XpubRef { 49 | /// Detects if the xpub reference is present 50 | pub fn is_some(&self) -> bool { self != &XpubRef::Unknown } 51 | 52 | /// Returns fingerprint of the extended public key, if the reference is 53 | /// present 54 | pub fn fingerprint(&self) -> Option { 55 | match self { 56 | XpubRef::Unknown => None, 57 | XpubRef::Fingerprint(fp) => Some(*fp), 58 | XpubRef::XpubIdentifier(xpubid) => Some(Fingerprint::from(&xpubid[0..4])), 59 | XpubRef::Xpub(xpub) => Some(xpub.fingerprint()), 60 | } 61 | } 62 | 63 | /// Returns [`XpubIdentifier`] of the extended public key, if the reference 64 | /// is present and has the form of identifier or full extended public key. 65 | pub fn identifier(&self) -> Option { 66 | match self { 67 | XpubRef::Unknown => None, 68 | XpubRef::Fingerprint(_) => None, 69 | XpubRef::XpubIdentifier(xpubid) => Some(*xpubid), 70 | XpubRef::Xpub(xpub) => Some(xpub.identifier()), 71 | } 72 | } 73 | 74 | /// Returns [`ExtendedPubKey`] of the extended public key, if the reference 75 | /// is present and has the form of full extended public key. 76 | pub fn xpubkey(&self) -> Option { 77 | match self { 78 | XpubRef::Unknown => None, 79 | XpubRef::Fingerprint(_) => None, 80 | XpubRef::XpubIdentifier(_) => None, 81 | XpubRef::Xpub(xpub) => Some(*xpub), 82 | } 83 | } 84 | } 85 | 86 | impl FromStr for XpubRef { 87 | type Err = bip32::Error; 88 | 89 | fn from_str(mut s: &str) -> Result { 90 | if s.is_empty() { 91 | return Ok(XpubRef::Unknown); 92 | } 93 | if s.starts_with("=[") { 94 | s = &s[2..s.len() - 1]; 95 | } else if s.starts_with('[') { 96 | s = &s[1..s.len() - 1] 97 | } 98 | Fingerprint::from_str(s) 99 | .map(XpubRef::from) 100 | .or_else(|_| XpubIdentifier::from_str(s).map(XpubRef::from)) 101 | .map_err(|_| bip32::Error::InvalidDerivationPathFormat) 102 | .or_else(|_| ExtendedPubKey::from_str(s).map(XpubRef::from)) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /libbitcoin/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "libbitcoin" 3 | version = "0.5.0-alpha.1" 4 | license = "Apache-2.0" 5 | authors = ["Dr Maxim Orlovsky "] 6 | description = "C library for building descriptor-based bitcoin wallets" 7 | repository = "https://github.com/rust-bitcoin/descriptor-wallet" 8 | homepage = "https://lnp-bp.org" 9 | keywords = ["bitcoin", "wallet", "cryptocurrency", "cryptography", "bip32"] 10 | categories = ["cryptography::cryptocurrencies", "encoding", "parsing"] 11 | edition = "2021" 12 | rust-version = "1.59.0" 13 | readme = "../README.md" 14 | 15 | [dependencies] 16 | libc = "0.2" 17 | lazy_static = "1.4" 18 | amplify = "3.14.2" 19 | amplify_derive = "2.11.2" 20 | bitcoin = "0.28.2" 21 | bip39 = "2.0.0" 22 | rand = "0.8.3" 23 | serde = { version = "1", features = ["derive"] } 24 | serde_with = "2.3.1" 25 | serde_json = "1" 26 | 27 | [workspace] 28 | -------------------------------------------------------------------------------- /libbitcoin/src/helpers.rs: -------------------------------------------------------------------------------- 1 | // C library for building descriptor-based bitcoin wallets 2 | // 3 | // Written in 2021 by 4 | // Dr. Maxim Orlovsky 5 | // 6 | // To the extent possible under law, the author(s) have dedicated all 7 | // copyright and related and neighboring rights to this software to 8 | // the public domain worldwide. This software is distributed without 9 | // any warranty. 10 | // 11 | // You should have received a copy of the Apache 2.0 License 12 | // along with this software. 13 | // If not, see . 14 | 15 | use std::ffi::CString; 16 | 17 | use libc::c_char; 18 | 19 | pub trait Wipe { 20 | unsafe fn wipe(self); 21 | } 22 | 23 | impl Wipe for CString { 24 | unsafe fn wipe(self) { 25 | let len = self.as_bytes().len(); 26 | let ptr = self.as_ptr() as *mut c_char; 27 | for i in 0..len as isize { 28 | *ptr.offset(i) = 0; 29 | } 30 | std::mem::drop(self); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /libbitcoin/src/lib.rs: -------------------------------------------------------------------------------- 1 | // C library for building descriptor-based bitcoin wallets 2 | // 3 | // Written in 2021 by 4 | // Dr. Maxim Orlovsky 5 | // 6 | // To the extent possible under law, the author(s) have dedicated all 7 | // copyright and related and neighboring rights to this software to 8 | // the public domain worldwide. This software is distributed without 9 | // any warranty. 10 | // 11 | // You should have received a copy of the Apache 2.0 License 12 | // along with this software. 13 | // If not, see . 14 | 15 | #![feature(try_trait_v2)] 16 | #![deny(dead_code, /* missing_docs, */ warnings)] 17 | #![allow(unused_unsafe)] 18 | #![allow(clippy::missing_safety_doc)] 19 | 20 | #[macro_use] 21 | extern crate amplify_derive; 22 | #[macro_use] 23 | extern crate lazy_static; 24 | 25 | pub mod helpers; 26 | mod signer; 27 | 28 | pub use signer::*; 29 | -------------------------------------------------------------------------------- /libbitcoin/src/signer.rs: -------------------------------------------------------------------------------- 1 | // C library for building descriptor-based bitcoin wallets 2 | // 3 | // Written in 2021 by 4 | // Dr. Maxim Orlovsky 5 | // 6 | // To the extent possible under law, the author(s) have dedicated all 7 | // copyright and related and neighboring rights to this software to 8 | // the public domain worldwide. This software is distributed without 9 | // any warranty. 10 | // 11 | // You should have received a copy of the Apache 2.0 License 12 | // along with this software. 13 | // If not, see . 14 | 15 | use std::convert::Infallible; 16 | use std::ffi::{CStr, CString}; 17 | use std::ops::{ControlFlow, FromResidual, Try}; 18 | use std::slice; 19 | use std::str::{FromStr, Utf8Error}; 20 | 21 | use bip39::Mnemonic; 22 | use bitcoin::util::bip32::{self, DerivationPath, Error, ExtendedPrivKey, ExtendedPubKey}; 23 | use bitcoin::Network; 24 | use libc::c_char; 25 | use rand::RngCore; 26 | 27 | use crate::helpers::Wipe; 28 | 29 | lazy_static! { 30 | /// Global Secp256k1 context object 31 | pub static ref SECP256K1: bitcoin::secp256k1::Secp256k1 = 32 | bitcoin::secp256k1::Secp256k1::new(); 33 | } 34 | 35 | #[derive( 36 | Clone, Copy, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, Display, Error, From 37 | )] 38 | #[allow(non_camel_case_types)] 39 | #[repr(u16)] 40 | #[display(doc_comments)] 41 | pub enum error_t { 42 | #[display("")] 43 | success = 0, 44 | 45 | /// got a null pointer as one of the function arguments 46 | null_pointer, 47 | 48 | /// result data must be a valid string which does not contain zero bytes 49 | invalid_result_data, 50 | 51 | /// invalid mnemonic string 52 | #[from(bip39::Error)] 53 | invalid_mnemonic, 54 | 55 | /// invalid UTF-8 string 56 | #[from(Utf8Error)] 57 | invalid_utf8_string, 58 | 59 | /// wrong BIP32 extended public or private key data 60 | wrong_extended_key, 61 | 62 | /// unable to derive hardened path from a public key 63 | unable_to_derive_hardened, 64 | 65 | /// invalid derivation path 66 | invalid_derivation_path, 67 | 68 | /// general BIP32-specific failure 69 | bip32_failure, 70 | 71 | /// invalid hexadecimal value 72 | hex, 73 | } 74 | 75 | impl Default for error_t { 76 | fn default() -> Self { error_t::success } 77 | } 78 | 79 | impl From for error_t { 80 | fn from(err: bip32::Error) -> Self { 81 | match err { 82 | Error::CannotDeriveFromHardenedKey => error_t::unable_to_derive_hardened, 83 | 84 | Error::InvalidChildNumber(_) 85 | | Error::InvalidChildNumberFormat 86 | | Error::InvalidDerivationPathFormat => error_t::invalid_derivation_path, 87 | 88 | Error::Base58(_) | Error::UnknownVersion(_) | Error::WrongExtendedKeyLength(_) => { 89 | error_t::wrong_extended_key 90 | } 91 | 92 | Error::Secp256k1(_) => error_t::bip32_failure, 93 | Error::Hex(_) => error_t::hex, 94 | } 95 | } 96 | } 97 | 98 | #[allow(non_camel_case_types)] 99 | #[repr(C)] 100 | pub struct string_result_t { 101 | pub code: error_t, 102 | pub details: result_details_t, 103 | } 104 | 105 | impl string_result_t { 106 | pub fn success(data: impl ToString) -> string_result_t { 107 | let (code, details) = match CString::new(data.to_string()) { 108 | Ok(s) => (error_t::success, result_details_t { data: s.into_raw() }), 109 | Err(err) => (error_t::invalid_result_data, result_details_t { 110 | data: CString::new(err.to_string()) 111 | .expect("Null byte in string_result_t success code doc comments") 112 | .into_raw(), 113 | }), 114 | }; 115 | string_result_t { code, details } 116 | } 117 | 118 | pub fn error(code: error_t) -> string_result_t { 119 | string_result_t { 120 | code, 121 | details: result_details_t { 122 | data: CString::new(code.to_string()) 123 | .expect("Null byte in error_t code doc comments") 124 | .into_raw(), 125 | }, 126 | } 127 | } 128 | 129 | pub fn is_success(&self) -> bool { self.code == error_t::success } 130 | } 131 | 132 | impl FromResidual> for string_result_t 133 | where 134 | E: std::error::Error + Into, 135 | { 136 | #[inline] 137 | fn from_residual(residual: Result) -> Self { Self::from(residual.unwrap_err()) } 138 | } 139 | 140 | impl FromResidual for string_result_t { 141 | #[inline] 142 | fn from_residual(residual: error_t) -> Self { Self::from(residual) } 143 | } 144 | 145 | impl Try for string_result_t { 146 | type Output = result_details_t; 147 | type Residual = error_t; 148 | 149 | fn from_output(output: Self::Output) -> Self { 150 | Self { 151 | code: error_t::success, 152 | details: output, 153 | } 154 | } 155 | 156 | fn branch(self) -> ControlFlow { 157 | match self.is_success() { 158 | true => ControlFlow::Continue(self.details), 159 | false => ControlFlow::Break(self.code), 160 | } 161 | } 162 | } 163 | 164 | impl From for string_result_t 165 | where 166 | E: std::error::Error + Into, 167 | { 168 | fn from(err: E) -> Self { 169 | string_result_t { 170 | details: result_details_t::from(&err), 171 | code: err.into(), 172 | } 173 | } 174 | } 175 | 176 | #[allow(non_camel_case_types)] 177 | #[repr(C)] 178 | pub union result_details_t { 179 | pub data: *const c_char, 180 | pub error: *const c_char, 181 | } 182 | 183 | impl From<&E> for result_details_t 184 | where 185 | E: std::error::Error, 186 | { 187 | fn from(err: &E) -> Self { 188 | result_details_t { 189 | error: CString::new(err.to_string()) 190 | .unwrap_or_else(|_| { 191 | CString::new("no string error representation") 192 | .expect("CString static parse failure") 193 | }) 194 | .into_raw(), 195 | } 196 | } 197 | } 198 | 199 | #[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug)] 200 | #[allow(non_camel_case_types)] 201 | #[repr(u16)] 202 | pub enum bip39_mnemonic_type { 203 | words_12, 204 | words_15, 205 | words_18, 206 | words_21, 207 | words_24, 208 | } 209 | 210 | impl bip39_mnemonic_type { 211 | pub fn byte_len(self) -> usize { 212 | match self { 213 | bip39_mnemonic_type::words_12 => 16, 214 | bip39_mnemonic_type::words_15 => 160 / 8, 215 | bip39_mnemonic_type::words_18 => 192 / 8, 216 | bip39_mnemonic_type::words_21 => 224 / 8, 217 | bip39_mnemonic_type::words_24 => 32, 218 | } 219 | } 220 | 221 | pub fn word_len(self) -> usize { (self.byte_len() * 8 + self.byte_len() * 8 / 32) / 11 } 222 | } 223 | 224 | #[no_mangle] 225 | pub unsafe extern "C" fn result_destroy(result: string_result_t) { 226 | let ptr = result.details.data; 227 | if ptr.is_null() { 228 | return; 229 | } 230 | let cs = CString::from_raw(ptr as *mut c_char); 231 | cs.wipe(); 232 | } 233 | 234 | /// Creates a rust-owned mnemonic string. You MUST always call 235 | /// [`result_destroy`] right after storing the mnemonic string and 236 | /// do not call other methods from this library on that mnemonic. If you need 237 | /// to call [`bip39_master_xpriv`] you MUST read mnemonic again and provide 238 | /// unowned string to the rust. 239 | #[no_mangle] 240 | pub unsafe extern "C" fn bip39_mnemonic_create( 241 | entropy: *const u8, 242 | mnemonic_type: bip39_mnemonic_type, 243 | ) -> string_result_t { 244 | let entropy = if entropy.is_null() { 245 | let mut inner = Vec::with_capacity(mnemonic_type.byte_len()); 246 | rand::thread_rng().fill_bytes(&mut inner); 247 | inner 248 | } else { 249 | unsafe { slice::from_raw_parts(entropy, mnemonic_type.byte_len()) }.to_vec() 250 | }; 251 | let mnemonic = bip39::Mnemonic::from_entropy(&entropy)?; 252 | string_result_t::success(mnemonic) 253 | } 254 | 255 | #[no_mangle] 256 | pub unsafe extern "C" fn bip39_master_xpriv( 257 | seed_phrase: *mut c_char, 258 | passwd: *mut c_char, 259 | wipe: bool, 260 | testnet: bool, 261 | ) -> string_result_t { 262 | if seed_phrase.is_null() { 263 | Err(error_t::null_pointer)? 264 | } 265 | 266 | let password = if passwd.is_null() { 267 | "" 268 | } else { 269 | unsafe { CStr::from_ptr(passwd).to_str()? } 270 | }; 271 | 272 | let mut seed = { 273 | let seed_phrase = unsafe { CString::from_raw(seed_phrase) }; 274 | let mnemonic = Mnemonic::from_str(seed_phrase.to_str()?)?; 275 | let seed = mnemonic.to_seed(password); 276 | if wipe { 277 | unsafe { seed_phrase.wipe() }; 278 | let s = mnemonic.to_string(); 279 | let len = s.len(); 280 | let ptr = s.as_ptr() as *mut c_char; 281 | for i in 0..len as isize { 282 | unsafe { *ptr.offset(i) = 0 }; 283 | } 284 | } 285 | seed 286 | }; 287 | let mut xpriv = ExtendedPrivKey::new_master( 288 | if testnet { 289 | Network::Testnet 290 | } else { 291 | Network::Bitcoin 292 | }, 293 | &seed, 294 | )?; 295 | seed.fill(0u8); 296 | if wipe && !passwd.is_null() { 297 | let len = password.len(); 298 | for i in 0..len as isize { 299 | unsafe { *passwd.offset(i) = 0 }; 300 | } 301 | } 302 | let xpriv_str = xpriv.to_string(); 303 | let ptr = xpriv.private_key.as_mut_ptr(); 304 | for i in 0..32 { 305 | unsafe { 306 | *ptr.offset(i) = 0; 307 | } 308 | } 309 | string_result_t::success(&xpriv_str) 310 | } 311 | 312 | #[no_mangle] 313 | pub unsafe extern "C" fn bip32_derive_xpriv( 314 | master: *mut c_char, 315 | wipe: bool, 316 | derivation: *const c_char, 317 | ) -> string_result_t { 318 | let master_cstring = unsafe { CString::from_raw(master) }; 319 | let mut master = ExtendedPrivKey::from_str(master_cstring.to_str()?)?; 320 | 321 | let derivation = unsafe { CStr::from_ptr(derivation).to_str()? }; 322 | let derivation = DerivationPath::from_str(derivation)?; 323 | 324 | let mut xpriv = master.derive_priv(&SECP256K1, &derivation)?; 325 | 326 | if wipe { 327 | unsafe { master_cstring.wipe() }; 328 | } 329 | 330 | let xpriv_str = xpriv.to_string(); 331 | let ptr1 = master.private_key.as_mut_ptr(); 332 | let ptr2 = xpriv.private_key.as_mut_ptr(); 333 | for i in 0..32 { 334 | unsafe { 335 | *ptr1.offset(i) = 0; 336 | *ptr2.offset(i) = 0; 337 | } 338 | } 339 | string_result_t::success(&xpriv_str) 340 | } 341 | 342 | #[no_mangle] 343 | pub unsafe extern "C" fn bip32_derive_xpub( 344 | master: *mut c_char, 345 | wipe: bool, 346 | derivation: *const c_char, 347 | ) -> string_result_t { 348 | let master_cstring = unsafe { CString::from_raw(master) }; 349 | 350 | let derivation = unsafe { CStr::from_ptr(derivation).to_str()? }; 351 | let derivation = DerivationPath::from_str(derivation)?; 352 | 353 | if let Ok(mut master) = ExtendedPrivKey::from_str(master_cstring.to_str()?) { 354 | let mut xpriv = master.derive_priv(&SECP256K1, &derivation)?; 355 | if wipe { 356 | unsafe { master_cstring.wipe() }; 357 | } 358 | 359 | let xpub = ExtendedPubKey::from_priv(&SECP256K1, &xpriv); 360 | 361 | let ptr1 = master.private_key.as_mut_ptr(); 362 | let ptr2 = xpriv.private_key.as_mut_ptr(); 363 | for i in 0..32 { 364 | unsafe { 365 | *ptr1.offset(i) = 0; 366 | *ptr2.offset(i) = 0; 367 | } 368 | } 369 | string_result_t::success(&xpub) 370 | } else { 371 | let master = ExtendedPubKey::from_str(master_cstring.to_str()?)?; 372 | let xpub = master.derive_pub(&SECP256K1, &derivation)?; 373 | string_result_t::success(&xpub) 374 | } 375 | } 376 | 377 | #[no_mangle] 378 | pub extern "C" fn psbt_sign( 379 | _psbt: *const c_char, 380 | _xpriv: *const c_char, 381 | _wipe: bool, 382 | ) -> string_result_t { 383 | todo!("#12 psbt_sign") 384 | } 385 | -------------------------------------------------------------------------------- /license_header: -------------------------------------------------------------------------------- 1 | // Descriptor wallet library extending bitcoin & miniscript functionality 2 | // by LNP/BP Association (https://lnp-bp.org) 3 | // Written in 2020-2022 by 4 | // Dr. Maxim Orlovsky 5 | // 6 | // To the extent possible under law, the author(s) have dedicated all 7 | // copyright and related and neighboring rights to this software to 8 | // the public domain worldwide. This software is distributed without 9 | // any warranty. 10 | // 11 | // You should have received a copy of the Apache-2.0 License 12 | // along with this software. 13 | // If not, see . 14 | 15 | -------------------------------------------------------------------------------- /onchain/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bitcoin_onchain" 3 | version = { workspace = true } 4 | license = { workspace = true } 5 | authors = { workspace = true } 6 | description = "Bitcoin descriptors library (part of descriptor-wallet)" 7 | repository = { workspace = true } 8 | homepage = { workspace = true } 9 | keywords = ["bitcoin", "wallet", "cryptocurrency", "descriptor", "bip32"] 10 | categories = { workspace = true } 11 | readme = { workspace = true } 12 | edition = { workspace = true } 13 | rust-version = { workspace = true } 14 | exclude = [] 15 | 16 | [dependencies] 17 | amplify = { workspace = true } 18 | strict_encoding = { workspace = true } 19 | bitcoin = { workspace = true } 20 | bitcoin_hd = { workspace = true } 21 | descriptors = { workspace = true, optional = true } 22 | miniscript_crate = { workspace = true, optional = true } 23 | electrum-client = { version = "0.14.0", optional = true } 24 | chrono = { workspace = true } 25 | serde_crate = { package = "serde", version = "1", features = ["derive"], optional = true } 26 | 27 | [features] 28 | default = [] 29 | all = ["miniscript_descriptors", "electrum", "serde"] 30 | miniscript = ["miniscript_crate"] 31 | miniscript_descriptors = [ 32 | "miniscript", 33 | "descriptors", 34 | "descriptors/miniscript", 35 | "bitcoin_hd/miniscript" 36 | ] 37 | electrum = ["electrum-client"] 38 | serde = ["serde_crate"] 39 | -------------------------------------------------------------------------------- /onchain/src/blockchain.rs: -------------------------------------------------------------------------------- 1 | // Wallet-level libraries for bitcoin protocol by LNP/BP Association 2 | // 3 | // Written in 2020-2022 by 4 | // Dr. Maxim Orlovsky 5 | // 6 | // This software is distributed without any warranty. 7 | // 8 | // You should have received a copy of the Apache-2.0 License 9 | // along with this software. 10 | // If not, see . 11 | 12 | //! Blockchain-specific data types useful for wallets 13 | 14 | use std::fmt::Debug; 15 | use std::hash::Hash; 16 | use std::str::FromStr; 17 | 18 | use bitcoin::blockdata::constants; 19 | use bitcoin::{BlockHash, Network, OutPoint}; 20 | use chrono::{DateTime, NaiveDateTime}; 21 | #[cfg(feature = "electrum")] 22 | use electrum_client::ListUnspentRes; 23 | use strict_encoding::{StrictDecode, StrictEncode}; 24 | 25 | /// Error parsing string representation of wallet data/structure 26 | #[derive( 27 | Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Display, From, Error 28 | )] 29 | #[display(doc_comments)] 30 | #[from(bitcoin::hashes::hex::Error)] 31 | #[from(chrono::ParseError)] 32 | #[from(std::num::ParseIntError)] 33 | #[from(bitcoin::consensus::encode::Error)] 34 | #[from(bitcoin::util::amount::ParseAmountError)] 35 | #[from(bitcoin::blockdata::transaction::ParseOutPointError)] 36 | pub struct ParseError; 37 | 38 | /// Block mining information 39 | #[derive(Getters, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, Display)] 40 | #[derive(StrictEncode, StrictDecode)] 41 | #[display("{block_height}#{block_hash}@{timestamp}")] 42 | pub struct TimeHeight { 43 | timestamp: NaiveDateTime, 44 | block_height: u32, 45 | block_hash: BlockHash, 46 | } 47 | 48 | impl Default for TimeHeight { 49 | fn default() -> Self { 50 | TimeHeight { 51 | timestamp: DateTime::from_timestamp_millis(1231006500) 52 | .expect("hardcoded value") 53 | .naive_utc(), 54 | block_height: 0, 55 | block_hash: constants::genesis_block(Network::Bitcoin).block_hash(), 56 | } 57 | } 58 | } 59 | 60 | impl FromStr for TimeHeight { 61 | type Err = ParseError; 62 | 63 | fn from_str(s: &str) -> Result { 64 | let mut data = s.split(&['#', '@'][..]); 65 | let me = Self { 66 | timestamp: data.next().ok_or(ParseError)?.parse()?, 67 | block_height: data.next().ok_or(ParseError)?.parse()?, 68 | block_hash: data.next().ok_or(ParseError)?.parse()?, 69 | }; 70 | if data.next().is_some() { 71 | Err(ParseError) 72 | } else { 73 | Ok(me) 74 | } 75 | } 76 | } 77 | 78 | /// Information about transaction mining status 79 | #[cfg_attr( 80 | feature = "serde", 81 | derive(Serialize, Deserialize), 82 | serde(crate = "serde_crate") 83 | )] 84 | #[derive( 85 | Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Default, Debug, Display 86 | )] 87 | #[derive(StrictEncode, StrictDecode)] 88 | pub enum MiningStatus { 89 | /// Transaction mining status is undefined 90 | #[default] 91 | #[display("undefined")] 92 | Undefined, 93 | 94 | /// Transaction is unknown 95 | #[display("unknown_tx")] 96 | UnknownTx, 97 | 98 | /// Transaction is not mined but present in mempool 99 | #[display("mempool")] 100 | Mempool, 101 | 102 | /// Transaction is mined onchain at a block with a given height 103 | #[display(inner)] 104 | Blockchain(u64), 105 | } 106 | 107 | /// Full UTXO information 108 | #[cfg_attr( 109 | feature = "serde", 110 | derive(Serialize, Deserialize), 111 | serde(crate = "serde_crate") 112 | )] 113 | #[derive(Getters, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, Display)] 114 | #[derive(StrictEncode, StrictDecode)] 115 | #[display("{amount}@{outpoint}")] 116 | pub struct Utxo { 117 | /// Status of the transaction containing this UTXO 118 | mined: MiningStatus, 119 | /// UTXO outpoint 120 | outpoint: OutPoint, 121 | /// Value stored in the UTXO 122 | #[cfg_attr( 123 | feature = "serde", 124 | serde(with = "bitcoin::util::amount::serde::as_btc") 125 | )] 126 | amount: bitcoin::Amount, 127 | } 128 | 129 | impl FromStr for Utxo { 130 | type Err = ParseError; 131 | 132 | fn from_str(s: &str) -> Result { 133 | let mut split = s.split('@'); 134 | match (split.next(), split.next(), split.next()) { 135 | (Some(amount), Some(outpoint), None) => Ok(Utxo { 136 | mined: MiningStatus::Undefined, 137 | amount: amount.parse()?, 138 | outpoint: outpoint.parse()?, 139 | }), 140 | _ => Err(ParseError), 141 | } 142 | } 143 | } 144 | 145 | #[cfg(feature = "electrum")] 146 | impl From for Utxo { 147 | fn from(res: ListUnspentRes) -> Self { 148 | Utxo { 149 | mined: if res.height == 0 { 150 | MiningStatus::Mempool 151 | } else { 152 | MiningStatus::Blockchain(res.height as u64) 153 | }, 154 | outpoint: OutPoint::new(res.tx_hash, res.tx_pos as u32), 155 | amount: bitcoin::Amount::from_sat(res.value), 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /onchain/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Wallet-level libraries for bitcoin protocol by LNP/BP Association 2 | // 3 | // Written in 2020-2022 by 4 | // Dr. Maxim Orlovsky 5 | // 6 | // This software is distributed without any warranty. 7 | // 8 | // You should have received a copy of the Apache-2.0 License 9 | // along with this software. 10 | // If not, see . 11 | 12 | //! Library for requesting and working with onchain bitcoin data: querying 13 | //! transaction information, mining status, tracking mempool etc. 14 | 15 | // Coding conventions 16 | #![recursion_limit = "256"] 17 | #![deny(dead_code, missing_docs, warnings)] 18 | 19 | #[macro_use] 20 | extern crate amplify; 21 | #[macro_use] 22 | extern crate strict_encoding; 23 | #[cfg(feature = "serde")] 24 | #[macro_use] 25 | extern crate serde_crate as serde; 26 | #[cfg(feature = "miniscript")] 27 | extern crate miniscript_crate as miniscript; 28 | 29 | pub mod blockchain; 30 | mod network; 31 | mod resolvers; 32 | 33 | pub use network::PublicNetwork; 34 | #[cfg(feature = "miniscript_descriptors")] 35 | pub use resolvers::ResolveDescriptor; 36 | pub use resolvers::{ResolveTx, ResolveTxFee, ResolveUtxo, TxResolverError, UtxoResolverError}; 37 | -------------------------------------------------------------------------------- /onchain/src/network.rs: -------------------------------------------------------------------------------- 1 | // Wallet-level libraries for bitcoin protocol by LNP/BP Association 2 | // 3 | // Written in 2020-2022 by 4 | // Dr. Maxim Orlovsky 5 | // 6 | // This software is distributed without any warranty. 7 | // 8 | // You should have received a copy of the Apache-2.0 License 9 | // along with this software. 10 | // If not, see . 11 | 12 | use bitcoin::Network; 13 | use bitcoin_hd::standards::DerivationBlockchain; 14 | 15 | /// Public variants of bitcoin networks 16 | #[derive( 17 | Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug, Display 18 | )] 19 | #[derive(StrictEncode, StrictDecode)] 20 | #[cfg_attr( 21 | feature = "serde", 22 | derive(Serialize, Deserialize), 23 | serde(crate = "serde_crate") 24 | )] 25 | pub enum PublicNetwork { 26 | /// Bitcoin mainnet 27 | #[display("mainnet")] 28 | Mainnet, 29 | 30 | /// Bitcoin testnet3 31 | #[default] 32 | #[display("testnet")] 33 | Testnet, 34 | 35 | /// Bitcoin signet 36 | #[display("signet")] 37 | Signet, 38 | } 39 | 40 | impl From for Network { 41 | fn from(network: PublicNetwork) -> Self { Network::from(&network) } 42 | } 43 | 44 | impl From<&PublicNetwork> for Network { 45 | fn from(network: &PublicNetwork) -> Self { 46 | match network { 47 | PublicNetwork::Mainnet => Network::Bitcoin, 48 | PublicNetwork::Testnet => Network::Testnet, 49 | PublicNetwork::Signet => Network::Signet, 50 | } 51 | } 52 | } 53 | 54 | impl TryFrom for PublicNetwork { 55 | type Error = (); 56 | fn try_from(network: Network) -> Result { 57 | Ok(match network { 58 | Network::Bitcoin => PublicNetwork::Mainnet, 59 | Network::Testnet => PublicNetwork::Testnet, 60 | Network::Signet => PublicNetwork::Signet, 61 | Network::Regtest => return Err(()), 62 | }) 63 | } 64 | } 65 | 66 | impl From for DerivationBlockchain { 67 | fn from(network: PublicNetwork) -> Self { DerivationBlockchain::from(&network) } 68 | } 69 | 70 | impl From<&PublicNetwork> for DerivationBlockchain { 71 | fn from(network: &PublicNetwork) -> Self { 72 | match network { 73 | PublicNetwork::Mainnet => DerivationBlockchain::Bitcoin, 74 | PublicNetwork::Testnet => DerivationBlockchain::Testnet, 75 | PublicNetwork::Signet => DerivationBlockchain::Testnet, 76 | } 77 | } 78 | } 79 | 80 | impl PublicNetwork { 81 | /// Detects if the public network is belongs to a testnet 82 | pub fn is_testnet(self) -> bool { 83 | matches!(self, PublicNetwork::Testnet | PublicNetwork::Signet) 84 | } 85 | 86 | /// Returns default electrum server port for the network 87 | pub fn electrum_port(self) -> u16 { 88 | match self { 89 | PublicNetwork::Mainnet => 50001, 90 | PublicNetwork::Testnet => 60001, 91 | PublicNetwork::Signet => 60601, 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /onchain/src/resolvers/electrum.rs: -------------------------------------------------------------------------------- 1 | // Wallet-level libraries for bitcoin protocol by LNP/BP Association 2 | // 3 | // Written in 2020-2022 by 4 | // Dr. Maxim Orlovsky 5 | // 6 | // This software is distributed without any warranty. 7 | // 8 | // You should have received a copy of the Apache-2.0 License 9 | // along with this software. 10 | // If not, see . 11 | 12 | use std::collections::HashSet; 13 | 14 | use bitcoin::{Script, Transaction, Txid}; 15 | use electrum_client::{Client, ElectrumApi}; 16 | 17 | use super::{ResolveTx, ResolveTxFee, ResolveUtxo, TxResolverError, UtxoResolverError}; 18 | use crate::blockchain::Utxo; 19 | 20 | impl ResolveTx for Client { 21 | fn resolve_tx(&self, txid: Txid) -> Result { 22 | self.transaction_get(&txid).map_err(|err| TxResolverError { 23 | txid, 24 | err: Some(Box::new(err)), 25 | }) 26 | } 27 | } 28 | 29 | impl ResolveTxFee for Client { 30 | fn resolve_tx_fee(&self, txid: Txid) -> Result, TxResolverError> { 31 | let tx = self.resolve_tx(txid)?; 32 | 33 | let input_amount: u64 = tx 34 | .input 35 | .iter() 36 | .map(|i| { 37 | Ok(( 38 | self.resolve_tx(i.previous_output.txid)?, 39 | i.previous_output.vout, 40 | )) 41 | }) 42 | .collect::, TxResolverError>>()? 43 | .into_iter() 44 | .map(|(tx, vout)| tx.output[vout as usize].value) 45 | .sum(); 46 | let output_amount = tx.output.iter().fold(0, |sum, o| sum + o.value); 47 | let fee = input_amount 48 | .checked_sub(output_amount) 49 | .ok_or_else(|| TxResolverError::with(txid))?; 50 | 51 | Ok(Some((tx, fee))) 52 | } 53 | } 54 | 55 | impl ResolveUtxo for Client { 56 | fn resolve_utxo<'script>( 57 | &self, 58 | scripts: impl IntoIterator + Clone, 59 | ) -> Result>, UtxoResolverError> { 60 | Ok(self 61 | .batch_script_list_unspent(scripts)? 62 | .into_iter() 63 | .map(|res| res.into_iter().map(Utxo::from).collect()) 64 | .collect()) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /onchain/src/resolvers/mod.rs: -------------------------------------------------------------------------------- 1 | // Wallet-level libraries for bitcoin protocol by LNP/BP Association 2 | // 3 | // Written in 2020-2022 by 4 | // Dr. Maxim Orlovsky 5 | // 6 | // This software is distributed without any warranty. 7 | // 8 | // You should have received a copy of the Apache-2.0 License 9 | // along with this software. 10 | // If not, see . 11 | 12 | //! Resolvers are traits allow accessing or computing information from a 13 | //! bitcoin transaction graph (from blockchain, state channel, index, PSBT etc). 14 | 15 | #[cfg(feature = "electrum")] 16 | mod electrum; 17 | 18 | use std::collections::{BTreeMap, HashSet}; 19 | 20 | use bitcoin::{Script, Transaction, Txid}; 21 | use bitcoin_hd::DeriveError; 22 | 23 | use crate::blockchain::Utxo; 24 | 25 | #[derive(Debug, Display, Error)] 26 | #[display(doc_comments)] 27 | /// unable to locate transaction {txid} 28 | pub struct TxResolverError { 29 | /// transaction id causing the error 30 | pub txid: Txid, 31 | /// error message 32 | pub err: Option>, 33 | } 34 | 35 | impl TxResolverError { 36 | /// Convenience function for constructing resolver error from simple 37 | /// transaction id without error message 38 | #[inline] 39 | pub fn with(txid: Txid) -> TxResolverError { TxResolverError { txid, err: None } } 40 | } 41 | 42 | /// Transaction resolver 43 | pub trait ResolveTx { 44 | /// Tries to find a transaction by transaction id ([`Txid`]) 45 | fn resolve_tx(&self, txid: Txid) -> Result; 46 | } 47 | 48 | /// Errors during UTXO resolution 49 | #[derive(Debug, Display, Error, From)] 50 | #[display(doc_comments)] 51 | pub enum UtxoResolverError { 52 | /// electrum server error {0} 53 | #[cfg(feature = "electrum")] 54 | #[from] 55 | Electrum(electrum_client::Error), 56 | 57 | /// Derivation error 58 | #[from] 59 | #[display(inner)] 60 | Derivation(DeriveError), 61 | 62 | /// unable to derive descriptor for index {0} which is out of range for 63 | /// unhardened index derivation 64 | IndexOutOfRange(usize), 65 | } 66 | 67 | /// UTXO resolver 68 | pub trait ResolveUtxo { 69 | /// Finds UTXO set for the provided address lists 70 | fn resolve_utxo<'script>( 71 | &self, 72 | scripts: impl IntoIterator + Clone, 73 | ) -> Result>, UtxoResolverError>; 74 | } 75 | 76 | #[cfg(feature = "miniscript_descriptors")] 77 | mod _miniscript_descriptors { 78 | use std::cell::RefCell; 79 | use std::collections::{BTreeMap, HashSet}; 80 | use std::rc::Rc; 81 | 82 | use bitcoin::secp256k1::{Secp256k1, Verification}; 83 | use bitcoin::Script; 84 | use bitcoin_hd::{DerivationAccount, DeriveError, SegmentIndexes, UnhardenedIndex}; 85 | use descriptors::derive::Descriptor; 86 | 87 | use crate::blockchain::Utxo; 88 | use crate::{ResolveUtxo, UtxoResolverError}; 89 | 90 | /// Does complex resolution for miniscript descriptors 91 | pub trait ResolveDescriptor: ResolveUtxo { 92 | /// Finds UTXO set for the addresses derivable from the given descriptor 93 | fn resolve_descriptor_utxo( 94 | &self, 95 | secp: &Secp256k1, 96 | descriptor: &miniscript::Descriptor, 97 | terminal_derivation: impl AsRef<[UnhardenedIndex]>, 98 | from_index: UnhardenedIndex, 99 | count: u32, 100 | ) -> Result)>, UtxoResolverError> { 101 | let terminal_derivation = terminal_derivation.as_ref(); 102 | let mut derivation = 103 | Vec::::with_capacity(terminal_derivation.len() + 1); 104 | derivation.extend(terminal_derivation); 105 | derivation.push(UnhardenedIndex::zero()); 106 | let derivation = Rc::new(RefCell::new(derivation)); 107 | 108 | let indexes = (0..count) 109 | .map(|offset| { 110 | from_index.checked_add(offset).ok_or_else(|| { 111 | UtxoResolverError::IndexOutOfRange( 112 | from_index.first_index() as usize + offset as usize, 113 | ) 114 | }) 115 | }) 116 | .collect::, UtxoResolverError>>()?; 117 | 118 | let scripts = indexes 119 | .into_iter() 120 | .map(|index| { 121 | if let Some(i) = derivation.borrow_mut().last_mut() { 122 | *i = index 123 | } 124 | Ok(( 125 | index, 126 | descriptor.script_pubkey_pretr(secp, &*derivation.borrow())?, 127 | )) 128 | }) 129 | .collect::, DeriveError>>()?; 130 | 131 | Ok(self 132 | .resolve_utxo(scripts.values())? 133 | .into_iter() 134 | .zip(scripts.keys()) 135 | .zip(scripts.values()) 136 | .map(|((utxo_set, index), script)| (*index, (script.clone(), utxo_set))) 137 | .collect()) 138 | } 139 | } 140 | 141 | impl ResolveDescriptor for T where T: ResolveUtxo {} 142 | } 143 | #[cfg(feature = "miniscript_descriptors")] 144 | pub use _miniscript_descriptors::ResolveDescriptor; 145 | 146 | impl ResolveTx for BTreeMap { 147 | fn resolve_tx(&self, txid: Txid) -> Result { 148 | self.get(&txid) 149 | .cloned() 150 | .ok_or_else(|| TxResolverError::with(txid)) 151 | } 152 | } 153 | 154 | /// Transaction resolver 155 | pub trait ResolveTxFee { 156 | /// Tries to find a transaction and comput its fee by transaction id 157 | /// ([`Txid`]) 158 | fn resolve_tx_fee(&self, txid: Txid) -> Result, TxResolverError>; 159 | } 160 | -------------------------------------------------------------------------------- /psbt/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "psbt" 3 | version = { workspace = true } 4 | license = { workspace = true } 5 | authors = { workspace = true } 6 | description = "Partially signed bitcoin transaction v0-2 library (bip174, bip370, bip371)" 7 | repository = { workspace = true } 8 | homepage = { workspace = true } 9 | keywords = ["bitcoin", "wallet", "cryptocurrency", "psbt", "taproot"] 10 | categories = { workspace = true } 11 | readme = { workspace = true } 12 | edition = { workspace = true } 13 | rust-version = { workspace = true } 14 | exclude = [] 15 | 16 | [dependencies] 17 | amplify = { workspace = true } 18 | strict_encoding = { workspace = true } 19 | bitcoin = { workspace = true, features = ["base64"] } 20 | bitcoin_scripts = { workspace = true } 21 | bitcoin_blockchain = { workspace = true } 22 | bitcoin_hd = { workspace = true } 23 | bitcoin_onchain = { workspace = true } 24 | descriptors = { workspace = true, optional = true } 25 | miniscript_crate = { workspace = true, optional = true } 26 | base64 = "0.21.4" 27 | serde_crate = { package = "serde", version = "1", optional = true } 28 | serde_with = { version = "2.3", features = ["hex"], optional = true } 29 | 30 | [dev-dependencies] 31 | strict_encoding_test = "0.9.0" 32 | 33 | [features] 34 | default = [] 35 | all = [ 36 | "serde", 37 | "construct", 38 | "sign" 39 | ] 40 | miniscript = ["miniscript_crate"] 41 | construct = [ 42 | "descriptors", 43 | "miniscript", 44 | "descriptors/miniscript", 45 | "bitcoin_hd/miniscript" 46 | ] 47 | sign = [ 48 | "bitcoin/rand", 49 | "descriptors", 50 | "miniscript", 51 | "descriptors/miniscript", 52 | "bitcoin_hd/miniscript" 53 | ] 54 | serde = [ 55 | "serde_crate", 56 | "serde_with", 57 | "bitcoin/serde", 58 | "bitcoin_scripts/serde", 59 | "bitcoin_blockchain/serde" 60 | ] 61 | -------------------------------------------------------------------------------- /psbt/README.md: -------------------------------------------------------------------------------- 1 | # PSBT implementation 2 | 3 | Implements both v0 (BIP-174) and v2 (BIP-370) versions of PSBT specification. 4 | 5 | Based on [bitcoin](https://crates.io/crate/bitcoin) PSBT implementation, but 6 | wraps it into new type system supporting v2 features and providing convenient 7 | functions to iterate over sets of transaction inputs/outputs and corresponding 8 | PSBT key maps. 9 | -------------------------------------------------------------------------------- /psbt/src/construct/mod.rs: -------------------------------------------------------------------------------- 1 | // Descriptor wallet library extending bitcoin & miniscript functionality 2 | // by LNP/BP Association (https://lnp-bp.org) 3 | // Written in 2020-2022 by 4 | // Dr. Maxim Orlovsky 5 | // 6 | // To the extent possible under law, the author(s) have dedicated all 7 | // copyright and related and neighboring rights to this software to 8 | // the public domain worldwide. This software is distributed without 9 | // any warranty. 10 | // 11 | // You should have received a copy of the Apache-2.0 License 12 | // along with this software. 13 | // If not, see . 14 | 15 | //! Functions, errors and traits specific for PSBT constructor role. 16 | 17 | use std::collections::BTreeSet; 18 | 19 | use bitcoin::secp256k1::SECP256K1; 20 | use bitcoin::util::psbt::TapTree; 21 | use bitcoin::util::taproot::{LeafVersion, TapLeafHash, TaprootBuilder, TaprootBuilderError}; 22 | use bitcoin::{Script, Txid, XOnlyPublicKey}; 23 | use bitcoin_hd::{DerivationAccount, DeriveError, SegmentIndexes, UnhardenedIndex}; 24 | use bitcoin_onchain::{ResolveTx, TxResolverError}; 25 | use bitcoin_scripts::PubkeyScript; 26 | use descriptors::derive::DeriveDescriptor; 27 | use descriptors::InputDescriptor; 28 | use miniscript::{Descriptor, ForEachKey, ToPublicKey}; 29 | 30 | use crate::{self as psbt, Psbt, PsbtVersion}; 31 | 32 | #[derive(Debug, Display, From)] 33 | #[display(doc_comments)] 34 | pub enum Error { 35 | /// unable to construct PSBT due to one of transaction inputs is not known 36 | #[from] 37 | ResolvingTx(TxResolverError), 38 | 39 | /// unable to construct PSBT due to failing key derivetion derivation 40 | #[from] 41 | Derive(DeriveError), 42 | 43 | /// unable to construct PSBT due to spent transaction {0} not having 44 | /// referenced output #{1} 45 | OutputUnknown(Txid, u32), 46 | 47 | /// derived scriptPubkey `{3}` does not match transaction scriptPubkey 48 | /// `{2}` for {0}:{1} 49 | ScriptPubkeyMismatch(Txid, u32, Script, Script), 50 | 51 | /// one of PSBT outputs has invalid script data. {0} 52 | #[from] 53 | Miniscript(miniscript::Error), 54 | 55 | /// taproot script tree construction error. {0} 56 | #[from] 57 | TaprootBuilderError(TaprootBuilderError), 58 | 59 | /// PSBT can't be constructed according to the consensus rules since 60 | /// it spends more ({output} sats) than the sum of its input amounts 61 | /// ({input} sats) 62 | Inflation { 63 | /// Amount spent: input amounts 64 | input: u64, 65 | 66 | /// Amount sent: sum of output value + transaction fee 67 | output: u64, 68 | }, 69 | } 70 | 71 | impl std::error::Error for Error { 72 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 73 | match self { 74 | Error::ResolvingTx(err) => Some(err), 75 | Error::Derive(err) => Some(err), 76 | Error::OutputUnknown(_, _) => None, 77 | Error::ScriptPubkeyMismatch(_, _, _, _) => None, 78 | Error::Miniscript(err) => Some(err), 79 | Error::Inflation { .. } => None, 80 | Error::TaprootBuilderError(err) => Some(err), 81 | } 82 | } 83 | } 84 | 85 | impl Psbt { 86 | pub fn construct<'inputs, 'outputs>( 87 | descriptor: &Descriptor, 88 | inputs: impl IntoIterator, 89 | outputs: impl IntoIterator, 90 | change_index: impl Into, 91 | fee: u64, 92 | tx_resolver: &impl ResolveTx, 93 | ) -> Result { 94 | let mut xpub = bmap! {}; 95 | descriptor.for_each_key(|account| { 96 | if let Some(key_source) = account.account_key_source() { 97 | xpub.insert(account.account_xpub, key_source); 98 | } 99 | true 100 | }); 101 | 102 | let mut total_spent = 0u64; 103 | let mut psbt_inputs: Vec = vec![]; 104 | 105 | for (index, input) in inputs.into_iter().enumerate() { 106 | let txid = input.outpoint.txid; 107 | let mut tx = tx_resolver.resolve_tx(txid)?; 108 | 109 | // Cut out witness data 110 | for inp in &mut tx.input { 111 | inp.witness = zero!(); 112 | } 113 | 114 | let prev_output = tx 115 | .output 116 | .get(input.outpoint.vout as usize) 117 | .ok_or(Error::OutputUnknown(txid, input.outpoint.vout))?; 118 | let (script_pubkey, dtype, tr_descriptor, pretr_descriptor) = match descriptor { 119 | Descriptor::Tr(_) => { 120 | let output_descriptor = DeriveDescriptor::::derive_descriptor( 121 | descriptor, 122 | SECP256K1, 123 | &input.terminal, 124 | )?; 125 | ( 126 | output_descriptor.script_pubkey(), 127 | descriptors::CompositeDescrType::from(&output_descriptor), 128 | Some(output_descriptor), 129 | None, 130 | ) 131 | } 132 | _ => { 133 | let output_descriptor = 134 | DeriveDescriptor::::derive_descriptor( 135 | descriptor, 136 | SECP256K1, 137 | &input.terminal, 138 | )?; 139 | ( 140 | output_descriptor.script_pubkey(), 141 | descriptors::CompositeDescrType::from(&output_descriptor), 142 | None, 143 | Some(output_descriptor), 144 | ) 145 | } 146 | }; 147 | if prev_output.script_pubkey != script_pubkey { 148 | return Err(Error::ScriptPubkeyMismatch( 149 | txid, 150 | input.outpoint.vout, 151 | prev_output.script_pubkey.clone(), 152 | script_pubkey, 153 | )); 154 | } 155 | let mut bip32_derivation = bmap! {}; 156 | let result = descriptor.for_each_key(|account| { 157 | match account.bip32_derivation(SECP256K1, &input.terminal) { 158 | Ok((pubkey, key_source)) => { 159 | bip32_derivation.insert(pubkey, key_source); 160 | true 161 | } 162 | Err(_) => false, 163 | } 164 | }); 165 | if !result { 166 | return Err(DeriveError::DerivePatternMismatch.into()); 167 | } 168 | 169 | total_spent += prev_output.value; 170 | 171 | let mut psbt_input = psbt::Input { 172 | index, 173 | previous_outpoint: input.outpoint, 174 | sequence_number: Some(input.seq_no), 175 | bip32_derivation, 176 | sighash_type: Some(input.sighash_type.into()), 177 | ..default!() 178 | }; 179 | 180 | if dtype.is_segwit() { 181 | psbt_input.witness_utxo = Some(prev_output.clone()); 182 | } 183 | // This is required even in case of segwit outputs, since at least Ledger Nano X 184 | // do not trust just `non_witness_utxo` data. 185 | psbt_input.non_witness_utxo = Some(tx.clone()); 186 | 187 | if let Some(Descriptor::::Tr(tr)) = tr_descriptor { 188 | psbt_input.bip32_derivation.clear(); 189 | psbt_input.tap_merkle_root = tr.spend_info().merkle_root(); 190 | psbt_input.tap_internal_key = Some(tr.internal_key().to_x_only_pubkey()); 191 | let spend_info = tr.spend_info(); 192 | psbt_input.tap_scripts = spend_info 193 | .as_script_map() 194 | .iter() 195 | .map(|((script, leaf_ver), _)| { 196 | ( 197 | spend_info 198 | .control_block(&(script.clone(), *leaf_ver)) 199 | .expect("taproot scriptmap is broken"), 200 | (script.clone(), *leaf_ver), 201 | ) 202 | }) 203 | .collect(); 204 | if let Some(taptree) = tr.taptree() { 205 | descriptor.for_each_key(|key| { 206 | let (pubkey, key_source) = key 207 | .bip32_derivation(SECP256K1, &input.terminal) 208 | .expect("failing on second pass of the same function"); 209 | let pubkey = XOnlyPublicKey::from(pubkey); 210 | let mut leaves = vec![]; 211 | for (_, ms) in taptree.iter() { 212 | for pk in ms.iter_pk() { 213 | if pk == pubkey { 214 | leaves.push(TapLeafHash::from_script( 215 | &ms.encode(), 216 | LeafVersion::TapScript, 217 | )); 218 | } 219 | } 220 | } 221 | let entry = psbt_input 222 | .tap_key_origins 223 | .entry(pubkey.to_x_only_pubkey()) 224 | .or_insert((vec![], key_source)); 225 | entry.0.extend(leaves); 226 | true 227 | }); 228 | } 229 | descriptor.for_each_key(|key| { 230 | let (pubkey, key_source) = key 231 | .bip32_derivation(SECP256K1, &input.terminal) 232 | .expect("failing on second pass of the same function"); 233 | let pubkey = XOnlyPublicKey::from(pubkey); 234 | if pubkey == *tr.internal_key() { 235 | psbt_input 236 | .tap_key_origins 237 | .entry(pubkey.to_x_only_pubkey()) 238 | .or_insert((vec![], key_source)); 239 | } 240 | true 241 | }); 242 | for (leaves, _) in psbt_input.tap_key_origins.values_mut() { 243 | *leaves = leaves 244 | .iter() 245 | .cloned() 246 | .collect::>() 247 | .into_iter() 248 | .collect(); 249 | } 250 | } else if let Some(output_descriptor) = pretr_descriptor { 251 | let lock_script = output_descriptor.explicit_script()?; 252 | if dtype.has_redeem_script() { 253 | psbt_input.redeem_script = Some(lock_script.clone().into()); 254 | } 255 | if dtype.has_witness_script() { 256 | psbt_input.witness_script = Some(lock_script.into()); 257 | } 258 | } 259 | 260 | psbt_inputs.push(psbt_input); 261 | } 262 | 263 | let mut total_sent = 0u64; 264 | let mut psbt_outputs: Vec<_> = outputs 265 | .into_iter() 266 | .enumerate() 267 | .map(|(index, (script, amount))| { 268 | total_sent += *amount; 269 | psbt::Output { 270 | index, 271 | amount: *amount, 272 | script: script.clone(), 273 | ..default!() 274 | } 275 | }) 276 | .collect(); 277 | 278 | let change = match total_spent.checked_sub(total_sent + fee) { 279 | Some(change) => change, 280 | None => { 281 | return Err(Error::Inflation { 282 | input: total_spent, 283 | output: total_sent + fee, 284 | }) 285 | } 286 | }; 287 | 288 | if change > 0 { 289 | let change_derivation = [UnhardenedIndex::one(), change_index.into()]; 290 | let mut bip32_derivation = bmap! {}; 291 | let bip32_derivation_fn = |account: &DerivationAccount| { 292 | let (pubkey, key_source) = account 293 | .bip32_derivation(SECP256K1, change_derivation) 294 | .expect("already tested descriptor derivation mismatch"); 295 | bip32_derivation.insert(pubkey, key_source); 296 | true 297 | }; 298 | 299 | let mut psbt_change_output = psbt::Output { 300 | index: psbt_outputs.len(), 301 | amount: change, 302 | ..default!() 303 | }; 304 | if let Descriptor::Tr(_) = descriptor { 305 | let change_descriptor = DeriveDescriptor::::derive_descriptor( 306 | descriptor, 307 | SECP256K1, 308 | change_derivation, 309 | )?; 310 | let change_descriptor = match change_descriptor { 311 | Descriptor::Tr(tr) => tr, 312 | _ => unreachable!(), 313 | }; 314 | 315 | psbt_change_output.script = change_descriptor.script_pubkey().into(); 316 | descriptor.for_each_key(bip32_derivation_fn); 317 | 318 | let internal_key: XOnlyPublicKey = 319 | change_descriptor.internal_key().to_x_only_pubkey(); 320 | psbt_change_output.tap_internal_key = Some(internal_key); 321 | if let Some(tree) = change_descriptor.taptree() { 322 | let mut builder = TaprootBuilder::new(); 323 | for (depth, ms) in tree.iter() { 324 | builder = builder 325 | .add_leaf(depth, ms.encode()) 326 | .expect("insane miniscript taptree"); 327 | } 328 | psbt_change_output.tap_tree = 329 | Some(TapTree::try_from(builder).expect("non-finalized TaprootBuilder")); 330 | } 331 | } else { 332 | let change_descriptor = DeriveDescriptor::::derive_descriptor( 333 | descriptor, 334 | SECP256K1, 335 | change_derivation, 336 | )?; 337 | psbt_change_output.script = change_descriptor.script_pubkey().into(); 338 | 339 | let dtype = descriptors::CompositeDescrType::from(&change_descriptor); 340 | descriptor.for_each_key(bip32_derivation_fn); 341 | 342 | let lock_script = change_descriptor.explicit_script()?; 343 | if dtype.has_redeem_script() { 344 | psbt_change_output.redeem_script = Some(lock_script.clone().into()); 345 | } 346 | if dtype.has_witness_script() { 347 | psbt_change_output.witness_script = Some(lock_script.into()); 348 | } 349 | } 350 | 351 | psbt_change_output.bip32_derivation = bip32_derivation; 352 | psbt_outputs.push(psbt_change_output); 353 | } 354 | 355 | Ok(Psbt { 356 | psbt_version: PsbtVersion::V0, 357 | tx_version: 2, 358 | xpub, 359 | inputs: psbt_inputs, 360 | outputs: psbt_outputs, 361 | fallback_locktime: None, 362 | proprietary: none!(), 363 | unknown: none!(), 364 | }) 365 | } 366 | } 367 | -------------------------------------------------------------------------------- /psbt/src/errors.rs: -------------------------------------------------------------------------------- 1 | // Wallet-level libraries for bitcoin protocol by LNP/BP Association 2 | // 3 | // Written in 2020-2022 by 4 | // Dr. Maxim Orlovsky 5 | // 6 | // This software is distributed without any warranty. 7 | // 8 | // You should have received a copy of the Apache-2.0 License 9 | // along with this software. 10 | // If not, see . 11 | 12 | use bitcoin::Txid; 13 | 14 | /// Errors during [`Input`](super::Input) construction from an unsigned 15 | /// transaction input (see [`Input::new`](super::Input::new)). 16 | #[derive( 17 | Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, Display, Error 18 | )] 19 | #[display(doc_comments)] 20 | pub enum TxinError { 21 | /// the scriptSigs in the {0} unsigned transaction output is not empty. 22 | UnsignedTxHasScriptSigs(usize), 23 | 24 | /// the scriptWitnesses in the {0} unsigned transaction output is not empty. 25 | UnsignedTxHasScriptWitnesses(usize), 26 | } 27 | 28 | /// Errors during [`Psbt`](super::Psbt) construction from an unsigned 29 | /// transaction data (see [`Psbt::with`](super::Psbt::with())). 30 | #[derive( 31 | Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, Display, Error, From 32 | )] 33 | pub enum TxError { 34 | /// Error in an unsigned transaction input (see [`TxinError`]). 35 | #[from] 36 | #[display(inner)] 37 | Txin(TxinError), 38 | 39 | /// the unsigned transaction has negative version value ({0}), which is not 40 | /// allowed in PSBT. 41 | InvalidTxVersion(i32), 42 | } 43 | 44 | /// Errors happening when PSBT or other resolver information does not match the 45 | /// structure of bitcoin transaction 46 | #[derive( 47 | Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Display, Error 48 | )] 49 | #[display(doc_comments)] 50 | pub enum InputMatchError { 51 | /// no `witness_utxo` and `non_witness_utxo` is provided 52 | NoInputTx, 53 | 54 | /// provided `non_witness_utxo` does not match transaction input `prev_out` 55 | NoTxidMatch(Txid), 56 | 57 | /// spent transaction does not contain input #{0} referenced by the PSBT 58 | /// input 59 | UnmatchedInputNumber(u32), 60 | } 61 | 62 | /// Errors happening during fee computation 63 | #[derive( 64 | Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Display, Error, From 65 | )] 66 | #[display(doc_comments)] 67 | pub enum FeeError { 68 | /// No input source information found because of wrong or incomplete PSBT 69 | /// structure 70 | #[from] 71 | MatchError(InputMatchError), 72 | 73 | /// Sum of inputs is less than sum of outputs 74 | InputsLessThanOutputs, 75 | } 76 | -------------------------------------------------------------------------------- /psbt/src/global.rs: -------------------------------------------------------------------------------- 1 | // Wallet-level libraries for bitcoin protocol by LNP/BP Association 2 | // 3 | // Written in 2020-2022 by 4 | // Dr. Maxim Orlovsky 5 | // 6 | // This software is distributed without any warranty. 7 | // 8 | // You should have received a copy of the Apache-2.0 License 9 | // along with this software. 10 | // If not, see . 11 | 12 | use std::collections::BTreeMap; 13 | use std::fmt::{Display, Formatter}; 14 | use std::str::FromStr; 15 | 16 | use base64::Engine; 17 | use bitcoin::util::bip32::{ExtendedPubKey, KeySource}; 18 | use bitcoin::{consensus, Transaction, Txid}; 19 | use bitcoin_blockchain::locks::LockTime; 20 | #[cfg(feature = "serde")] 21 | use serde_with::{hex::Hex, As, Same}; 22 | 23 | use crate::serialize::{Deserialize, Serialize}; 24 | use crate::v0::PsbtV0; 25 | use crate::{raw, Error, FeeError, Input, Output, PsbtVersion, TxError}; 26 | 27 | // TODO: Do manual serde and strict encoding implementation to check the 28 | // deserialized values 29 | #[derive(Clone, Eq, PartialEq, Debug, Default)] 30 | #[derive(StrictEncode, StrictDecode)] 31 | #[cfg_attr( 32 | feature = "serde", 33 | derive(Serialize, Deserialize), 34 | serde(crate = "serde_crate") 35 | )] 36 | pub struct Psbt { 37 | /// The version number of this PSBT. If omitted, the version number is 0. 38 | pub psbt_version: PsbtVersion, 39 | 40 | /// Transaction version. 41 | pub tx_version: u32, 42 | 43 | /// Fallback locktime (used if none of the inputs specifies their locktime). 44 | pub fallback_locktime: Option, 45 | 46 | /// The corresponding key-value map for each input. 47 | pub inputs: Vec, 48 | 49 | /// The corresponding key-value map for each output. 50 | pub outputs: Vec, 51 | 52 | /// A global map from extended public keys to the used key fingerprint and 53 | /// derivation path as defined by BIP 32 54 | pub xpub: BTreeMap, 55 | 56 | /// Global proprietary key-value pairs. 57 | #[cfg_attr(feature = "serde", serde(with = "As::>"))] 58 | pub proprietary: BTreeMap>, 59 | 60 | /// Unknown global key-value pairs. 61 | #[cfg_attr(feature = "serde", serde(with = "As::>"))] 62 | pub unknown: BTreeMap>, 63 | } 64 | 65 | impl Psbt { 66 | /// Checks that unsigned transaction does not have scriptSig's or witness 67 | /// data 68 | pub fn with(tx: Transaction, psbt_version: PsbtVersion) -> Result { 69 | let inputs = tx 70 | .input 71 | .into_iter() 72 | .enumerate() 73 | .map(|(index, txin)| Input::new(index, txin).map_err(TxError::from)) 74 | .collect::>()?; 75 | let outputs = tx 76 | .output 77 | .into_iter() 78 | .enumerate() 79 | .map(|(index, txout)| Output::new(index, txout)) 80 | .collect(); 81 | 82 | let i32_version = tx.version; 83 | let tx_version = i32_version 84 | .try_into() 85 | .map_err(|_| TxError::InvalidTxVersion(i32_version))?; 86 | 87 | let fallback_locktime = match tx.lock_time.0 { 88 | 0 => None, 89 | other => Some(other.into()), 90 | }; 91 | 92 | Ok(Psbt { 93 | psbt_version, 94 | xpub: Default::default(), 95 | tx_version, 96 | fallback_locktime, 97 | inputs, 98 | outputs, 99 | proprietary: Default::default(), 100 | unknown: Default::default(), 101 | }) 102 | } 103 | 104 | pub fn lock_time(&self) -> LockTime { 105 | let required_time_locktime = self 106 | .inputs 107 | .iter() 108 | .filter_map(|input| input.required_time_locktime) 109 | .max(); 110 | let required_height_locktime = self 111 | .inputs 112 | .iter() 113 | .filter_map(|input| input.required_height_locktime) 114 | .max(); 115 | 116 | match ( 117 | required_time_locktime, 118 | required_height_locktime, 119 | self.fallback_locktime, 120 | ) { 121 | (None, None, fallback) => fallback.unwrap_or_default(), 122 | (Some(lock), None, _) => lock.into(), 123 | (None, Some(lock), _) => lock.into(), 124 | (Some(lock1), Some(_lock2), Some(fallback)) if fallback.is_time_based() => lock1.into(), 125 | (Some(_lock1), Some(lock2), Some(fallback)) if fallback.is_height_based() => { 126 | lock2.into() 127 | } 128 | (Some(lock1), Some(_lock2), _) => lock1.into(), 129 | } 130 | } 131 | 132 | pub(crate) fn tx_version(&self) -> i32 { i32::from_be_bytes(self.tx_version.to_be_bytes()) } 133 | 134 | /// Returns fee for a transaction, or returns error reporting resolver 135 | /// problem or wrong transaction structure 136 | pub fn fee(&self) -> Result { 137 | let mut input_sum = 0; 138 | for inp in &self.inputs { 139 | input_sum += inp.input_prevout()?.value; 140 | } 141 | 142 | let output_sum = self.outputs.iter().map(|output| output.amount).sum(); 143 | 144 | if input_sum < output_sum { 145 | Err(FeeError::InputsLessThanOutputs) 146 | } else { 147 | Ok(input_sum - output_sum) 148 | } 149 | } 150 | 151 | /// Returns transaction ID for an unsigned transaction. For SegWit 152 | /// transactions this is equal to the signed transaction id. 153 | #[inline] 154 | pub fn to_txid(&self) -> Txid { self.to_unsigned_tx().txid() } 155 | 156 | /// Constructs transaction with empty `scriptSig` and `witness` 157 | pub fn to_unsigned_tx(&self) -> Transaction { 158 | let version = self.tx_version(); 159 | 160 | let lock_time = bitcoin::PackedLockTime(self.lock_time().into_consensus()); 161 | 162 | let tx_inputs = self.inputs.iter().map(Input::to_unsigned_txin).collect(); 163 | let tx_outputs = self.outputs.iter().map(Output::to_txout).collect(); 164 | 165 | Transaction { 166 | version, 167 | lock_time, 168 | input: tx_inputs, 169 | output: tx_outputs, 170 | } 171 | } 172 | 173 | /// Returns transaction with empty `scriptSig` and `witness` 174 | pub fn into_unsigned_tx(self) -> Transaction { 175 | let version = self.tx_version(); 176 | 177 | let lock_time = bitcoin::PackedLockTime(self.lock_time().into_consensus()); 178 | 179 | let tx_inputs = self.inputs.iter().map(Input::to_unsigned_txin).collect(); 180 | let tx_outputs = self.outputs.into_iter().map(Output::into_txout).collect(); 181 | 182 | Transaction { 183 | version, 184 | lock_time, 185 | input: tx_inputs, 186 | output: tx_outputs, 187 | } 188 | } 189 | 190 | /// Extract the (partially) signed transaction from this PSBT by filling in 191 | /// the available signature information in place. 192 | #[inline] 193 | pub fn extract_signed_tx(&self) -> Transaction { 194 | let mut tx: Transaction = self.to_unsigned_tx(); 195 | 196 | for (vin, psbtin) in tx.input.iter_mut().zip(self.inputs.iter()) { 197 | vin.script_sig = psbtin.final_script_sig.clone().unwrap_or_default().into(); 198 | vin.witness = psbtin.final_script_witness.clone().unwrap_or_default(); 199 | } 200 | 201 | tx 202 | } 203 | 204 | /// Combines this [`Psbt`] with `other` PSBT as described by BIP 174. 205 | /// 206 | /// In accordance with BIP 174 this function is commutative i.e., 207 | /// `A.combine(B) == B.combine(A)` 208 | #[inline] 209 | pub fn combine(self, other: Self) -> Result { 210 | let mut first = PsbtV0::from(self); 211 | first.combine(other.into())?; 212 | Ok(first.into()) 213 | } 214 | } 215 | 216 | impl From for Psbt { 217 | fn from(v0: PsbtV0) -> Self { 218 | let tx = v0.unsigned_tx; 219 | 220 | let inputs = v0 221 | .inputs 222 | .into_iter() 223 | .zip(tx.input) 224 | .enumerate() 225 | .map(|(index, (input, txin))| Input::with(index, input, txin)) 226 | .collect(); 227 | 228 | let outputs = v0 229 | .outputs 230 | .into_iter() 231 | .zip(tx.output) 232 | .enumerate() 233 | .map(|(index, (output, txout))| Output::with(index, output, txout)) 234 | .collect(); 235 | 236 | let tx_version = u32::from_be_bytes(tx.version.to_be_bytes()); 237 | 238 | let fallback_locktime = match tx.lock_time.0 { 239 | 0 => None, 240 | other => Some(other.into()), 241 | }; 242 | 243 | Psbt { 244 | // We need to serialize back in the same version we deserialzied from 245 | psbt_version: PsbtVersion::V0, 246 | xpub: v0.xpub, 247 | tx_version, 248 | fallback_locktime, 249 | inputs, 250 | outputs, 251 | proprietary: v0.proprietary, 252 | unknown: v0.unknown, 253 | } 254 | } 255 | } 256 | 257 | impl From for PsbtV0 { 258 | fn from(psbt: Psbt) -> Self { 259 | let version = psbt.tx_version(); 260 | let lock_time = bitcoin::PackedLockTime(psbt.lock_time().into_consensus()); 261 | 262 | let (v0_inputs, tx_inputs) = psbt.inputs.into_iter().map(Input::split).unzip(); 263 | let (v0_outputs, tx_outputs) = psbt.outputs.into_iter().map(Output::split).unzip(); 264 | 265 | let unsigned_tx = Transaction { 266 | version, 267 | lock_time, 268 | input: tx_inputs, 269 | output: tx_outputs, 270 | }; 271 | 272 | PsbtV0 { 273 | unsigned_tx, 274 | version: PsbtVersion::V0 as u32, 275 | xpub: psbt.xpub, 276 | proprietary: psbt.proprietary, 277 | unknown: psbt.unknown, 278 | inputs: v0_inputs, 279 | outputs: v0_outputs, 280 | } 281 | } 282 | } 283 | 284 | // TODO: Implement own PSBT BIP174 serialization trait and its own custom error 285 | // type handling different PSBT versions. 286 | impl Serialize for Psbt { 287 | fn serialize(&self) -> Vec { consensus::encode::serialize::(&self.clone().into()) } 288 | } 289 | 290 | impl Deserialize for Psbt { 291 | fn deserialize(bytes: &[u8]) -> Result { 292 | consensus::deserialize::(bytes).map(Psbt::from) 293 | } 294 | } 295 | 296 | impl Display for Psbt { 297 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 298 | let engine = base64::engine::GeneralPurpose::new( 299 | &base64::alphabet::STANDARD, 300 | base64::engine::GeneralPurposeConfig::new(), 301 | ); 302 | f.write_str(&engine.encode(self.serialize())) 303 | } 304 | } 305 | 306 | #[derive(Debug, Display, Error, From)] 307 | #[display(inner)] 308 | pub enum PsbtParseError { 309 | #[from] 310 | Data(consensus::encode::Error), 311 | 312 | #[from] 313 | Base64(base64::DecodeError), 314 | } 315 | 316 | impl FromStr for Psbt { 317 | type Err = PsbtParseError; 318 | 319 | fn from_str(s: &str) -> Result { 320 | let engine = base64::engine::GeneralPurpose::new( 321 | &base64::alphabet::STANDARD, 322 | base64::engine::GeneralPurposeConfig::new(), 323 | ); 324 | let bytes = engine.decode(s)?; 325 | Psbt::deserialize(&bytes).map_err(PsbtParseError::from) 326 | } 327 | } 328 | 329 | #[cfg(test)] 330 | mod test { 331 | use amplify::hex::FromHex; 332 | 333 | use super::*; 334 | 335 | #[test] 336 | #[ignore] 337 | fn psbt_bip174_serialization() { 338 | let hex = "\ 339 | 70736274ff0100750200000001268171371edff285e937adeea4b37b78000c0566\ 340 | cbb3ad64641713ca42171bf60000000000feffffff02d3dff505000000001976a91\ 341 | 4d0c59903c5bac2868760e90fd521a4665aa7652088ac00e1f5050000000017a914\ 342 | 3545e6e33b832c47050f24d3eeb93c9c03948bc787b32e1300000100fda50101000\ 343 | 00000010289a3c71eab4d20e0371bbba4cc698fa295c9463afa2e397f8533ccb62f\ 344 | 9567e50100000017160014be18d152a9b012039daf3da7de4f53349eecb985fffff\ 345 | fff86f8aa43a71dff1448893a530a7237ef6b4608bbb2dd2d0171e63aec6a4890b4\ 346 | 0100000017160014fe3e9ef1a745e974d902c4355943abcb34bd5353ffffffff020\ 347 | 0c2eb0b000000001976a91485cff1097fd9e008bb34af709c62197b38978a4888ac\ 348 | 72fef84e2c00000017a914339725ba21efd62ac753a9bcd067d6c7a6a39d0587024\ 349 | 7304402202712be22e0270f394f568311dc7ca9a68970b8025fdd3b240229f07f8a\ 350 | 5f3a240220018b38d7dcd314e734c9276bd6fb40f673325bc4baa144c800d2f2f02\ 351 | db2765c012103d2e15674941bad4a996372cb87e1856d3652606d98562fe39c5e9e\ 352 | 7e413f210502483045022100d12b852d85dcd961d2f5f4ab660654df6eedcc794c0\ 353 | c33ce5cc309ffb5fce58d022067338a8e0e1725c197fb1a88af59f51e44e4255b20\ 354 | 167c8684031c05d1f2592a01210223b72beef0965d10be0778efecd61fcac6f79a4\ 355 | ea169393380734464f84f2ab300000000000000"; 356 | 357 | let psbt = Psbt::from_str(hex).unwrap(); 358 | let hex_prime = psbt.to_string(); 359 | let psbt_prime = Psbt::deserialize(&Vec::from_hex(&hex_prime).unwrap()).unwrap(); 360 | assert_eq!(psbt, psbt_prime); 361 | assert_eq!(hex, hex_prime); 362 | } 363 | } 364 | -------------------------------------------------------------------------------- /psbt/src/lex_order.rs: -------------------------------------------------------------------------------- 1 | // Wallet-level libraries for bitcoin protocol by LNP/BP Association 2 | // 3 | // Written in 2020-2022 by 4 | // Dr. Maxim Orlovsky 5 | // 6 | // This software is distributed without any warranty. 7 | // 8 | // You should have received a copy of the Apache-2.0 License 9 | // along with this software. 10 | // If not, see . 11 | 12 | //! Lexicographic sorting functions. 13 | 14 | use std::cmp::Ordering; 15 | 16 | use bitcoin::{secp256k1, Transaction, TxIn, TxOut}; 17 | 18 | use crate::v0::PsbtV0; 19 | use crate::{Input, Output, Psbt}; 20 | 21 | pub trait LexOrder { 22 | fn lex_order(&mut self); 23 | 24 | fn lex_ordered(mut self) -> Self 25 | where 26 | Self: Sized, 27 | { 28 | self.lex_order(); 29 | self 30 | } 31 | } 32 | 33 | impl LexOrder for Vec { 34 | fn lex_order(&mut self) { self.sort() } 35 | } 36 | 37 | impl LexOrder for Vec { 38 | fn lex_order(&mut self) { self.sort() } 39 | } 40 | 41 | impl LexOrder for Vec { 42 | fn lex_order(&mut self) { self.sort_by_key(|txin| txin.previous_output) } 43 | } 44 | 45 | impl LexOrder for Vec { 46 | fn lex_order(&mut self) { self.sort_by(txout_cmp) } 47 | } 48 | 49 | impl LexOrder for Transaction { 50 | fn lex_order(&mut self) { 51 | self.input.lex_order(); 52 | self.output.lex_order(); 53 | } 54 | } 55 | 56 | impl LexOrder for Vec<(TxOut, crate::v0::OutputV0)> { 57 | fn lex_order(&mut self) { self.sort_by(|(a, _), (b, _)| txout_cmp(a, b)); } 58 | } 59 | 60 | impl LexOrder for PsbtV0 { 61 | fn lex_order(&mut self) { 62 | let tx = &mut self.unsigned_tx; 63 | let mut inputs = tx 64 | .input 65 | .clone() 66 | .into_iter() 67 | .zip(self.inputs.clone()) 68 | .collect::>(); 69 | inputs.sort_by_key(|(k, _)| k.previous_output); 70 | 71 | let mut outputs = tx 72 | .output 73 | .clone() 74 | .into_iter() 75 | .zip(self.outputs.clone()) 76 | .collect::>(); 77 | outputs.lex_order(); 78 | 79 | let (in_tx, in_map): (Vec<_>, Vec<_>) = inputs.into_iter().unzip(); 80 | let (out_tx, out_map): (Vec<_>, Vec<_>) = outputs.into_iter().unzip(); 81 | tx.input = in_tx; 82 | tx.output = out_tx; 83 | self.inputs = in_map; 84 | self.outputs = out_map; 85 | } 86 | } 87 | 88 | impl LexOrder for Vec { 89 | fn lex_order(&mut self) { 90 | self.sort_by_key(|input| input.previous_outpoint); 91 | for (index, input) in self.iter_mut().enumerate() { 92 | input.index = index; 93 | } 94 | } 95 | } 96 | 97 | impl LexOrder for Vec { 98 | fn lex_order(&mut self) { 99 | self.sort_by(psbtout_cmp); 100 | for (index, output) in self.iter_mut().enumerate() { 101 | output.index = index; 102 | } 103 | } 104 | } 105 | 106 | impl LexOrder for Psbt { 107 | fn lex_order(&mut self) { 108 | self.inputs.lex_order(); 109 | self.outputs.lex_order(); 110 | } 111 | } 112 | 113 | fn txout_cmp(left: &TxOut, right: &TxOut) -> Ordering { 114 | match (left.value, right.value) { 115 | (l, r) if l < r => Ordering::Less, 116 | (l, r) if l > r => Ordering::Greater, 117 | _ => left.script_pubkey.cmp(&right.script_pubkey), 118 | } 119 | } 120 | 121 | fn psbtout_cmp(left: &Output, right: &Output) -> Ordering { 122 | match (left.amount, right.amount) { 123 | (l, r) if l < r => Ordering::Less, 124 | (l, r) if l > r => Ordering::Greater, 125 | _ => left.script.cmp(&right.script), 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /psbt/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Wallet-level libraries for bitcoin protocol by LNP/BP Association 2 | // 3 | // Written in 2020-2022 by 4 | // Dr. Maxim Orlovsky 5 | // 6 | // This software is distributed without any warranty. 7 | // 8 | // You should have received a copy of the Apache-2.0 License 9 | // along with this software. 10 | // If not, see . 11 | 12 | // Coding conventions 13 | #![recursion_limit = "256"] 14 | #![deny(dead_code, /* missing_docs, */ warnings)] 15 | 16 | //! PSBT bitcoin library, providing all PSBT functionality from [`bitcoin`] 17 | //! library, plus 18 | //! - constructor, supporting miniscript-based descriptors, input descriptors, 19 | //! all sighash types, spendings from P2C, S2C-tweaked inputs ([`construct`]); 20 | //! - advanced signer, supporting pre-segwit, bare and nested segwit v0, taproot 21 | //! key and path spendings, different forms of tweaks & commitments, all 22 | //! sighash types ([`sign`]); 23 | //! - commitment-related features: managing tapret-, P2C and S2C-related 24 | //! proprietary keys; 25 | //! - utility methods for fee computing, lexicographic reordering etc; 26 | //! - command-line utility for editing PSBT data (WIP). 27 | 28 | #[macro_use] 29 | extern crate amplify; 30 | #[cfg(feature = "serde")] 31 | #[macro_use] 32 | extern crate serde_crate as serde; 33 | #[macro_use] 34 | extern crate strict_encoding; 35 | #[cfg(feature = "miniscript")] 36 | extern crate miniscript_crate as miniscript; 37 | 38 | mod errors; 39 | mod global; 40 | mod input; 41 | mod output; 42 | pub mod p2c; 43 | 44 | #[cfg(feature = "construct")] 45 | pub mod construct; 46 | pub mod lex_order; 47 | mod proprietary; 48 | #[cfg(feature = "sign")] 49 | pub mod sign; 50 | 51 | pub use bitcoin::psbt::raw::ProprietaryKey; 52 | pub use bitcoin::psbt::{raw, serialize, Error, PsbtSighashType}; 53 | pub use errors::{FeeError, InputMatchError, TxError, TxinError}; 54 | pub use global::{Psbt, PsbtParseError}; 55 | pub use input::Input; 56 | pub use output::Output; 57 | pub(crate) mod v0 { 58 | pub use bitcoin::psbt::{ 59 | Input as InputV0, Output as OutputV0, PartiallySignedTransaction as PsbtV0, 60 | }; 61 | } 62 | pub use p2c::{PSBT_IN_P2C_TWEAK, PSBT_P2C_PREFIX}; 63 | pub use proprietary::{ 64 | ProprietaryKeyDescriptor, ProprietaryKeyError, ProprietaryKeyLocation, ProprietaryKeyType, 65 | }; 66 | 67 | /// Version of the PSBT (V0 stands for BIP174-defined version; V2 - for BIP370). 68 | #[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, Default)] 69 | #[derive(StrictEncode, StrictDecode)] 70 | #[strict_encoding(repr = u32)] 71 | #[cfg_attr( 72 | feature = "serde", 73 | derive(Serialize, Deserialize), 74 | serde(crate = "serde_crate") 75 | )] 76 | #[repr(u32)] 77 | pub enum PsbtVersion { 78 | /// Version defined by BIP174. 79 | #[default] 80 | V0 = 0x0, 81 | /// Version defined by BIP370. 82 | V2 = 0x2, 83 | } 84 | -------------------------------------------------------------------------------- /psbt/src/output.rs: -------------------------------------------------------------------------------- 1 | // Wallet-level libraries for bitcoin protocol by LNP/BP Association 2 | // 3 | // Written in 2020-2022 by 4 | // Dr. Maxim Orlovsky 5 | // 6 | // This software is distributed without any warranty. 7 | // 8 | // You should have received a copy of the Apache-2.0 License 9 | // along with this software. 10 | // If not, see . 11 | 12 | use std::collections::BTreeMap; 13 | 14 | use bitcoin::psbt::TapTree; 15 | use bitcoin::util::bip32::KeySource; 16 | use bitcoin::util::taproot::TapLeafHash; 17 | use bitcoin::{secp256k1, TxOut, XOnlyPublicKey}; 18 | use bitcoin_scripts::{PubkeyScript, RedeemScript, WitnessScript}; 19 | #[cfg(feature = "serde")] 20 | use serde_with::{hex::Hex, As, Same}; 21 | 22 | use crate::raw; 23 | use crate::v0::OutputV0; 24 | 25 | // TODO: Do manual serde implementation to check the deserialized values 26 | #[derive(Clone, Eq, PartialEq, Debug, Default)] 27 | #[derive(StrictEncode, StrictDecode)] 28 | #[cfg_attr( 29 | feature = "serde", 30 | derive(Serialize, Deserialize), 31 | serde(crate = "serde_crate") 32 | )] 33 | pub struct Output { 34 | /// The index of this output. Used in error reporting. 35 | pub(crate) index: usize, 36 | 37 | /// The output's amount in satoshis. 38 | pub amount: u64, 39 | 40 | /// The script for this output, also known as the scriptPubKey. 41 | pub script: PubkeyScript, 42 | 43 | /// The redeem script for this output. 44 | pub redeem_script: Option, 45 | 46 | /// The witness script for this output. 47 | pub witness_script: Option, 48 | 49 | /// A map from public keys needed to spend this output to their 50 | /// corresponding master key fingerprints and derivation paths. 51 | #[cfg_attr(feature = "serde", serde(with = "As::>"))] 52 | pub bip32_derivation: BTreeMap, 53 | 54 | /// The internal pubkey. 55 | pub tap_internal_key: Option, 56 | 57 | /// Taproot Output tree. 58 | pub tap_tree: Option, 59 | 60 | /// Map of tap root x only keys to origin info and leaf hashes contained in 61 | /// it. 62 | #[cfg_attr( 63 | feature = "serde", 64 | serde(with = "As::, Same)>>") 65 | )] 66 | pub tap_key_origins: BTreeMap, KeySource)>, 67 | 68 | /// Proprietary key-value pairs for this output. 69 | #[cfg_attr(feature = "serde", serde(with = "As::>"))] 70 | pub proprietary: BTreeMap>, 71 | 72 | /// Unknown key-value pairs for this output. 73 | #[cfg_attr(feature = "serde", serde(with = "As::>"))] 74 | pub unknown: BTreeMap>, 75 | } 76 | 77 | impl Output { 78 | pub fn new(index: usize, txout: TxOut) -> Self { 79 | Output { 80 | index, 81 | amount: txout.value, 82 | script: txout.script_pubkey.into(), 83 | ..Output::default() 84 | } 85 | } 86 | 87 | pub fn with(index: usize, v0: OutputV0, txout: TxOut) -> Self { 88 | Output { 89 | index, 90 | amount: txout.value, 91 | script: txout.script_pubkey.into(), 92 | redeem_script: v0.redeem_script.map(Into::into), 93 | witness_script: v0.witness_script.map(Into::into), 94 | bip32_derivation: v0.bip32_derivation, 95 | tap_internal_key: v0.tap_internal_key, 96 | tap_tree: v0.tap_tree, 97 | tap_key_origins: v0.tap_key_origins, 98 | proprietary: v0.proprietary, 99 | unknown: v0.unknown, 100 | } 101 | } 102 | 103 | #[inline] 104 | pub fn index(&self) -> usize { self.index } 105 | 106 | pub fn to_txout(&self) -> TxOut { 107 | TxOut { 108 | value: self.amount, 109 | script_pubkey: self.script.clone().into(), 110 | } 111 | } 112 | 113 | pub fn into_txout(self) -> TxOut { 114 | TxOut { 115 | value: self.amount, 116 | script_pubkey: self.script.into(), 117 | } 118 | } 119 | 120 | pub fn split(self) -> (OutputV0, TxOut) { 121 | ( 122 | OutputV0 { 123 | redeem_script: self.redeem_script.map(Into::into), 124 | witness_script: self.witness_script.map(Into::into), 125 | bip32_derivation: self.bip32_derivation, 126 | tap_internal_key: self.tap_internal_key, 127 | tap_tree: self.tap_tree, 128 | tap_key_origins: self.tap_key_origins, 129 | proprietary: self.proprietary, 130 | unknown: self.unknown, 131 | }, 132 | TxOut { 133 | value: self.amount, 134 | script_pubkey: self.script.into(), 135 | }, 136 | ) 137 | } 138 | } 139 | 140 | impl From for OutputV0 { 141 | fn from(output: Output) -> Self { 142 | OutputV0 { 143 | redeem_script: output.redeem_script.map(Into::into), 144 | witness_script: output.witness_script.map(Into::into), 145 | bip32_derivation: output.bip32_derivation, 146 | tap_internal_key: output.tap_internal_key, 147 | tap_tree: output.tap_tree, 148 | tap_key_origins: output.tap_key_origins, 149 | proprietary: output.proprietary, 150 | unknown: output.unknown, 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /psbt/src/p2c.rs: -------------------------------------------------------------------------------- 1 | // Wallet-level libraries for bitcoin protocol by LNP/BP Association 2 | // 3 | // Written in 2020-2022 by 4 | // Dr. Maxim Orlovsky 5 | // 6 | // This software is distributed without any warranty. 7 | // 8 | // You should have received a copy of the Apache-2.0 License 9 | // along with this software. 10 | // If not, see . 11 | 12 | // TODO: Relocate to BP DBC library 13 | 14 | //! Processing proprietary PSBT keys related to pay-to-contract (P2C) 15 | //! commitments. 16 | 17 | use amplify::Slice32; 18 | use bitcoin::secp256k1; 19 | use bitcoin::secp256k1::PublicKey; 20 | 21 | use crate::raw::ProprietaryKey; 22 | use crate::Input; 23 | 24 | pub const PSBT_P2C_PREFIX: &[u8] = b"P2C"; 25 | pub const PSBT_IN_P2C_TWEAK: u8 = 0; 26 | 27 | impl Input { 28 | /// Adds information about DBC P2C public key to PSBT input 29 | pub fn set_p2c_tweak(&mut self, pubkey: PublicKey, tweak: Slice32) { 30 | let mut value = pubkey.serialize().to_vec(); 31 | value.extend(&tweak[..]); 32 | self.proprietary.insert( 33 | ProprietaryKey { 34 | prefix: PSBT_P2C_PREFIX.to_vec(), 35 | subtype: PSBT_IN_P2C_TWEAK, 36 | key: vec![], 37 | }, 38 | value, 39 | ); 40 | } 41 | 42 | /// Finds a tweak for the provided bitcoin public key, if is known 43 | pub fn p2c_tweak(&self, pk: PublicKey) -> Option { 44 | self.proprietary.iter().find_map( 45 | |( 46 | ProprietaryKey { 47 | prefix, 48 | subtype, 49 | key, 50 | }, 51 | value, 52 | )| { 53 | if prefix.as_slice() == PSBT_P2C_PREFIX 54 | && *subtype == PSBT_IN_P2C_TWEAK 55 | && key == &Vec::::new() 56 | && value.len() == 33 + 32 57 | { 58 | secp256k1::PublicKey::from_slice(&value[..33]) 59 | .ok() 60 | .and_then(|pubkey| { 61 | if pk == pubkey { 62 | Slice32::from_slice(&value[33..]) 63 | } else { 64 | None 65 | } 66 | }) 67 | } else { 68 | None 69 | } 70 | }, 71 | ) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /psbt/src/proprietary.rs: -------------------------------------------------------------------------------- 1 | // Wallet-level libraries for bitcoin protocol by LNP/BP Association 2 | // 3 | // Written in 2020-2022 by 4 | // Dr. Maxim Orlovsky 5 | // 6 | // This software is distributed without any warranty. 7 | // 8 | // You should have received a copy of the Apache-2.0 License 9 | // along with this software. 10 | // If not, see . 11 | 12 | use core::fmt::{self, Display, Formatter}; 13 | use core::str::FromStr; 14 | 15 | use amplify::hex::{FromHex, ToHex}; 16 | 17 | use crate::raw::ProprietaryKey; 18 | 19 | #[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, Display, Error)] 20 | #[display(doc_comments)] 21 | pub enum ProprietaryKeyError { 22 | /// incorrect proprietary key location `{0}`; allowed location formats are 23 | /// `input(X)`, `output(X)` and `global`, where `X` is a 16-bit decimal 24 | /// integer. 25 | WrongLocation(String), 26 | 27 | /// incorrect proprietary key type definition `{0}`. 28 | /// 29 | /// Type definition must start with a ket prefix in form of a short ASCII 30 | /// string, followed by a key subtype represented by a 8-bit decimal integer 31 | /// in parentheses without whitespacing. Example: `DBC(5)` 32 | WrongType(String), 33 | 34 | /// incorrect proprietary key format `{0}`. 35 | /// 36 | /// Proprietary key descriptor must consists of whitespace-separated three 37 | /// parts: 38 | /// 1) key location, in form of `input(no)`, `output(no)`, or `global`; 39 | /// 2) key type, in form of `prefix(no)`; 40 | /// 3) key-value pair, in form of `key:value`, where both key and value must 41 | /// be hexadecimal bytestrings; one of them may be omitted (for instance, 42 | /// `:value` or `key:`). 43 | /// 44 | /// If the proprietary key does not have associated data, the third part of 45 | /// the descriptor must be fully omitted. 46 | WrongFormat(String), 47 | 48 | /// input at index {0} exceeds the number of inputs {1} 49 | InputOutOfRange(u16, usize), 50 | 51 | /// output at index {0} exceeds the number of outputs {1} 52 | OutputOutOfRange(u16, usize), 53 | } 54 | 55 | #[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, Display)] 56 | pub enum ProprietaryKeyLocation { 57 | #[display("global")] 58 | Global, 59 | 60 | #[display("input({0})")] 61 | Input(u16), 62 | 63 | #[display("output({0})")] 64 | Output(u16), 65 | } 66 | 67 | impl FromStr for ProprietaryKeyLocation { 68 | type Err = ProprietaryKeyError; 69 | 70 | fn from_str(s: &str) -> Result { 71 | let mut split = s.trim_end_matches(')').split('('); 72 | match ( 73 | split.next(), 74 | split.next().map(u16::from_str).transpose().ok().flatten(), 75 | split.next(), 76 | ) { 77 | (Some("global"), None, _) => Ok(ProprietaryKeyLocation::Global), 78 | (Some("input"), Some(pos), None) => Ok(ProprietaryKeyLocation::Input(pos)), 79 | (Some("output"), Some(pos), None) => Ok(ProprietaryKeyLocation::Output(pos)), 80 | _ => Err(ProprietaryKeyError::WrongLocation(s.to_owned())), 81 | } 82 | } 83 | } 84 | 85 | #[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, Display)] 86 | #[display("{prefix}({subtype})")] 87 | pub struct ProprietaryKeyType { 88 | pub prefix: String, 89 | pub subtype: u8, 90 | } 91 | 92 | impl FromStr for ProprietaryKeyType { 93 | type Err = ProprietaryKeyError; 94 | 95 | fn from_str(s: &str) -> Result { 96 | let mut split = s.trim_end_matches(')').split('('); 97 | match ( 98 | split.next().map(str::to_owned), 99 | split.next().map(u8::from_str).transpose().ok().flatten(), 100 | split.next(), 101 | ) { 102 | (Some(prefix), Some(subtype), None) => Ok(ProprietaryKeyType { prefix, subtype }), 103 | _ => Err(ProprietaryKeyError::WrongType(s.to_owned())), 104 | } 105 | } 106 | } 107 | 108 | // --proprietary-key "input(1) DBC(1) 8536ba03:~" 109 | #[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug)] 110 | pub struct ProprietaryKeyDescriptor { 111 | pub location: ProprietaryKeyLocation, 112 | pub ty: ProprietaryKeyType, 113 | pub key: Option>, 114 | pub value: Option>, 115 | } 116 | 117 | impl From for ProprietaryKey { 118 | fn from(key: ProprietaryKeyDescriptor) -> Self { ProprietaryKey::from(&key) } 119 | } 120 | 121 | impl From<&ProprietaryKeyDescriptor> for ProprietaryKey { 122 | fn from(key: &ProprietaryKeyDescriptor) -> Self { 123 | ProprietaryKey { 124 | prefix: key.ty.prefix.as_bytes().to_vec(), 125 | subtype: key.ty.subtype, 126 | key: key.key.as_ref().cloned().unwrap_or_default(), 127 | } 128 | } 129 | } 130 | 131 | impl FromStr for ProprietaryKeyDescriptor { 132 | type Err = ProprietaryKeyError; 133 | 134 | fn from_str(s: &str) -> Result { 135 | let err = ProprietaryKeyError::WrongFormat(s.to_owned()); 136 | let mut split = s.split_whitespace(); 137 | match ( 138 | split 139 | .next() 140 | .map(ProprietaryKeyLocation::from_str) 141 | .transpose()?, 142 | split.next().map(ProprietaryKeyType::from_str).transpose()?, 143 | split.next().and_then(|s| s.split_once(':')), 144 | split.next(), 145 | ) { 146 | (Some(location), Some(ty), None, None) => Ok(ProprietaryKeyDescriptor { 147 | location, 148 | ty, 149 | key: None, 150 | value: None, 151 | }), 152 | (Some(location), Some(ty), Some((k, v)), None) => Ok(ProprietaryKeyDescriptor { 153 | location, 154 | ty, 155 | key: if k.is_empty() { 156 | None 157 | } else { 158 | Some(Vec::from_hex(k).map_err(|_| err.clone())?) 159 | }, 160 | value: if v.is_empty() { 161 | None 162 | } else { 163 | Some(Vec::from_hex(v).map_err(|_| err)?) 164 | }, 165 | }), 166 | _ => Err(err), 167 | } 168 | } 169 | } 170 | 171 | impl Display for ProprietaryKeyDescriptor { 172 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 173 | write!(f, "{} {} ", self.location, self.ty)?; 174 | if (&self.key, &self.value) == (&None, &None) { 175 | return Ok(()); 176 | } 177 | write!( 178 | f, 179 | "{}:{}", 180 | self.key.as_deref().map(<[u8]>::to_hex).unwrap_or_default(), 181 | self.value 182 | .as_deref() 183 | .map(<[u8]>::to_hex) 184 | .unwrap_or_default() 185 | ) 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /psbt/src/sign/inmem.rs: -------------------------------------------------------------------------------- 1 | // Descriptor wallet library extending bitcoin & miniscript functionality 2 | // by LNP/BP Association (https://lnp-bp.org) 3 | // Written in 2020-2022 by 4 | // Dr. Maxim Orlovsky 5 | // 6 | // To the extent possible under law, the author(s) have dedicated all 7 | // copyright and related and neighboring rights to this software to 8 | // the public domain worldwide. This software is distributed without 9 | // any warranty. 10 | // 11 | // You should have received a copy of the Apache-2.0 License 12 | // along with this software. 13 | // If not, see . 14 | 15 | use std::cmp::Ordering; 16 | use std::collections::BTreeSet; 17 | use std::hash::Hasher; 18 | 19 | use bitcoin::secp256k1::{KeyPair, PublicKey, Secp256k1, SecretKey, Signing, XOnlyPublicKey}; 20 | use bitcoin::util::bip32::{DerivationPath, ExtendedPrivKey, ExtendedPubKey, Fingerprint}; 21 | use bitcoin::XpubIdentifier; 22 | use bitcoin_hd::{AccountStep, DerivationAccount, TerminalStep, XpubRef}; 23 | #[cfg(feature = "miniscript")] 24 | use bitcoin_hd::{Bip43, DerivationStandard}; 25 | #[cfg(feature = "miniscript")] 26 | use miniscript::Descriptor; 27 | 28 | use super::{SecretProvider, SecretProviderError}; 29 | 30 | /// Account-specific extended private key, kept in memory with information about 31 | /// account path derivation from the master key. 32 | /// 33 | /// Accounts are uniquially identified by a [`XpubIdentifier`] generated from 34 | /// an extended public key corresponding to the account-level extended private 35 | /// key (i.e. not master extended key, but a key at account-level derivation 36 | /// path). 37 | #[derive(Clone, Getters, Debug, Display)] 38 | #[display("m[{master_id}]/{derivation}=[{account_xpub}]")] 39 | pub struct MemorySigningAccount { 40 | master_id: XpubIdentifier, 41 | derivation: DerivationPath, 42 | account_xpriv: ExtendedPrivKey, 43 | account_xpub: ExtendedPubKey, 44 | } 45 | 46 | impl Ord for MemorySigningAccount { 47 | #[inline] 48 | fn cmp(&self, other: &Self) -> Ordering { self.account_xpub.cmp(&other.account_xpub) } 49 | } 50 | 51 | impl PartialOrd for MemorySigningAccount { 52 | #[inline] 53 | fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } 54 | } 55 | 56 | impl PartialEq for MemorySigningAccount { 57 | fn eq(&self, other: &Self) -> bool { self.account_xpub == other.account_xpub } 58 | } 59 | 60 | impl Eq for MemorySigningAccount {} 61 | 62 | impl std::hash::Hash for MemorySigningAccount { 63 | fn hash(&self, state: &mut H) { self.account_xpub.hash(state) } 64 | } 65 | 66 | impl MemorySigningAccount { 67 | #[inline] 68 | pub fn with( 69 | secp: &Secp256k1, 70 | master_id: XpubIdentifier, 71 | derivation: impl Into, 72 | account_xpriv: ExtendedPrivKey, 73 | ) -> MemorySigningAccount { 74 | MemorySigningAccount { 75 | master_id, 76 | derivation: derivation.into(), 77 | account_xpriv, 78 | account_xpub: ExtendedPubKey::from_priv(secp, &account_xpriv), 79 | } 80 | } 81 | 82 | #[inline] 83 | pub fn master_fingerprint(&self) -> Fingerprint { 84 | Fingerprint::from(&self.master_id[..4]) 85 | // TODO: Do a convertor from XpubIdentifier to Fingerprint in 86 | // rust-bitcoin 87 | } 88 | 89 | #[inline] 90 | pub fn account_id(&self) -> XpubIdentifier { self.account_xpub.identifier() } 91 | 92 | #[inline] 93 | pub fn account_fingerprint(&self) -> Fingerprint { self.account_xpub.fingerprint() } 94 | 95 | #[inline] 96 | pub fn derive_seckey( 97 | &self, 98 | secp: &Secp256k1, 99 | derivation: &DerivationPath, 100 | ) -> SecretKey { 101 | let xpriv = self 102 | .account_xpriv 103 | .derive_priv(secp, derivation) 104 | .expect("ExtendedPrivKey integrity issue"); 105 | xpriv.private_key 106 | } 107 | 108 | #[inline] 109 | pub fn derive_keypair( 110 | &self, 111 | secp: &Secp256k1, 112 | derivation: &DerivationPath, 113 | ) -> KeyPair { 114 | KeyPair::from_secret_key(secp, &self.derive_seckey(secp, derivation)) 115 | } 116 | 117 | #[inline] 118 | pub fn to_account(&self) -> DerivationAccount { 119 | DerivationAccount { 120 | master: XpubRef::Fingerprint(self.master_fingerprint()), 121 | account_path: self 122 | .derivation 123 | .into_iter() 124 | .copied() 125 | .map(AccountStep::try_from) 126 | .collect::>() 127 | .expect("ChildNumber is broken"), 128 | account_xpub: self.account_xpub, 129 | revocation_seal: None, 130 | terminal_path: vec![TerminalStep::Wildcard, TerminalStep::Wildcard].into(), 131 | } 132 | } 133 | 134 | #[cfg(feature = "miniscript")] 135 | pub fn recommended_descriptor(&self) -> Option> { 136 | let account = self.to_account(); 137 | Some(match Bip43::deduce(&self.derivation)? { 138 | Bip43::Bip44 => Descriptor::new_pkh(account), 139 | Bip43::Bip84 => Descriptor::new_wpkh(account).expect("miniscript descriptors broken"), 140 | Bip43::Bip49 => { 141 | Descriptor::new_sh_wpkh(account).expect("miniscript descriptors broken") 142 | } 143 | Bip43::Bip86 => { 144 | Descriptor::new_tr(account, None).expect("miniscript descriptors broken") 145 | } 146 | Bip43::Bip45 => Descriptor::new_sh_sortedmulti(1, vec![account]) 147 | .expect("miniscript descriptors broken"), 148 | Bip43::Bip48Nested => Descriptor::new_sh_sortedmulti(1, vec![account]) 149 | .expect("miniscript descriptors broken"), 150 | Bip43::Bip87 => Descriptor::new_sh_wsh_sortedmulti(1, vec![account]) 151 | .expect("miniscript descriptors broken"), 152 | _ => return None, 153 | }) 154 | } 155 | } 156 | 157 | /// Provider of signing keys which uses memory storage for extended 158 | /// account-specific private keys. 159 | #[derive(Debug)] 160 | pub struct MemoryKeyProvider<'secp, C> 161 | where 162 | C: Signing, 163 | { 164 | accounts: BTreeSet, 165 | secp: &'secp Secp256k1, 166 | /// Participate keys from this provider in musigs 167 | musig: bool, 168 | } 169 | 170 | impl<'secp, C> MemoryKeyProvider<'secp, C> 171 | where 172 | C: Signing, 173 | { 174 | pub fn with(secp: &'secp Secp256k1, musig: bool) -> Self { 175 | Self { 176 | accounts: default!(), 177 | secp, 178 | musig, 179 | } 180 | } 181 | 182 | #[inline] 183 | pub fn add_account(&mut self, account: MemorySigningAccount) -> bool { 184 | self.accounts.insert(account) 185 | } 186 | } 187 | 188 | impl<'secp, C> IntoIterator for &'secp MemoryKeyProvider<'secp, C> 189 | where 190 | C: Signing, 191 | { 192 | type Item = &'secp MemorySigningAccount; 193 | type IntoIter = std::collections::btree_set::Iter<'secp, MemorySigningAccount>; 194 | 195 | #[inline] 196 | fn into_iter(self) -> Self::IntoIter { self.accounts.iter() } 197 | } 198 | 199 | impl<'secp, C> SecretProvider for MemoryKeyProvider<'secp, C> 200 | where 201 | C: Signing, 202 | { 203 | #[inline] 204 | fn secp_context(&self) -> &Secp256k1 { self.secp } 205 | 206 | fn secret_key( 207 | &self, 208 | fingerprint: Fingerprint, 209 | derivation: &DerivationPath, 210 | pubkey: PublicKey, 211 | ) -> Result { 212 | for account in &self.accounts { 213 | let derivation = if account.account_fingerprint() == fingerprint { 214 | derivation.clone() 215 | } else if account.master_fingerprint() == fingerprint { 216 | let mut iter = account.derivation.into_iter(); 217 | let remaining_derivation = derivation 218 | .into_iter() 219 | .skip_while(|child| Some(*child) == iter.next()); 220 | let remaining_derivation = remaining_derivation.cloned().collect(); 221 | if iter.count() > 0 { 222 | continue; 223 | } 224 | remaining_derivation 225 | } else { 226 | continue; 227 | }; 228 | let seckey = account.derive_seckey(self.secp, &derivation); 229 | // We need to skip party flag 230 | if PublicKey::from_secret_key(self.secp, &seckey).serialize()[1..] 231 | != pubkey.serialize()[1..] 232 | { 233 | continue; 234 | } 235 | return Ok(seckey); 236 | } 237 | 238 | Err(SecretProviderError::AccountUnknown(fingerprint, pubkey)) 239 | } 240 | 241 | #[inline] 242 | fn key_pair( 243 | &self, 244 | fingerprint: Fingerprint, 245 | derivation: &DerivationPath, 246 | pubkey: XOnlyPublicKey, 247 | ) -> Result { 248 | let mut data: Vec = vec![0x02]; 249 | data.extend(pubkey.serialize().iter()); 250 | let pk = PublicKey::from_slice(&data).expect("fixed size slice"); 251 | let seckey = self.secret_key(fingerprint, derivation, pk)?; 252 | Ok(KeyPair::from_secret_key(self.secp, &seckey)) 253 | } 254 | 255 | #[inline] 256 | fn use_musig(&self) -> bool { self.musig } 257 | } 258 | -------------------------------------------------------------------------------- /psbt/src/sign/mod.rs: -------------------------------------------------------------------------------- 1 | // Descriptor wallet library extending bitcoin & miniscript functionality 2 | // by LNP/BP Association (https://lnp-bp.org) 3 | // Written in 2020-2022 by 4 | // Dr. Maxim Orlovsky 5 | // 6 | // To the extent possible under law, the author(s) have dedicated all 7 | // copyright and related and neighboring rights to this software to 8 | // the public domain worldwide. This software is distributed without 9 | // any warranty. 10 | // 11 | // You should have received a copy of the Apache-2.0 License 12 | // along with this software. 13 | // If not, see . 14 | 15 | //! Interfaces for signing PSBTs with key sign providers 16 | 17 | // TODO: Add Hash secret provider and hash secret satisfaction 18 | 19 | use bitcoin::secp256k1::{KeyPair, PublicKey, Secp256k1, SecretKey, Signing, XOnlyPublicKey}; 20 | use bitcoin::util::bip32::{DerivationPath, Fingerprint}; 21 | 22 | mod inmem; 23 | #[cfg(feature = "miniscript")] 24 | mod signer; 25 | 26 | pub use inmem::{MemoryKeyProvider, MemorySigningAccount}; 27 | #[cfg(feature = "miniscript")] 28 | pub use signer::{SignAll, SignError, SignInputError}; 29 | 30 | /// Errors returned by secret providers (see [`SecretProvider`]) 31 | #[derive( 32 | Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, Error, Display, From 33 | )] 34 | #[display(doc_comments)] 35 | pub enum SecretProviderError { 36 | /// the account corresponding to the given fingerprint {0} that can 37 | /// generate public key {1} is unknown to the key provider 38 | AccountUnknown(Fingerprint, PublicKey), 39 | } 40 | 41 | /// Structures extended private keys after their corresponding ids ("account 42 | /// ids") and performs derivation to produce corresponding public keys under a 43 | /// given account 44 | pub trait SecretProvider { 45 | /// Returns [`Secp256k1`] context object used by the provider 46 | fn secp_context(&self) -> &Secp256k1; 47 | 48 | /// Returns secret key matching provided public key by iterating over all 49 | /// extended private keys having the provided fingerprint. 50 | /// 51 | /// # Error 52 | /// 53 | /// Errors with [`SecretProviderError::AccountUnknown`] if none of the known 54 | /// extended private keys has the specified fingerprint _and_ can be 55 | /// derived with a given path into the provided public key. 56 | /// 57 | /// NB: This does not imply that the given key can't be derived from know 58 | /// extended public keys, but with a differend derivation. I.e. the function 59 | /// will error just because fingerprint does not match correct extended 60 | /// public key - or derivation path contains a erorr. 61 | fn secret_key( 62 | &self, 63 | fingerprint: Fingerprint, 64 | derivation: &DerivationPath, 65 | pubkey: PublicKey, 66 | ) -> Result; 67 | 68 | /// Returns BIP-340 key pair matching provided public key by iterating over 69 | /// all extended private keys having the provided fingerprint. 70 | /// 71 | /// # Error 72 | /// 73 | /// Errors with [`SecretProviderError::AccountUnknown`] if none of the known 74 | /// extended private keys has the specified fingerprint _and_ can be 75 | /// derived with a given path into the provided public key. 76 | /// 77 | /// NB: This does not imply that the given key can't be derived from know 78 | /// extended public keys, but with a differend derivation. I.e. the function 79 | /// will error just because fingerprint does not match correct extended 80 | /// public key - or derivation path contains a erorr. 81 | fn key_pair( 82 | &self, 83 | fingerprint: Fingerprint, 84 | derivation: &DerivationPath, 85 | pubkey: XOnlyPublicKey, 86 | ) -> Result; 87 | 88 | /// Returns whether keys returned by this provider can be used for creating 89 | /// aggregated Schnorr signatures. 90 | fn use_musig(&self) -> bool; 91 | } 92 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "stable" 3 | -------------------------------------------------------------------------------- /slip132/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "slip132" 3 | version = { workspace = true } 4 | license = { workspace = true } 5 | authors = { workspace = true } 6 | description = "Bitcoin SLIP-132 standard implementation (parsing custom xpub/xpriv key formats)" 7 | repository = { workspace = true } 8 | homepage = { workspace = true } 9 | keywords = ["bitcoin", "wallet", "cryptocurrency", "slip132", "bip32"] 10 | categories = { workspace = true } 11 | readme = { workspace = true } 12 | edition = { workspace = true } 13 | rust-version = { workspace = true } 14 | exclude = [] 15 | 16 | [lib] 17 | name = "slip132" 18 | path = "src/lib.rs" 19 | 20 | [dependencies] 21 | amplify = { workspace = true } 22 | strict_encoding = { workspace = true, optional = true } 23 | bitcoin = { workspace = true } 24 | serde_crate = { package = "serde", version = "1", features = ["derive"], optional = true } 25 | serde_with = { version = "2.3", features = ["hex"], optional = true } 26 | 27 | [features] 28 | default = [] 29 | all = ["serde", "strict_encoding"] 30 | serde = ["serde_crate", "serde_with"] 31 | -------------------------------------------------------------------------------- /slip132/README.md: -------------------------------------------------------------------------------- 1 | # Rust SLIP-132 library 2 | 3 | Bitcoin SLIP-132 standard implementation for parsing custom xpub/xpriv key 4 | formats -------------------------------------------------------------------------------- /src/cli/mod.rs: -------------------------------------------------------------------------------- 1 | // Descriptor wallet library extending bitcoin & miniscript functionality 2 | // by LNP/BP Association (https://lnp-bp.org) 3 | // Written in 2020-2022 by 4 | // Dr. Maxim Orlovsky 5 | // 6 | // To the extent possible under law, the author(s) have dedicated all 7 | // copyright and related and neighboring rights to this software to 8 | // the public domain worldwide. This software is distributed without 9 | // any warranty. 10 | // 11 | // You should have received a copy of the Apache-2.0 License 12 | // along with this software. 13 | // If not, see . 14 | 15 | //! Module exclusively used only by binary command-line tools of the crate. 16 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // Descriptor wallet library extending bitcoin & miniscript functionality 2 | // by LNP/BP Association (https://lnp-bp.org) 3 | // Written in 2020-2022 by 4 | // Dr. Maxim Orlovsky 5 | // 6 | // To the extent possible under law, the author(s) have dedicated all 7 | // copyright and related and neighboring rights to this software to 8 | // the public domain worldwide. This software is distributed without 9 | // any warranty. 10 | // 11 | // You should have received a copy of the Apache-2.0 License 12 | // along with this software. 13 | // If not, see . 14 | 15 | //! Descriptor wallet library extending bitcoin & miniscript functionality. 16 | 17 | // Coding conventions 18 | #![recursion_limit = "256"] 19 | #![deny(dead_code, missing_docs, warnings)] 20 | 21 | pub extern crate bitcoin_hd as hd; 22 | pub extern crate bitcoin_onchain as onchain; 23 | pub extern crate descriptors; 24 | pub extern crate psbt; 25 | pub extern crate slip132; 26 | 27 | #[cfg(feature = "cli")] 28 | pub(crate) mod cli; 29 | 30 | pub mod lex_order { 31 | //! Lexicographic sorting functions. 32 | #[deprecated(since = "0.6.1", note = "Use `wallet::lex_order` instead")] 33 | pub use psbt::lex_order; 34 | pub use psbt::lex_order::*; 35 | } 36 | --------------------------------------------------------------------------------