diff --git a/.github/workflows/Dockerfile b/.github/workflows/Dockerfile
index 5e54b9ece..93dad7e5c 100644
--- a/.github/workflows/Dockerfile
+++ b/.github/workflows/Dockerfile
@@ -1,14 +1,30 @@
# An image derived from ledgerhq/speculos but also containing the bitcoin-core binaries
+# compiled from the master branch
FROM ghcr.io/ledgerhq/speculos:latest
-# install curl
-RUN apt update -y && apt install -y curl
+# install git and curl
+RUN apt update -y && apt install -y git curl
-# download bitcoin-core and decompress it to /bitcoin
-RUN curl -o /tmp/bitcoin.tar.gz https://bitcoin.org/bin/bitcoin-core-22.0/bitcoin-22.0-x86_64-linux-gnu.tar.gz && \
- tar -xf /tmp/bitcoin.tar.gz -C / && \
- mv /bitcoin-22.0 /bitcoin
+# install autotools bitcoin-core build dependencies
+RUN apt install -y automake autotools-dev bsdmainutils build-essential ccache git libboost-dev libboost-filesystem-dev libboost-system-dev libboost-test-dev libevent-dev libminiupnpc-dev libnatpmp-dev libqt5gui5 libqt5core5a libqt5dbus5 libsqlite3-dev libtool libzmq3-dev pkg-config python3 qttools5-dev qttools5-dev-tools qtwayland5 systemtap-sdt-dev
+
+# clone bitcoin-core from github and compile it
+RUN cd / && \
+ git clone --depth=1 https://github.com/bitcoin/bitcoin.git && \
+ cd bitcoin && \
+ ./autogen.sh && \
+ ./configure --enable-suppress-external-warnings && \
+ make -j "$(($(nproc)+1))" && \
+ mkdir bin && \
+ cp src/bitcoind src/bitcoin-cli src/bitcoin-tx src/bitcoin-util src/bitcoin-wallet ./bin
+
+
+FROM ghcr.io/ledgerhq/speculos:latest
+COPY --from=0 /bitcoin/bin /bitcoin/bin
+
+# install runtime dependencies for bitcoind
+RUN apt update -y && apt install -y libminiupnpc-dev libminiupnpc-dev libnatpmp-dev libevent-dev libzmq3-dev
# Add bitcoin binaries to path
ENV PATH=/bitcoin/bin:$PATH
diff --git a/.github/workflows/build_and_functional_tests.yml b/.github/workflows/build_and_functional_tests.yml
new file mode 100644
index 000000000..7e57e5c65
--- /dev/null
+++ b/.github/workflows/build_and_functional_tests.yml
@@ -0,0 +1,34 @@
+name: Build and run functional tests using ragger through reusable workflow
+
+# This workflow will build the app and then run functional tests using the Ragger framework upon Speculos emulation.
+# It calls a reusable workflow developed by Ledger's internal developer team to build the application and upload the
+# resulting binaries.
+# It then calls another reusable workflow to run the Ragger tests on the compiled application binary.
+#
+# While this workflow is optional, having functional testing on your application is mandatory and this workflow and
+# tooling environment is meant to be easy to use and adapt after forking your application
+
+on:
+ workflow_dispatch:
+ push:
+ branches:
+ - master
+ - main
+ - develop
+ pull_request:
+
+jobs:
+ build_application:
+ name: Build application using the reusable workflow
+ uses: LedgerHQ/ledger-app-workflows/.github/workflows/reusable_build.yml@v1
+ with:
+ upload_app_binaries_artifact: "compiled_app_binaries"
+ flags: "DEBUG=0 COIN=bitcoin_testnet"
+
+ ragger_tests:
+ name: Run ragger tests using the reusable workflow
+ needs: build_application
+ uses: LedgerHQ/ledger-app-workflows/.github/workflows/reusable_ragger_tests.yml@v1
+ with:
+ download_app_binaries_artifact: "compiled_app_binaries"
+
diff --git a/.github/workflows/builder-image-workflow.yml b/.github/workflows/builder-image-workflow.yml
index b5ab35ea1..11128cc20 100644
--- a/.github/workflows/builder-image-workflow.yml
+++ b/.github/workflows/builder-image-workflow.yml
@@ -1,13 +1,11 @@
name: Build custom speculos-bitcoin image
on:
+ workflow_dispatch:
push:
branches:
- master
- develop
- paths:
- - .github/workflows/builder-image-workflow.yml
- - .github/workflows/Dockerfile
jobs:
build:
diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml
index bbabb75f9..910682551 100644
--- a/.github/workflows/ci-workflow.yml
+++ b/.github/workflows/ci-workflow.yml
@@ -13,7 +13,20 @@ on:
jobs:
job_build:
- name: Compilation
+ name: Compilation for NanoS, X, S+, and Stax
+
+ strategy:
+ matrix:
+ include:
+ - model: nanos
+ SDK: "$NANOS_SDK"
+ - model: nanox
+ SDK: "$NANOX_SDK"
+ - model: nanosp
+ SDK: "$NANOSP_SDK"
+ - model: stax
+ SDK: "$STAX_SDK"
+
runs-on: ubuntu-latest
container:
@@ -25,29 +38,21 @@ jobs:
- name: Build
run: |
- make DEBUG=0 COIN=bitcoin && mv bin/ bitcoin-bin/
- make clean
- make DEBUG=0 COIN=bitcoin_testnet && mv bin/ bitcoin-testnet-bin/
+ make DEBUG=0 COIN=bitcoin BOLOS_SDK=${{ matrix.SDK }} && mv bin/ bitcoin-bin/
make clean
- make DEBUG=0 COIN=bitcoin_testnet_lib && mv bin/ bitcoin-testnet-lib-bin/
+ make DEBUG=0 COIN=bitcoin_testnet BOLOS_SDK=${{ matrix.SDK }} && mv bin/ bitcoin-testnet-bin/
- name: Upload Bitcoin app binary
uses: actions/upload-artifact@v2
with:
- name: bitcoin-app
+ name: bitcoin-app-${{ matrix.model }}
path: bitcoin-bin
- name: Upload Bitcoin Testnet app binary
uses: actions/upload-artifact@v2
with:
- name: bitcoin-testnet-app
+ name: bitcoin-testnet-app-${{ matrix.model }}
path: bitcoin-testnet-bin
- - name: Upload Bitcoin Testnet app binary (lib version)
- uses: actions/upload-artifact@v2
- with:
- name: bitcoin-testnet-lib-app
- path: bitcoin-testnet-lib-bin
-
job_unit_test:
name: Unit test
needs: job_build
@@ -78,10 +83,10 @@ jobs:
path: unit-tests/coverage
- name: Upload to codecov.io
- uses: codecov/codecov-action@v1
+ uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
- file: ./unit-tests/coverage.info
+ files: ./unit-tests/coverage.info
flags: unittests
name: codecov-app-bitcoin
fail_ci_if_error: true
@@ -95,8 +100,16 @@ jobs:
name: documentation
path: doc/html
- job_test:
- name: Tests
+ job_test_mainnet:
+ name: Tests on mainnet
+ strategy:
+ matrix:
+ include:
+ - model: nanos
+ - model: nanox
+ - model: nanosp
+ - model: stax
+
needs: job_build
runs-on: ubuntu-latest
@@ -115,20 +128,20 @@ jobs:
- name: Clone
uses: actions/checkout@v2
- - name: Download Bitcoin Testnet app binary
+ - name: Download Bitcoin app binary
uses: actions/download-artifact@v2
with:
- name: bitcoin-testnet-app
+ name: bitcoin-app-${{matrix.model}}
path: bin
- name: Run tests
run: |
- cd tests
+ cd tests_mainnet
pip install -r requirements.txt
- PYTHONPATH=$PYTHONPATH:/speculos pytest --headless
+ PYTHONPATH=$PYTHONPATH:/speculos pytest --headless --model=${{ matrix.model }} --timeout=300
- job_test_mainnet:
- name: Tests on mainnet
+ job_test_python_lib_legacyapp:
+ name: Tests with the Python library and legacy Bitcoin app
needs: job_build
runs-on: ubuntu-latest
@@ -147,17 +160,12 @@ jobs:
- name: Clone
uses: actions/checkout@v2
- - name: Download Bitcoin app binary
- uses: actions/download-artifact@v2
- with:
- name: bitcoin-app
- path: bin
-
- name: Run tests
run: |
- cd tests_mainnet
+ cd bitcoin_client/tests
pip install -r requirements.txt
- PYTHONPATH=$PYTHONPATH:/speculos pytest --headless
+ PYTHONPATH=$PYTHONPATH:/speculos pytest --headless --timeout=300
+
job_test_js_lib:
name: Tests with the JS library
@@ -191,7 +199,7 @@ jobs:
- name: Download Bitcoin Testnet app binary
uses: actions/download-artifact@v2
with:
- name: bitcoin-testnet-app
+ name: bitcoin-testnet-app-nanos
path: bin
- name: Run tests
@@ -200,74 +208,20 @@ jobs:
yarn install
LOG_SPECULOS=1 LOG_APDUS=1 SPECULOS="/speculos/speculos.py" yarn test
- job_test_legacy_native:
- name: Legacy tests
- needs: job_build
- runs-on: ubuntu-latest
-
- container:
- image: ghcr.io/ledgerhq/app-bitcoin-new/speculos-bitcoin:latest
- ports:
- - 1234:1234
- - 9999:9999
- - 40000:40000
- - 41000:41000
- - 42000:42000
- - 43000:43000
- options: --entrypoint /bin/bash
- steps:
- - name: Clone
- uses: actions/checkout@v2
-
- - name: Download Bitcoin Testnet app binary
- uses: actions/download-artifact@v2
- with:
- name: bitcoin-testnet-app
- path: tests-legacy/bitcoin-testnet-bin
-
- - name: Run tests
- run: |
- cd tests-legacy
- pip install -r requirements.txt
- PATH=$PATH:/speculos pytest
-
-
- job_test_legacy_lib:
- name: Legacy tests (library)
+ job_test_rust_client:
+ name: Tests for rust client library
needs: job_build
runs-on: ubuntu-latest
container:
- image: ghcr.io/ledgerhq/app-bitcoin-new/speculos-bitcoin:latest
- ports:
- - 1234:1234
- - 9999:9999
- - 40000:40000
- - 41000:41000
- - 42000:42000
- - 43000:43000
- options: --entrypoint /bin/bash
+ image: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder:latest
steps:
- name: Clone
uses: actions/checkout@v2
- - name: Download Bitcoin app binary
- uses: actions/download-artifact@v2
- with:
- name: bitcoin-app
- path: tests-legacy/bitcoin-bin
-
- - name: Download Bitcoin Testnet app binary (library version)
- uses: actions/download-artifact@v2
- with:
- name: bitcoin-testnet-lib-app
- path: tests-legacy/bitcoin-testnet-bin
-
-
- name: Run tests
run: |
- cd tests-legacy
- pip install -r requirements.txt
- PATH=$PATH:/speculos pytest
+ cd bitcoin_client_rs/
+ cargo test --no-default-features --features="async"
diff --git a/.github/workflows/codeql-workflow.yml b/.github/workflows/codeql-workflow.yml
new file mode 100644
index 000000000..05c758ae5
--- /dev/null
+++ b/.github/workflows/codeql-workflow.yml
@@ -0,0 +1,47 @@
+name: "CodeQL"
+
+on:
+ workflow_dispatch:
+ push:
+ branches:
+ - master
+ - develop
+ pull_request:
+ branches:
+ - master
+ - develop
+
+jobs:
+ analyse:
+ name: CodeQL Analyse of boilerplate application
+ strategy:
+ matrix:
+ include:
+ - SDK: "$NANOS_SDK"
+ artifact: boilerplate-app-nanoS
+ - SDK: "$NANOX_SDK"
+ artifact: boilerplate-app-nanoX
+ - SDK: "$NANOSP_SDK"
+ artifact: boilerplate-app-nanoSP
+ language: [ 'cpp' ]
+ runs-on: ubuntu-latest
+ container:
+ image: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder-legacy:latest
+
+ steps:
+ - name: Clone
+ uses: actions/checkout@v3
+
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v2
+ with:
+ languages: ${{ matrix.language }}
+ queries: security-and-quality
+
+ - name: Build
+ run: |
+ make BOLOS_SDK=${{ matrix.SDK }}
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v2
+
diff --git a/.github/workflows/guidelines-enforcer.yml b/.github/workflows/guidelines-enforcer.yml
new file mode 100644
index 000000000..c154d6cf5
--- /dev/null
+++ b/.github/workflows/guidelines-enforcer.yml
@@ -0,0 +1,22 @@
+name: Ensure compliance with Ledger guidelines
+
+# This workflow is mandatory in all applications
+# It calls a reusable workflow guidelines_enforcer developed by Ledger's internal developer team.
+# The successful completion of the reusable workflow is a mandatory step for an app to be available on the Ledger
+# application store.
+#
+# More information on the guidelines can be found in the repository:
+# LedgerHQ/ledger-app-workflows/
+
+on:
+ workflow_dispatch:
+ push:
+ branches:
+ - master
+ - develop
+ pull_request:
+
+jobs:
+ guidelines_enforcer:
+ name: Call Ledger guidelines_enforcer
+ uses: LedgerHQ/ledger-app-workflows/.github/workflows/reusable_guidelines_enforcer.yml@v1
diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml
index 693bcc4a8..6269b0103 100644
--- a/.github/workflows/sonarcloud.yml
+++ b/.github/workflows/sonarcloud.yml
@@ -12,55 +12,37 @@ on:
types: [opened, synchronize, reopened]
jobs:
- build:
- name: Build
+ sonarcloud:
runs-on: ubuntu-latest
container:
- image: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder:latest
+ image: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder-legacy:latest
+
env:
- SONAR_SCANNER_VERSION: 4.4.0.2170
- SONAR_SERVER_URL: "https://sonarcloud.io"
BUILD_WRAPPER_OUT_DIR: build_wrapper_output_directory # Directory where build-wrapper output will be placed
steps:
- - uses: actions/checkout@v2
- with:
- fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
- - name: Set up JDK 11
- uses: actions/setup-java@v1
- with:
- java-version: 11
- - name: Download and set up sonar-scanner
- env:
- SONAR_SCANNER_DOWNLOAD_URL: https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-${{ env.SONAR_SCANNER_VERSION }}-linux.zip
- run: |
- apt-get update -y
- apt-get upgrade -y
- apt-get install -y
- curl -sL https://deb.nodesource.com/setup_16.x | bash -
- apt-get install -y gcovr nodejs unzip
- mkdir -p $HOME/.sonar
- curl -sSLo $HOME/.sonar/sonar-scanner.zip ${{ env.SONAR_SCANNER_DOWNLOAD_URL }}
- unzip -o $HOME/.sonar/sonar-scanner.zip -d $HOME/.sonar/
- echo "$HOME/.sonar/sonar-scanner-${{ env.SONAR_SCANNER_VERSION }}-linux/bin" >> $GITHUB_PATH
- - name: Download and set up build-wrapper
- env:
- BUILD_WRAPPER_DOWNLOAD_URL: ${{ env.SONAR_SERVER_URL }}/static/cpp/build-wrapper-linux-x86.zip
- run: |
- curl -sSLo $HOME/.sonar/build-wrapper-linux-x86.zip ${{ env.BUILD_WRAPPER_DOWNLOAD_URL }}
- unzip -o $HOME/.sonar/build-wrapper-linux-x86.zip -d $HOME/.sonar/
- echo "$HOME/.sonar/build-wrapper-linux-x86" >> $GITHUB_PATH
- - name: Generate code coverage
- run: |
- cd unit-tests/
- cmake -Bbuild -H. && make -C build
- make -C build test
- gcovr --root .. --sonarqube coverage.xml
- - name: Run build-wrapper
- run: |
+ - uses: actions/checkout@v3
+ with:
+ # Disabling shallow clone is recommended for improving relevancy of reporting
+ fetch-depth: 0
+ - name: Install dependencies
+ run: |
+ apt-get update -y
+ apt-get upgrade -y
+ DEBIAN_FRONTEND=noninteractive apt-get install -y tzdata
+ apt-get install -y libcmocka-dev gcovr unzip
+ - name: Install sonar-scanner and build-wrapper
+ uses: sonarsource/sonarcloud-github-c-cpp@v2
+ - name: Generate code coverage
+ run: |
+ cd unit-tests/
+ cmake -Bbuild -H. && make -C build
+ make -C build test
+ gcovr --root .. --sonarqube coverage.xml
+ - name: Run build-wrapper
+ run: |
build-wrapper-linux-x86-64 --out-dir ${{ env.BUILD_WRAPPER_OUT_DIR }} make clean all
- - name: Run sonar-scanner
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- run: |
- sonar-scanner --define sonar.host.url="${{ env.SONAR_SERVER_URL }}" --define sonar.cfamily.build-wrapper-output="${{ env.BUILD_WRAPPER_OUT_DIR }}"
\ No newline at end of file
+ - name: Run sonar-scanner
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+ run: sonar-scanner --define sonar.cfamily.build-wrapper-output="${{ env.BUILD_WRAPPER_OUT_DIR }}" #Consult https://docs.sonarcloud.io/advanced-setup/ci-based-analysis/sonarscanner-cli/ for more information and options
\ No newline at end of file
diff --git a/.github/workflows/swap-ci-workflow.yml b/.github/workflows/swap-ci-workflow.yml
new file mode 100644
index 000000000..85225ecac
--- /dev/null
+++ b/.github/workflows/swap-ci-workflow.yml
@@ -0,0 +1,16 @@
+name: Swap functional tests
+
+on:
+ workflow_dispatch:
+ push:
+ branches:
+ - master
+ - develop
+ pull_request:
+
+jobs:
+ job_functional_tests:
+ uses: LedgerHQ/app-exchange/.github/workflows/reusable_swap_functional_tests.yml@develop
+ with:
+ branch_for_bitcoin: ${{ github.ref }}
+ test_filter: '"btc or bitcoin or Bitcoin"'
diff --git a/.gitignore b/.gitignore
index 97a1d7083..3a33f742d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,11 +12,9 @@ unit-tests/build/
unit-tests/coverage/
unit-tests/coverage.info
-tests-legacy/bitcoin-bin
-tests-legacy/bitcoin-testnet-bin
-
# temporary folder used during tests
tests/.test_bitcoin
+tests/snapshots-tmp
# Fuzzing
fuzzing/build/
@@ -35,13 +33,12 @@ __pycache__/
.eggs/
.python-version
.pytest_cache
+venv/
+ledger/
# Doxygen
doc/html
doc/latex
-# Python virtual environment
-.venv*
-
# Mac directory metadata
-.DS_Store
+.DS_Store
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9a2e6974b..65d405494 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,106 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
Dates are in `dd-mm-yyyy` format.
+## [2.2.3] - 06-05-2024
+
+### Added
+
+- Support for signing transactions with `OP_RETURN` outputs extended to up to 5 push opcodes, instead of a single one.
+
+## [2.2.2] - 08-04-2024
+
+### Added
+
+- During wallet policy registration, the app will recognize and explicitly label as `dummy` any extended public key whose compressed pubkey is `0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0`. This is useful especially for taproot miniscript policies which do not intend to use keypath spending.
+
+### Changed
+
+- Message signing: will now show the full text of the message, instead of its hash. If the message is too long (over 640 characters) or it contains non-printable characters (not in the range `0x20..0x70`, inclusive), then the SHA256 hash will be shown, as in previous versions of the app.
+- Transaction signing: changed the wording to make the ux slightly simpler and clearer.
+
+## [2.2.1] - 18-03-2024
+
+### Fixed
+
+- Signing failure for certain taproot policies in versions 2.1.2, 2.1.3 and 2.2.0: returned tapleaf hashes (and corresponding signatures) are incorrect if the descriptor template has a derivation path not ending for `/**` or `/<0;1>/*` for that key.
+
+## [2.2.0] - 29-01-2024
+
+### Added
+
+- 🥕 Support for miniscript on taproot wallet policies.
+- Warning if the fees are above 10% of the amount, if the total amount is above 10000 sats (0.0001 ₿).
+
+### Changed
+
+- Increased limits for the maximum in-memory size of wallet policies.
+
+## [2.1.3] - 21-06-2023
+
+### Changed
+
+- Improved UX for self-transfers, that is, transactions where all the outputs are change outputs.
+- Outputs containing a single `OP_RETURN` (without any data push) can now be signed in order to support [BIP-0322](https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki) implementations.
+
+
+### Fixed
+
+- Wrong address generation for miniscript policies containing an unusual `thresh(1,X)` fragment (that is, with threshold 1, and a single condition). This should not happen in practice, as the policy is redundant for just `X`. Client libraries have been updated to detect and prevent usage of these policies.
+- Resolved a slight regression in signing performance introduced in v2.1.2.
+
+## [2.1.2] - 03-04-2023
+
+### Added
+
+- 🥕 Initial support for taproot scripts; taproot trees support up to 8 leaves, and the only supported scripts in tapleaves are `pk`, `multi_a` and `sortedmulti_a`.
+
+### Fixed
+
+- Miniscript policies containing an `a:` fragment returned an incorrect address in versions `2.1.0` and `2.1.1` of the app. The **upgrade is strongly recommended** for users of miniscript wallets.
+- The app will now reject showing or returning an address for a wallet policy if the `address_index` is larger than or equal to `2147483648`; previous version would return an address for a hardened derivation, which is undesirable.
+- Nested segwit transactions (P2SH-P2WPKH and P2SH-P2WSH) can now be signed (with a warning) if the PSBT contains the witness-utxo but no non-witness-utxo. This aligns their behavior to other types of Segwitv0 transactions since version 2.0.6.
+
+## [2.1.1] - 23-01-2023
+
+### Changed
+
+- Allow silent xpub exports at the `m/45'/coin_type'/account'` derivation paths.
+- Allow silent xpub exports for any unhardened child of an allowed path.
+- Allow up to 8 derivation steps for BIP-32 paths (instead of 6).
+
+## [2.1.0] - 16-11-2022
+
+### Added
+
+- Miniscript support on SegWit.
+- Improved support for wallet policies.
+- Support for sighash flags.
+
+### Changed
+
+- Wallet policies now allow external keys with no key origin information.
+- Wallet policies now allow multiple internal keys.
+
+### Removed
+
+- Support for legacy protocol (pre-2.0.0 version) and support for altcoins, now done via separate apps. Substantial binary size reduction as a consequence.
+
+## [2.0.6] - 06-06-2022
+
+### Added
+
+- Support signing of segwit V0 transactions with unverified inputs for compatibility with software unable to provide the previous transaction.
+
+### Fixed
+
+- Fixed bug preventing signing transactions with external inputs (or with mixed script types).
+
+## [2.0.5] - 03-05-2022
+
+### Changed
+
+- Technical release; restore compatibility with some client libraries that rely on deprecated legacy behavior.
+
## [2.0.4] - 28-03-2022
### Added
diff --git a/Makefile b/Makefile
index 84bdd7561..0f5cfed61 100644
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,6 @@
# ****************************************************************************
# Ledger App for Bitcoin
-# (c) 2021 Ledger SAS.
+# (c) 2024 Ledger SAS.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -15,10 +15,12 @@
# limitations under the License.
# ****************************************************************************
-ifdef DEBUG
ifndef DEBUG_LOG_LEVEL
-DEBUG_LOG_LEVEL := $(DEBUG)
-endif
+ ifdef DEBUG
+ DEBUG_LOG_LEVEL := $(DEBUG)
+ else
+ DEBUG_LOG_LEVEL := 0
+ endif
endif
ifeq ($(BOLOS_SDK),)
@@ -27,387 +29,248 @@ endif
include $(BOLOS_SDK)/Makefile.defines
-# TODO: compile with the right path restrictions
-# APP_LOAD_PARAMS = --curve secp256k1
-APP_LOAD_PARAMS = $(COMMON_LOAD_PARAMS)
-APP_PATH = ""
+# TODO: Compile with the right path restrictions
+#
+# The right path restriction would be something like
+# --path "*'/0'"
+# for mainnet, and
+# --path "*'/1'"
+# for testnet.
+#
+# That is, restrict the BIP-44 coin_type, but not the purpose.
+# However, such wildcards are not currently supported by the OS.
+#
+# Note that the app still requires explicit user approval before exporting
+# any xpub outside of a small set of allowed standard paths.
+
+# Application allowed derivation curves.
+CURVE_APP_LOAD_PARAMS = secp256k1
+# Application allowed derivation paths.
+PATH_APP_LOAD_PARAMS = ""
+
+# Allowed SLIP21 paths
+PATH_SLIP21_APP_LOAD_PARAMS = "LEDGER-Wallet policy"
+
+# Application version
APPVERSION_M = 2
-APPVERSION_N = 0
-APPVERSION_P = 4
-APPVERSION = "$(APPVERSION_M).$(APPVERSION_N).$(APPVERSION_P)"
+APPVERSION_N = 2
+APPVERSION_P = 3
+APPVERSION_SUFFIX = # if not empty, appended at the end. Do not add a dash.
+ifeq ($(APPVERSION_SUFFIX),)
+APPVERSION = "$(APPVERSION_M).$(APPVERSION_N).$(APPVERSION_P)"
+else
+APPVERSION = "$(APPVERSION_M).$(APPVERSION_N).$(APPVERSION_P)-$(strip $(APPVERSION_SUFFIX))"
+endif
-APP_STACK_SIZE = 1500
+# Setting to allow building variant applications
+VARIANT_PARAM = COIN
+VARIANT_VALUES = bitcoin_testnet bitcoin liquid_regtest liquid_regtest_headless liquid liquid_headless
# simplify for tests
ifndef COIN
COIN=liquid_regtest
endif
-# Flags: BOLOS_SETTINGS, GLOBAL_PIN, DERIVE_MASTER
-# Dependency to Bitcoin app (for altcoins)
-APP_LOAD_FLAGS=--appFlags 0xa50 --dep Bitcoin:$(APPVERSION)
+########################################
+# Application custom permissions #
+########################################
+HAVE_APPLICATION_FLAG_DERIVE_MASTER = 1
+HAVE_APPLICATION_FLAG_GLOBAL_PIN = 1
+HAVE_APPLICATION_FLAG_BOLOS_SETTINGS = 1
+ifneq (,$(findstring bitcoin,$(COIN)))
+HAVE_APPLICATION_FLAG_LIBRARY = 1
+endif
-ifeq ($(COIN),bitcoin_testnet)
-# we're not using the lib :)
-DEFINES_LIB=
-APP_LOAD_FLAGS=--appFlags 0xa50
+ifeq ($(COIN),bitcoin_testnet)
-# Bitcoin testnet (can also be used for signet)
+# Bitcoin testnet, no legacy support
DEFINES += BIP32_PUBKEY_VERSION=0x043587CF
-DEFINES += BIP32_PRIVKEY_VERSION=0x04358394
DEFINES += BIP44_COIN_TYPE=1
-DEFINES += BIP44_COIN_TYPE_2=1
DEFINES += COIN_P2PKH_VERSION=111
DEFINES += COIN_P2SH_VERSION=196
DEFINES += COIN_NATIVE_SEGWIT_PREFIX=\"tb\"
-DEFINES += COIN_FAMILY=1
-DEFINES += COIN_COINID=\"Bitcoin\"
-DEFINES += COIN_COINID_HEADER=\"BITCOIN\"
-DEFINES += COIN_COINID_NAME=\"Bitcoin\"
DEFINES += COIN_COINID_SHORT=\"TEST\"
-DEFINES += COIN_KIND=COIN_KIND_BITCOIN_TESTNET
-DEFINES += COIN_FLAGS=FLAG_SEGWIT_CHANGE_SUPPORT
APPNAME = "Bitcoin Test"
else ifeq ($(COIN),bitcoin)
-# we're not using the lib :)
-DEFINES_LIB=
-APP_LOAD_FLAGS=--appFlags 0xa50
-
-# Bitcoin mainnet
+# Bitcoin mainnet, no legacy support
DEFINES += BIP32_PUBKEY_VERSION=0x0488B21E
-DEFINES += BIP32_PRIVKEY_VERSION=0x0488ADE4
DEFINES += BIP44_COIN_TYPE=0
-DEFINES += BIP44_COIN_TYPE_2=0
DEFINES += COIN_P2PKH_VERSION=0
DEFINES += COIN_P2SH_VERSION=5
DEFINES += COIN_NATIVE_SEGWIT_PREFIX=\"bc\"
-DEFINES += COIN_FAMILY=1
-DEFINES += COIN_COINID=\"Bitcoin\"
-DEFINES += COIN_COINID_HEADER=\"BITCOIN\"
-DEFINES += COIN_COINID_NAME=\"Bitcoin\\x20Testnet\"
DEFINES += COIN_COINID_SHORT=\"BTC\"
-DEFINES += COIN_KIND=COIN_KIND_BITCOIN
-DEFINES += COIN_FLAGS=FLAG_SEGWIT_CHANGE_SUPPORT
APPNAME = "Bitcoin"
-else ifeq ($(COIN),bitcoin_regtest)
-# This target can be used to compile a version of the app that uses regtest addresses
-
-# we're not using the lib :)
-DEFINES_LIB=
-APP_LOAD_FLAGS=--appFlags 0xa50
-
-# Bitcoin regtest test network
-DEFINES += BIP32_PUBKEY_VERSION=0x043587CF
-DEFINES += BIP32_PRIVKEY_VERSION=0x04358394
-DEFINES += BIP44_COIN_TYPE=1
-DEFINES += BIP44_COIN_TYPE_2=1
-DEFINES += COIN_P2PKH_VERSION=111
-DEFINES += COIN_P2SH_VERSION=196
-DEFINES += COIN_NATIVE_SEGWIT_PREFIX=\"bcrt\"
-DEFINES += COIN_FAMILY=1
-DEFINES += COIN_COINID=\"Bitcoin\"
-DEFINES += COIN_COINID_HEADER=\"BITCOIN\"
-DEFINES += COIN_COINID_NAME=\"Bitcoin\"
-DEFINES += COIN_COINID_SHORT=\"TEST\"
-DEFINES += COIN_KIND=COIN_KIND_BITCOIN_TESTNET
-DEFINES += COIN_FLAGS=FLAG_SEGWIT_CHANGE_SUPPORT
-APPNAME = "Bitcoin Regtest"
-
else ifeq ($(COIN),liquid_regtest)
-# we're not using the lib :)
-DEFINES_LIB=
-# Flags: DERIVE_MASTER, GLOBAL_PIN, BOLOS_SETTINGS
-APP_LOAD_FLAGS=--appFlags 0x250
-
# Liquid regtest
DEFINES += BIP32_PUBKEY_VERSION=0x043587CF
DEFINES += BIP32_PRIVKEY_VERSION=0x04358394
DEFINES += BIP44_COIN_TYPE=1
-DEFINES += BIP44_COIN_TYPE_2=1
DEFINES += COIN_P2PKH_VERSION=111
DEFINES += COIN_P2SH_VERSION=75
DEFINES += COIN_PREFIX_CONFIDENTIAL=4
DEFINES += HAVE_LIQUID
DEFINES += HAVE_LIQUID_TEST
DEFINES += COIN_BLINDED_VERSION=4
-DEFINES += COIN_FAMILY=1
-DEFINES += COIN_COINID=\"Bitcoin\"
-DEFINES += COIN_COINID_HEADER=\"BITCOIN\"
-DEFINES += COIN_COINID_NAME=\"Bitcoin\"
-DEFINES += COIN_COINID_SHORT=\"BTC\"
+DEFINES += COIN_COINID_SHORT=\"L-BTC\"
DEFINES += COIN_NATIVE_SEGWIT_PREFIX=\"ert\"
DEFINES += COIN_NATIVE_SEGWIT_PREFIX_CONFIDENTIAL=\"el\"
-DEFINES += COIN_KIND=COIN_KIND_BITCOIN
-DEFINES += COIN_FLAGS=FLAG_SEGWIT_CHANGE_SUPPORT
+
APPNAME = "Liquid Regtest"
-# -disabled- APP_LOAD_PARAMS += --curve secp256k1
else ifeq ($(COIN),liquid_regtest_headless)
-# we're not using the lib :)
-DEFINES_LIB=
-# Flags: DERIVE_MASTER, GLOBAL_PIN, BOLOS_SETTINGS
-APP_LOAD_FLAGS=--appFlags 0x250
-
# Liquid regtest headless
DEFINES += BIP32_PUBKEY_VERSION=0x043587CF
DEFINES += BIP32_PRIVKEY_VERSION=0x04358394
DEFINES += BIP44_COIN_TYPE=1
-DEFINES += BIP44_COIN_TYPE_2=1
DEFINES += COIN_P2PKH_VERSION=111
DEFINES += COIN_P2SH_VERSION=75
DEFINES += COIN_PREFIX_CONFIDENTIAL=4
DEFINES += HAVE_LIQUID
DEFINES += HAVE_LIQUID_TEST
DEFINES += COIN_BLINDED_VERSION=4
-DEFINES += COIN_FAMILY=1
-DEFINES += COIN_COINID=\"Bitcoin\"
-DEFINES += COIN_COINID_HEADER=\"BITCOIN\"
-DEFINES += COIN_COINID_NAME=\"Bitcoin\"
-DEFINES += COIN_COINID_SHORT=\"BTC\"
+DEFINES += COIN_COINID_SHORT=\"L-BTC\"
DEFINES += COIN_NATIVE_SEGWIT_PREFIX=\"ert\"
DEFINES += COIN_NATIVE_SEGWIT_PREFIX_CONFIDENTIAL=\"el\"
-DEFINES += COIN_KIND=COIN_KIND_BITCOIN
-DEFINES += COIN_FLAGS=FLAG_SEGWIT_CHANGE_SUPPORT
DEFINES += HAVE_LIQUID_HEADLESS
+
APPNAME = "Liquid Regtest Hless"
-# -disabled- APP_LOAD_PARAMS += --curve secp256k1
else ifeq ($(COIN),liquid)
-# we're not using the lib :)
-DEFINES_LIB=
-# Flags: DERIVE_MASTER, GLOBAL_PIN, BOLOS_SETTINGS
-APP_LOAD_FLAGS=--appFlags 0x250
-
# Liquid
DEFINES += BIP32_PUBKEY_VERSION=0x0488B21E
DEFINES += BIP32_PRIVKEY_VERSION=0x0488ADE4
DEFINES += BIP44_COIN_TYPE=1776
-DEFINES += BIP44_COIN_TYPE_2=1776
DEFINES += COIN_P2PKH_VERSION=57
DEFINES += COIN_P2SH_VERSION=39
DEFINES += COIN_PREFIX_CONFIDENTIAL=12
DEFINES += HAVE_LIQUID
DEFINES += COIN_BLINDED_VERSION=12
-DEFINES += COIN_FAMILY=1
-DEFINES += COIN_COINID=\"Bitcoin\"
-DEFINES += COIN_COINID_HEADER=\"BITCOIN\"
-DEFINES += COIN_COINID_NAME=\"Bitcoin\"
-DEFINES += COIN_COINID_SHORT=\"BTC\"
+DEFINES += COIN_COINID_SHORT=\"L-BTC\"
DEFINES += COIN_NATIVE_SEGWIT_PREFIX=\"ex\"
DEFINES += COIN_NATIVE_SEGWIT_PREFIX_CONFIDENTIAL=\"lq\"
-DEFINES += COIN_KIND=COIN_KIND_BITCOIN
-DEFINES += COIN_FLAGS=FLAG_SEGWIT_CHANGE_SUPPORT
+
APPNAME = "Liquid"
-# -disabled- APP_LOAD_PARAMS += --curve secp256k1
else ifeq ($(COIN),liquid_headless)
-# we're not using the lib :)
-DEFINES_LIB=
-# Flags: DERIVE_MASTER, GLOBAL_PIN, BOLOS_SETTINGS
-APP_LOAD_FLAGS=--appFlags 0x250
-
# Liquid Headless
DEFINES += BIP32_PUBKEY_VERSION=0x0488B21E
DEFINES += BIP32_PRIVKEY_VERSION=0x0488ADE4
DEFINES += BIP44_COIN_TYPE=1776
-DEFINES += BIP44_COIN_TYPE_2=1776
DEFINES += COIN_P2PKH_VERSION=57
DEFINES += COIN_P2SH_VERSION=39
DEFINES += COIN_PREFIX_CONFIDENTIAL=12
DEFINES += HAVE_LIQUID
DEFINES += COIN_BLINDED_VERSION=12
-DEFINES += COIN_FAMILY=1
-DEFINES += COIN_COINID=\"Bitcoin\"
-DEFINES += COIN_COINID_HEADER=\"BITCOIN\"
DEFINES += COIN_COLOR_HDR=0xFCB653
DEFINES += COIN_COLOR_DB=0xFEDBA9
-DEFINES += COIN_COINID_NAME=\"Bitcoin\"
-DEFINES += COIN_COINID_SHORT=\"BTC\"
+DEFINES += COIN_COINID_SHORT=\"L-BTC\"
DEFINES += COIN_NATIVE_SEGWIT_PREFIX=\"ex\"
DEFINES += COIN_NATIVE_SEGWIT_PREFIX_CONFIDENTIAL=\"lq\"
-DEFINES += COIN_KIND=COIN_KIND_BITCOIN
-DEFINES += COIN_FLAGS=FLAG_SEGWIT_CHANGE_SUPPORT
DEFINES += HAVE_LIQUID_HEADLESS
+
APPNAME = "Liquid Hless"
-# -disabled- APP_LOAD_PARAMS += --curve secp256k1
else
ifeq ($(filter clean,$(MAKECMDGOALS)),)
-$(error Unsupported COIN - use bitcoin_testnet, bitcoin, bitcoin_regtest, liquid_regtest, liquid_regtest_headless, liquid, liquid_headless)
+$(error Unsupported COIN - use bitcoin_testnet, bitcoin, liquid_regtest, liquid_regtest_headless, liquid, liquid_headless)
endif
endif
-APP_LOAD_PARAMS += $(APP_LOAD_FLAGS)
-DEFINES += $(DEFINES_LIB)
-
-ifeq ($(TARGET_NAME),TARGET_NANOS)
-ICONNAME=icons/nanos_app_$(COIN).gif
+# Application icons following guidelines:
+# https://developers.ledger.com/docs/embedded-app/design-requirements/#device-icon
+
+ifneq (,$(findstring bitcoin,$(COIN)))
+# Bitcoin icons
+ICON_NANOS = icons/nanos_app_bitcoin.gif
+ICON_NANOX = icons/nanox_app_bitcoin.gif
+ICON_NANOSP = icons/nanox_app_bitcoin.gif
+ICON_STAX = icons/stax_app_bitcoin.gif
+else ifneq (,$(findstring liquid,$(COIN)))
+# Liquid icons
+ICON_NANOS = icons/nanos_app_liquid.gif
+ICON_NANOX = icons/nanox_app_liquid.gif
+ICON_NANOSP = icons/nanox_app_liquid.gif
+ICON_STAX = icons/stax_app_liquid.gif
else
-ICONNAME=icons/nanox_app_$(COIN).gif
+$(error Unsupported COIN)
endif
-all: default
-
-# TODO: double check if all those flags are still relevant/needed (was copied from legacy app-bitcoin)
+########################################
+# Application communication interfaces #
+########################################
+ENABLE_BLUETOOTH = 1
-DEFINES += APPVERSION=\"$(APPVERSION)\"
-DEFINES += MAJOR_VERSION=$(APPVERSION_M) MINOR_VERSION=$(APPVERSION_N) PATCH_VERSION=$(APPVERSION_P)
-DEFINES += OS_IO_SEPROXYHAL
-DEFINES += HAVE_BAGL HAVE_SPRINTF HAVE_SNPRINTF_FORMAT_U
-DEFINES += HAVE_IO_USB HAVE_L4_USBLIB IO_USB_MAX_ENDPOINTS=4 IO_HID_EP_LENGTH=64 HAVE_USB_APDU
-DEFINES += LEDGER_MAJOR_VERSION=$(APPVERSION_M) LEDGER_MINOR_VERSION=$(APPVERSION_N) LEDGER_PATCH_VERSION=$(APPVERSION_P) TCS_LOADER_PATCH_VERSION=0
-DEFINES += HAVE_UX_FLOW
+########################################
+# NBGL custom features #
+########################################
+ENABLE_NBGL_QRCODE = 1
-DEFINES += HAVE_WEBUSB WEBUSB_URL_SIZE_B=0 WEBUSB_URL=""
+########################################
+# Features disablers #
+########################################
+# Don't use standard app file to avoid conflicts for now
+DISABLE_STANDARD_APP_FILES = 1
-DEFINES += UNUSED\(x\)=\(void\)x
-DEFINES += APPVERSION=\"$(APPVERSION)\"
+# Don't use default IO_SEPROXY_BUFFER_SIZE to use another
+# value for NANOS for an unknown reason.
+DISABLE_DEFAULT_IO_SEPROXY_BUFFER_SIZE = 1
DEFINES += HAVE_BOLOS_APP_STACK_CANARY
-
ifeq ($(TARGET_NAME),TARGET_NANOS)
DEFINES += IO_SEPROXYHAL_BUFFER_SIZE_B=72
DEFINES += HAVE_WALLET_ID_SDK
else
DEFINES += IO_SEPROXYHAL_BUFFER_SIZE_B=300
-DEFINES += HAVE_BAGL BAGL_WIDTH=128 BAGL_HEIGHT=64
-DEFINES += HAVE_BAGL_ELLIPSIS # long label truncation feature
-DEFINES += HAVE_BAGL_FONT_OPEN_SANS_REGULAR_11PX
-DEFINES += HAVE_BAGL_FONT_OPEN_SANS_EXTRABOLD_11PX
-DEFINES += HAVE_BAGL_FONT_OPEN_SANS_LIGHT_16PX
-endif
-
-ifeq ($(TARGET_NAME),TARGET_NANOX)
-DEFINES += HAVE_BLE BLE_COMMAND_TIMEOUT_MS=2000
-DEFINES += HAVE_BLE_APDU # basic ledger apdu transport over BLE
endif
ifeq ($(TARGET_NAME),TARGET_NANOS)
# enables optimizations using the shared 1K CXRAM region
DEFINES += USE_CXRAM_SECTION
+ # enables usage of the NVRAM to free up some RAM
+ DEFINES += USE_NVRAM_STASH
endif
# debugging helper functions and macros
CFLAGS += -include debug-helpers/debug.h
-# DEFINES += HAVE_PRINT_STACK_POINTER
+# DEFINES += HAVE_PRINT_STACK_POINTER
+# DEFINES += HAVE_LOG_PROCESSOR
+# DEFINES += HAVE_APDU_LOG
ifeq ($(TEST),1)
$(warning On-device tests should only be run with Speculos!)
- DEBUG_LOG_LEVEL = 10
- DEFINES += RUN_ON_DEVICE_TESTS
-endif
-
-ifndef DEBUG_LOG_LEVEL
- DEBUG_LOG_LEVEL = 0
-endif
-
-ifeq ($(DEBUG_LOG_LEVEL),0)
- DEFINES += PRINTF\(...\)=
-else
- ifeq ($(DEBUG_LOG_LEVEL),10)
- $(warning Using semihosted PRINTF. Only run with Speculos!)
- DEFINES += HAVE_PRINTF HAVE_SEMIHOSTED_PRINTF PRINTF=semihosted_printf
- #DEFINES += HAVE_LOG_PROCESSOR
- #DEFINES += HAVE_APDU_LOG
- #DEFINES += HAVE_PRINT_STACK_POINTER
- else ifeq ($(DEBUG_LOG_LEVEL),11)
- $(warning CCMD PRINTF is used! APDU exchage is affected.)
- DEFINES += HAVE_CCMD_PRINTF
- else
- ifeq ($(TARGET_NAME),TARGET_NANOS)
- DEFINES += HAVE_PRINTF PRINTF=screen_printf
- else
- DEFINES += HAVE_PRINTF PRINTF=mcu_usb_printf
- endif
- endif
+ DEFINES += RUN_ON_DEVICE_TESTS HAVE_PRINTF HAVE_SEMIHOSTED_PRINTF
+else ifeq ($(DEBUG_LOG_LEVEL),10)
+ $(warning Using semihosted PRINTF. Only run with speculos!)
+ DEFINES += HAVE_PRINTF HAVE_SEMIHOSTED_PRINTF
+else ifeq ($(DEBUG_LOG_LEVEL),11)
+ $(warning CCMD PRINTF is used! APDU exchage is affected.)
+ DEFINES += HAVE_CCMD_PRINTF
endif
-
# Needed to be able to include the definition of G_cx
INCLUDES_PATH += $(BOLOS_SDK)/lib_cxng/src
-
-ifneq ($(BOLOS_ENV),)
-$(info BOLOS_ENV=$(BOLOS_ENV))
-CLANGPATH := $(BOLOS_ENV)/clang-arm-fropi/bin/
-GCCPATH := $(BOLOS_ENV)/gcc-arm-none-eabi-5_3-2016q1/bin/
-else
-$(info BOLOS_ENV is not set: falling back to CLANGPATH and GCCPATH)
-endif
-ifeq ($(CLANGPATH),)
-$(info CLANGPATH is not set: clang will be used from PATH)
-endif
-ifeq ($(GCCPATH),)
-$(info GCCPATH is not set: arm-none-eabi-* will be used from PATH)
-endif
-
-CC := $(CLANGPATH)clang
-AS := $(GCCPATH)arm-none-eabi-gcc
-LD := $(GCCPATH)arm-none-eabi-gcc
-LDLIBS += -lm -lgcc -lc
-
-ifeq ($(DEBUG_LOG_LEVEL),0)
- $(info *** Release version is being built ***)
- CFLAGS += -Oz
- LDFLAGS += -O3 -Os
-else
- $(info *** Debug version is being built ***)
- CFLAGS += -Og -g
- LDFLAGS += -Og
-endif
-
-include $(BOLOS_SDK)/Makefile.glyphs
-
+# Application source files
APP_SOURCE_PATH += src
-SDK_SOURCE_PATH += lib_stusb lib_stusb_impl lib_ux
-
-ifeq ($(TARGET_NAME),TARGET_NANOX)
- SDK_SOURCE_PATH += lib_blewbxx lib_blewbxx_impl
-endif
-
-load: all
- python3 -m ledgerblue.loadApp $(APP_LOAD_PARAMS)
-
-load-offline: all
- python3 -m ledgerblue.loadApp $(APP_LOAD_PARAMS) --offline
-load-no-build:
- python3 -m ledgerblue.loadApp $(APP_LOAD_PARAMS)
-
-load-offline-no-build:
- python3 -m ledgerblue.loadApp $(APP_LOAD_PARAMS) --offline
-
-delete:
- python3 -m ledgerblue.deleteApp $(COMMON_DELETE_PARAMS)
-
-include $(BOLOS_SDK)/Makefile.rules
-
-dep/%.d: %.c Makefile
-
-
-# Temporary restriction until we a Resistance Nano X icon
-ifeq ($(TARGET_NAME),TARGET_NANOS)
-listvariants:
- @echo VARIANTS COIN bitcoin_testnet bitcoin bitcoin_cash bitcoin_gold litecoin dogecoin dash zcash horizen komodo stratis peercoin pivx viacoin vertcoin stealth digibyte qtum bitcoin_private firo gamecredits zclassic xsn nix lbry ravencoin resistance hydra hydra_testnet xrhodium
-else
-listvariants:
- @echo VARIANTS COIN bitcoin_testnet bitcoin bitcoin_cash bitcoin_gold litecoin dogecoin dash zcash horizen komodo stratis peercoin pivx viacoin vertcoin stealth digibyte qtum bitcoin_private firo gamecredits zclassic xsn nix lbry ravencoin hydra hydra_testnet xrhodium
-endif
+# Allow usage of function from lib_standard_app/crypto_helpers.c
+APP_SOURCE_FILES += ${BOLOS_SDK}/lib_standard_app/crypto_helpers.c
+include $(BOLOS_SDK)/Makefile.standard_app
# Makes a detailed report of code and data size in debug/size-report.txt
# More useful for production builds with DEBUG=0
diff --git a/README.md b/README.md
index 07b3b3564..6ae34cd42 100644
--- a/README.md
+++ b/README.md
@@ -40,7 +40,7 @@ the process outputs HTML and LaTeX documentations in `doc/html` and `doc/latex`
## Client libraries
-A [Python client library](bitcoin_client) and a [TypeScript client library](bitcoin_client_js) are available in this repository.
+A [Python client library](bitcoin_client), a [TypeScript client library](bitcoin_client_js) and a [Rust client library](bitcoin_client_rs) are available in this repository.
## Tests & Continuous Integration
diff --git a/bitcoin_client/.gitignore b/bitcoin_client/.gitignore
index 0021b4fb3..108413e8c 100644
--- a/bitcoin_client/.gitignore
+++ b/bitcoin_client/.gitignore
@@ -1 +1,2 @@
-dist/**
\ No newline at end of file
+dist/**
+**/.venv
diff --git a/bitcoin_client/CHANGELOG.md b/bitcoin_client/CHANGELOG.md
index 70da3dc6b..baf9065cd 100644
--- a/bitcoin_client/CHANGELOG.md
+++ b/bitcoin_client/CHANGELOG.md
@@ -7,6 +7,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
Dates are in `dd-mm-yyyy` format.
+## [0.2.1] - 18-04-2023
+
+### Changed
+- Avoid using miniscript policies containing an `a:` fragment on versions below `2.1.2` of the bitcoin app.
+
+## [0.2.0] - 3-04-2023
+
+This release introduces a breaking change in the return type of the `sign_psbt`method.
+
+### Added
+- Added new `PartialSignature` data class together with support for taproot script signing, which is supported in version `2.1.2` of the bitcoin app.
+
+## [0.1.2] - 09-01-2023
+
+### Fixed
+- Added missing dependency.
+
+## [0.1.1] - 26-10-2022
+
+### Changed
+
+- Improved interface of TransportClient for better interoperability with HID.
+- `sign_psbt` now accepts the psbt to be passed as `bytes` or `str`.
+
+## [0.1.0] - 18-10-2022
+
+### Changed
+
+Upgraded library to version 2.1.0 of the app.
+
## [0.0.3] - 25-04-2022
### Changed
diff --git a/bitcoin_client/README.md b/bitcoin_client/README.md
index 817b3c730..3183a9a8a 100644
--- a/bitcoin_client/README.md
+++ b/bitcoin_client/README.md
@@ -35,7 +35,7 @@ It is possible to run the app and the library with the [speculos](https://github
⚠️ Currently, speculos does not correctly emulate the version of the app, always returning a dummy value; in order to use the library, it is necessary to set the `SPECULOS_APPNAME` environment variable before starting speculos, for example with:
```
-$ export SPECULOS_APPNAME="Bitcoin Test:2.0.0"
+$ export SPECULOS_APPNAME="Bitcoin Test:2.1.0"
```
Similarly, to test the library behavior on a legacy version of the app, one can set the version to `1.6.5` (the final version of the 1.X series).
@@ -53,7 +53,7 @@ Testing the `sign_psbt` method requires producing a valid PSBT (with any externa
```python
from typing import Optional
-from ledger_bitcoin import createClient, Chain, MultisigWallet, MultisigWallet, PolicyMapWallet, AddressType, TransportClient
+from ledger_bitcoin import createClient, Chain, MultisigWallet, MultisigWallet, WalletPolicy, AddressType, TransportClient
from ledger_bitcoin.psbt import PSBT
@@ -71,11 +71,11 @@ def main():
# ==> Get and display on screen the first taproot address
first_taproot_account_pubkey = client.get_extended_pubkey("m/86'/1'/0'")
- first_taproot_account_policy = PolicyMapWallet(
+ first_taproot_account_policy = WalletPolicy(
"",
- "tr(@0)",
+ "tr(@0/**)",
[
- f"[{fpr}/86'/1'/0']{first_taproot_account_pubkey}/**"
+ f"[{fpr}/86'/1'/0']{first_taproot_account_pubkey}"
],
)
first_taproot_account_address = client.get_wallet_address(
@@ -91,15 +91,15 @@ def main():
# ==> Register a multisig wallet named "Cold storage"
our_pubkey = client.get_extended_pubkey("m/48'/1'/0'/2'")
- other_key_info = "[76223a6e/48'/1'/0'/2']tpubDE7NQymr4AFtewpAsWtnreyq9ghkzQBXpCZjWLFVRAvnbf7vya2eMTvT2fPapNqL8SuVvLQdbUbMfWLVDCZKnsEBqp6UK93QEzL8Ck23AwF/**"
+ other_key_info = "[76223a6e/48'/1'/0'/2']tpubDE7NQymr4AFtewpAsWtnreyq9ghkzQBXpCZjWLFVRAvnbf7vya2eMTvT2fPapNqL8SuVvLQdbUbMfWLVDCZKnsEBqp6UK93QEzL8Ck23AwF"
multisig_policy = MultisigWallet(
name="Cold storage",
address_type=AddressType.WIT,
threshold=2,
keys_info=[
- other_key_info, # some other bitcoiner
- f"[{fpr}/48'/1'/0'/2']{our_pubkey}/**", # that's us
+ other_key_info, # some other bitcoiner
+ f"[{fpr}/48'/1'/0'/2']{our_pubkey}", # that's us
],
)
@@ -118,7 +118,7 @@ def main():
# TODO: set a wallet policy and a valid psbt file in order to test psbt signing
psbt_filename: Optional[str] = None
- signing_policy: Optional[PolicyMapWallet] = None
+ signing_policy: Optional[WalletPolicy] = None
signing_policy_hmac: Optional[bytes] = None
if not psbt_filename or not signing_policy:
print("Nothing to sign :(")
diff --git a/bitcoin_client/ledger_bitcoin/__init__.py b/bitcoin_client/ledger_bitcoin/__init__.py
index 52038368b..7c52114cf 100644
--- a/bitcoin_client/ledger_bitcoin/__init__.py
+++ b/bitcoin_client/ledger_bitcoin/__init__.py
@@ -1,10 +1,22 @@
"""Ledger Nano Bitcoin app client"""
-from .client_base import Client, TransportClient
+from .client_base import Client, TransportClient, PartialSignature
from .client import createClient
from .common import Chain
-from .wallet import AddressType, Wallet, MultisigWallet, PolicyMapWallet, BlindedWallet, BlindedMultisigWallet
+from .wallet import AddressType, WalletPolicy, MultisigWallet, WalletType, WalletPolicy, BlindedWallet, BlindedMultisigWallet
-__all__ = ["Client", "TransportClient", "createClient", "Chain", "AddressType", "Wallet", "MultisigWallet", "PolicyMapWallet"]
+__version__ = '0.3.0'
+
+__all__ = [
+ "Client",
+ "TransportClient",
+ "PartialSignature",
+ "createClient",
+ "Chain",
+ "AddressType",
+ "WalletPolicy",
+ "MultisigWallet",
+ "WalletType"
+]
diff --git a/bitcoin_client/ledger_bitcoin/btchip/btchipHelpers.py b/bitcoin_client/ledger_bitcoin/btchip/btchipHelpers.py
index ba5b66c5f..bb74cd56b 100644
--- a/bitcoin_client/ledger_bitcoin/btchip/btchipHelpers.py
+++ b/bitcoin_client/ledger_bitcoin/btchip/btchipHelpers.py
@@ -21,8 +21,8 @@
import re
# from pycoin
-SATOSHI_PER_COIN = decimal.Decimal(1e8)
-COIN_PER_SATOSHI = decimal.Decimal(1)/SATOSHI_PER_COIN
+SATOSHI_PER_COIN = decimal.Decimal(100_000_000)
+COIN_PER_SATOSHI = decimal.Decimal('0.00000001')
def satoshi_to_btc(satoshi_count):
if satoshi_count == 0:
diff --git a/bitcoin_client/ledger_bitcoin/client.py b/bitcoin_client/ledger_bitcoin/client.py
index 8d810a454..8e3ced614 100644
--- a/bitcoin_client/ledger_bitcoin/client.py
+++ b/bitcoin_client/ledger_bitcoin/client.py
@@ -1,16 +1,23 @@
+from packaging.version import parse as parse_version
from typing import Tuple, List, Mapping, Optional, Union
import base64
from io import BytesIO, BufferedReader
+from .embit.base import EmbitError
+from .embit.descriptor import Descriptor
+from .embit.networks import NETWORKS
+
from .command_builder import BitcoinCommandBuilder, BitcoinInsType
-from .common import Chain, read_varint
+from .common import Chain, read_uint, read_varint
from .client_command import ClientCommandInterpreter, ClientCommandCode
-from .client_base import Client, TransportClient
+from .client_base import Client, TransportClient, PartialSignature
from .client_legacy import LegacyClient
from .exception import DeviceException
+from .errors import UnknownDeviceError
from .merkle import get_merkleized_map_commitment
-from .wallet import Wallet, WalletType, PolicyMapWallet
-from .psbt import PSBT
+from .wallet import WalletPolicy, WalletType
+from .psbt import PSBT, normalize_psbt
+from . import segwit_addr
from ._serialize import deser_string
@@ -32,6 +39,23 @@ def parse_stream_to_map(f: BufferedReader) -> Mapping[bytes, bytes]:
return result
+def _make_partial_signature(pubkey_augm: bytes, signature: bytes) -> PartialSignature:
+ if len(pubkey_augm) == 64:
+ # tapscript spend: pubkey_augm is the concatenation of:
+ # - a 32-byte x-only pubkey
+ # - the 32-byte tapleaf_hash
+ return PartialSignature(signature=signature, pubkey=pubkey_augm[0:32], tapleaf_hash=pubkey_augm[32:])
+
+ else:
+ # either legacy, segwit or taproot keypath spend
+ # pubkey must be 32 (taproot x-only pubkey) or 33 bytes (compressed pubkey)
+
+ if len(pubkey_augm) not in [32, 33]:
+ raise UnknownDeviceError(f"Invalid pubkey length returned: {len(pubkey_augm)}")
+
+ return PartialSignature(signature=signature, pubkey=pubkey_augm)
+
+
class NewClient(Client):
# internal use for testing: if set to True, sign_psbt will not clone the psbt before converting to psbt version 2
_no_clone_psbt: bool = False
@@ -69,14 +93,17 @@ def get_extended_pubkey(self, path: str, display: bool = False) -> str:
return response.decode()
- def register_wallet(self, wallet: Wallet) -> Tuple[bytes, bytes]:
- if wallet.type != WalletType.POLICYMAP:
- raise ValueError("wallet type must be POLICYMAP")
+ def register_wallet(self, wallet: WalletPolicy) -> Tuple[bytes, bytes]:
+ if wallet.version not in [WalletType.WALLET_POLICY_V1, WalletType.WALLET_POLICY_V2]:
+ raise ValueError("invalid wallet policy version")
client_intepreter = ClientCommandInterpreter()
client_intepreter.add_known_preimage(wallet.serialize())
client_intepreter.add_known_list([k.encode() for k in wallet.keys_info])
+ # necessary for version 1 of the protocol (introduced in version 2.1.0)
+ client_intepreter.add_known_preimage(wallet.descriptor_template.encode())
+
sw, response = self._make_request(
self.builder.register_wallet(wallet), client_intepreter
)
@@ -90,21 +117,25 @@ def register_wallet(self, wallet: Wallet) -> Tuple[bytes, bytes]:
wallet_id = response[0:32]
wallet_hmac = response[32:64]
+ # sanity check: for miniscripts, derive the first address independently with python-bip380
+ first_addr_device = self.get_wallet_address(wallet, wallet_hmac, 0, 0, False)
+
+ if first_addr_device != self._derive_address_for_policy(wallet, False, 0):
+ raise RuntimeError("Invalid address. Please update your Bitcoin app. If the problem persists, report a bug at https://github.com/LedgerHQ/app-bitcoin-new")
+
return wallet_id, wallet_hmac
def get_wallet_address(
self,
- wallet: Wallet,
+ wallet: WalletPolicy,
wallet_hmac: Optional[bytes],
change: int,
address_index: int,
display: bool,
) -> str:
- if wallet.type != WalletType.POLICYMAP or not isinstance(
- wallet, PolicyMapWallet
- ):
- raise ValueError("wallet type must be POLICYMAP")
+ if not isinstance(wallet, WalletPolicy) or wallet.version not in [WalletType.WALLET_POLICY_V1, WalletType.WALLET_POLICY_V2]:
+ raise ValueError("wallet type must be WalletPolicy, with version either WALLET_POLICY_V1 or WALLET_POLICY_V2")
if change != 0 and change != 1:
raise ValueError("Invalid change")
@@ -113,6 +144,9 @@ def get_wallet_address(
client_intepreter.add_known_list([k.encode() for k in wallet.keys_info])
client_intepreter.add_known_preimage(wallet.serialize())
+ # necessary for version 1 of the protocol (introduced in version 2.1.0)
+ client_intepreter.add_known_preimage(wallet.descriptor_template.encode())
+
sw, response = self._make_request(
self.builder.get_wallet_address(
wallet, wallet_hmac, address_index, change, display
@@ -123,32 +157,19 @@ def get_wallet_address(
if sw != 0x9000:
raise DeviceException(error_code=sw, ins=BitcoinInsType.GET_WALLET_ADDRESS)
- return response.decode()
+ result = response.decode()
- def sign_psbt(self, psbt: PSBT, wallet: Wallet, wallet_hmac: Optional[bytes]) -> Mapping[int, bytes]:
- """Signs a PSBT using a registered wallet (or a standard wallet that does not need registration).
+ # sanity check: for miniscripts, derive the address independently with python-bip380
- Signature requires explicit approval from the user.
+ if result != self._derive_address_for_policy(wallet, change, address_index):
+ raise RuntimeError("Invalid address. Please update your Bitcoin app. If the problem persists, report a bug at https://github.com/LedgerHQ/app-bitcoin-new")
- Parameters
- ----------
- psbt : PSBT
- A PSBT of version 0 or 2, with all the necessary information to sign the inputs already filled in; what the
- required fields changes depending on the type of input.
- The non-witness UTXO must be present for both legacy and SegWit inputs, or the hardware wallet will reject
- signing. This is not required for Taproot inputs.
+ return result
- wallet : Wallet
- The registered wallet policy, or a standard wallet policy.
+ def sign_psbt(self, psbt: Union[PSBT, bytes, str], wallet: WalletPolicy, wallet_hmac: Optional[bytes]) -> List[Tuple[int, PartialSignature]]:
- wallet_hmac: Optional[bytes]
- For a registered wallet, the hmac obtained at wallet registration. `None` for a standard wallet policy.
+ psbt = normalize_psbt(psbt)
- Returns
- -------
- Mapping[int, bytes]
- A mapping that has as keys the indexes of inputs that the Hardware Wallet signed, and the corresponding signatures as values.
- """
if psbt.version != 2:
if self._no_clone_psbt:
psbt.convert_to_v2()
@@ -173,6 +194,9 @@ def sign_psbt(self, psbt: PSBT, wallet: Wallet, wallet_hmac: Optional[bytes]) ->
client_intepreter.add_known_list([k.encode() for k in wallet.keys_info])
client_intepreter.add_known_preimage(wallet.serialize())
+ # necessary for version 1 of the protocol (introduced in version 2.1.0)
+ client_intepreter.add_known_preimage(wallet.descriptor_template.encode())
+
global_map: Mapping[bytes, bytes] = parse_stream_to_map(f)
client_intepreter.add_known_mapping(global_map)
@@ -211,18 +235,19 @@ def sign_psbt(self, psbt: PSBT, wallet: Wallet, wallet_hmac: Optional[bytes]) ->
if any(len(x) <= 1 for x in results):
raise RuntimeError("Invalid response")
- results_map = {}
+ results_list: List[Tuple[int, PartialSignature]] = []
for res in results:
res_buffer = BytesIO(res)
input_index = read_varint(res_buffer)
- signature = res_buffer.read()
- if input_index in results_map:
- raise RuntimeError(f"Multiple signatures produced for the same input: {input_index}")
+ pubkey_augm_len = read_uint(res_buffer, 8)
+ pubkey_augm = res_buffer.read(pubkey_augm_len)
+
+ signature = res_buffer.read()
- results_map[input_index] = signature
+ results_list.append((input_index, _make_partial_signature(pubkey_augm, signature)))
- return results_map
+ return results_list
def get_master_fingerprint(self) -> bytes:
sw, response = self._make_request(self.builder.get_master_fingerprint())
@@ -265,6 +290,16 @@ def liquid_get_blinding_key(self, script: bytes) -> bytes:
raise DeviceException(error_code=sw, ins=BitcoinInsType.LIQUID_GET_BLINDING_KEY)
return response
+ def _derive_address_for_policy(self, wallet: WalletPolicy, change: bool, address_index: int) -> Optional[str]:
+ desc_str = wallet.get_descriptor(change)
+ try:
+ desc = Descriptor.from_string(desc_str)
+
+ desc = desc.derive(address_index)
+ net = NETWORKS['main'] if self.chain == Chain.MAIN else NETWORKS['test']
+ return desc.script_pubkey().address(net)
+ except EmbitError:
+ return None
def createClient(comm_client: Optional[TransportClient] = None, chain: Chain = Chain.MAIN, debug: bool = False) -> Union[LegacyClient, NewClient]:
@@ -272,8 +307,16 @@ def createClient(comm_client: Optional[TransportClient] = None, chain: Chain = C
comm_client = TransportClient("hid")
base_client = Client(comm_client, chain, debug)
- _, app_version, _ = base_client.get_version()
- if app_version >= "2":
- return NewClient(comm_client, chain, debug)
- else:
+ app_name, app_version, _ = base_client.get_version()
+
+ version = parse_version(app_version)
+
+ # Use the legacy client if either:
+ # - the name of the app is "Bitcoin Legacy" or "Bitcoin Test Legacy" (regardless of the version)
+ # - the version is strictly less than 2.1
+ use_legacy = app_name in ["Bitcoin Legacy", "Bitcoin Test Legacy"] or version.major < 2 or (version.major == 2 and version.minor == 0)
+
+ if use_legacy:
return LegacyClient(comm_client, chain, debug)
+ else:
+ return NewClient(comm_client, chain, debug)
diff --git a/bitcoin_client/ledger_bitcoin/client_base.py b/bitcoin_client/ledger_bitcoin/client_base.py
index cdcb7c333..1b69f93d1 100644
--- a/bitcoin_client/ledger_bitcoin/client_base.py
+++ b/bitcoin_client/ledger_bitcoin/client_base.py
@@ -1,14 +1,17 @@
-from typing import Tuple, Mapping, Optional, Union, Literal
+from dataclasses import dataclass
+from typing import List, Tuple, Optional, Union, Literal
from io import BytesIO
-from ledgercomm import Transport
+from ledgercomm.interfaces.hid_device import HID
+
+from .transport import Transport
from .common import Chain
from .command_builder import DefaultInsType
from .exception import DeviceException
-from .wallet import Wallet
+from .wallet import WalletPolicy
from .psbt import PSBT
from ._serialize import deser_string
@@ -24,8 +27,8 @@ def __init__(self, sw: int, data: bytes) -> None:
class TransportClient:
- def __init__(self, interface: Literal['hid', 'tcp'] = "tcp", server: str = "127.0.0.1", port: int = 9999, debug: bool = False):
- self.transport = Transport('hid', debug=debug) if interface == 'hid' else Transport(interface, server, port, debug)
+ def __init__(self, interface: Literal['hid', 'tcp'] = "tcp", *, server: str = "127.0.0.1", port: int = 9999, path: Optional[str] = None, hid: Optional[HID] = None, debug: bool = False):
+ self.transport = Transport('hid', path=path, hid=hid, debug=debug) if interface == 'hid' else Transport(interface, server=server, port=port, debug=debug)
def apdu_exchange(
self, cla: int, ins: int, data: bytes = b"", p1: int = 0, p2: int = 0
@@ -45,6 +48,7 @@ def apdu_exchange_nowait(
def stop(self) -> None:
self.transport.close()
+
def print_apdu(apdu_dict: dict) -> None:
serialized_apdu = b''.join([
apdu_dict["cla"].to_bytes(1, byteorder='big'),
@@ -56,10 +60,25 @@ def print_apdu(apdu_dict: dict) -> None:
])
print(f"=> {serialized_apdu.hex()}")
+
def print_response(sw: int, data: bytes) -> None:
print(f"<= {data.hex()}{sw.to_bytes(2, byteorder='big').hex()}")
+@dataclass(frozen=True)
+class PartialSignature:
+ """Represents a partial signature returned by sign_psbt.
+
+ It always contains a pubkey and a signature.
+ The pubkey
+
+ The tapleaf_hash is also filled if signing a for a tapscript.
+ """
+ pubkey: bytes
+ signature: bytes
+ tapleaf_hash: Optional[bytes] = None
+
+
class Client:
def __init__(self, transport_client: TransportClient, chain: Chain = Chain.MAIN, debug: bool = False) -> None:
self.transport_client = transport_client
@@ -146,12 +165,12 @@ def get_extended_pubkey(self, path: str, display: bool = False) -> str:
raise NotImplementedError
- def register_wallet(self, wallet: Wallet) -> Tuple[bytes, bytes]:
+ def register_wallet(self, wallet: WalletPolicy) -> Tuple[bytes, bytes]:
"""Registers a wallet policy with the user. After approval returns the wallet id and hmac to be stored on the client.
Parameters
----------
- wallet : Wallet
+ wallet : WalletPolicy
The Wallet policy to register on the device.
Returns
@@ -165,7 +184,7 @@ def register_wallet(self, wallet: Wallet) -> Tuple[bytes, bytes]:
def get_wallet_address(
self,
- wallet: Wallet,
+ wallet: WalletPolicy,
wallet_hmac: Optional[bytes],
change: int,
address_index: int,
@@ -176,7 +195,7 @@ def get_wallet_address(
Parameters
----------
- wallet : Wallet
+ wallet : WalletPolicy
The registered wallet policy, or a standard wallet policy.
wallet_hmac: Optional[bytes]
@@ -199,20 +218,21 @@ def get_wallet_address(
raise NotImplementedError
- def sign_psbt(self, psbt: PSBT, wallet: Wallet, wallet_hmac: Optional[bytes]) -> Mapping[int, bytes]:
+ def sign_psbt(self, psbt: Union[PSBT, bytes, str], wallet: WalletPolicy, wallet_hmac: Optional[bytes]) -> List[Tuple[int, PartialSignature]]:
"""Signs a PSBT using a registered wallet (or a standard wallet that does not need registration).
Signature requires explicit approval from the user.
Parameters
----------
- psbt : PSBT
+ psbt : PSBT | bytes | str
A PSBT of version 0 or 2, with all the necessary information to sign the inputs already filled in; what the
required fields changes depending on the type of input.
The non-witness UTXO must be present for both legacy and SegWit inputs, or the hardware wallet will reject
signing (this will change for Taproot inputs).
+ The argument can be either a `PSBT` object, or `bytes`, or a base64-encoded `str`.
- wallet : Wallet
+ wallet : WalletPolicy
The registered wallet policy, or a standard wallet policy.
wallet_hmac: Optional[bytes]
@@ -220,8 +240,10 @@ def sign_psbt(self, psbt: PSBT, wallet: Wallet, wallet_hmac: Optional[bytes]) ->
Returns
-------
- Mapping[int, bytes]
- A mapping that has as keys the indexes of inputs that the Hardware Wallet signed, and the corresponding signatures as values.
+ List[Tuple[int, PartialSignature]]
+ A list of tuples returned by the hardware wallets, where each element is a tuple of:
+ - an integer, the index of the input being signed;
+ - an instance of `PartialSignature`.
"""
raise NotImplementedError
diff --git a/bitcoin_client/ledger_bitcoin/client_command.py b/bitcoin_client/ledger_bitcoin/client_command.py
index 5a50a2fb5..c7a158a1a 100644
--- a/bitcoin_client/ledger_bitcoin/client_command.py
+++ b/bitcoin_client/ledger_bitcoin/client_command.py
@@ -247,8 +247,8 @@ def __init__(self):
self.commands = {cmd.code: cmd for cmd in commands}
def execute(self, hw_response: bytes) -> bytes:
- """Interprets the client command requested by the hardware wallet, returning the appropriet
- response and updating the client interpreter's internal state if appropriate.
+ """Interprets the client command requested by the hardware wallet, returning the appropriate
+ response and updating the client interpreter's internal state if needed.
Parameters
----------
diff --git a/bitcoin_client/ledger_bitcoin/client_legacy.py b/bitcoin_client/ledger_bitcoin/client_legacy.py
index 32101e115..1f21a922a 100644
--- a/bitcoin_client/ledger_bitcoin/client_legacy.py
+++ b/bitcoin_client/ledger_bitcoin/client_legacy.py
@@ -10,14 +10,15 @@
import re
import base64
+from .client_base import PartialSignature
from .client import Client, TransportClient
-from typing import List, Tuple, Mapping, Optional, Union
+from typing import List, Tuple, Optional, Union
from .common import AddressType, Chain, hash160
from .key import ExtendedKey, parse_path
-from .psbt import PSBT
-from .wallet import Wallet, PolicyMapWallet
+from .psbt import PSBT, normalize_psbt
+from .wallet import WalletPolicy
from ._script import is_p2sh, is_witness, is_p2wpkh, is_p2wsh
@@ -26,12 +27,12 @@
from .btchip.bitcoinTransaction import bitcoinTransaction
-def get_address_type_for_policy(policy: PolicyMapWallet) -> AddressType:
- if policy.policy_map == "pkh(@0)":
+def get_address_type_for_policy(policy: WalletPolicy) -> AddressType:
+ if policy.descriptor_template in ["pkh(@0/**)", "pkh(@0/<0;1>/*)"]:
return AddressType.LEGACY
- elif policy.policy_map == "wpkh(@0)":
+ elif policy.descriptor_template in ["wpkh(@0/**)", "wpkh(@0/<0:1>/*)"]:
return AddressType.WIT
- elif policy.policy_map == "sh(wpkh(@0))":
+ elif policy.descriptor_template in ["sh(wpkh(@0/**))", "sh(wpkh(@0/<0;1>/*))"]:
return AddressType.SH_WIT
else:
raise ValueError("Invalid or unsupported policy")
@@ -76,7 +77,7 @@ def __init__(self, comm_client: TransportClient, chain: Chain = Chain.MAIN, debu
self.app = btchip(DongleAdaptor(comm_client))
- if self.app.getAppName() not in ["Bitcoin", "Bitcoin Test", "app"]:
+ if self.app.getAppName() not in ["Bitcoin", "Bitcoin Legacy", "Bitcoin Test", "Bitcoin Test Legacy", "app"]:
raise ValueError("Ledger is not in either the Bitcoin or Bitcoin Testnet app")
def get_extended_pubkey(self, path: str, display: bool = False) -> str:
@@ -116,12 +117,12 @@ def get_extended_pubkey(self, path: str, display: bool = False) -> str:
)
return xpub.to_string()
- def register_wallet(self, wallet: Wallet) -> Tuple[bytes, bytes]:
- raise NotImplementedError # legacy app does not have this functionality
+ def register_wallet(self, wallet: WalletPolicy) -> Tuple[bytes, bytes]:
+ raise NotImplementedError # legacy app does not have this functionality
def get_wallet_address(
self,
- wallet: Wallet,
+ wallet: WalletPolicy,
wallet_hmac: Optional[bytes],
change: int,
address_index: int,
@@ -129,11 +130,11 @@ def get_wallet_address(
) -> str:
# TODO: check keypath
- if wallet_hmac != None or wallet.n_keys != 1:
+ if wallet_hmac is not None or wallet.n_keys != 1:
raise NotImplementedError("Policy wallets are only supported from version 2.0.0. Please update your Ledger hardware wallet")
- if not isinstance(wallet, PolicyMapWallet):
- raise ValueError("Invalid wallet policy type, it must be PolicyMapWallet")
+ if not isinstance(wallet, WalletPolicy):
+ raise ValueError("Invalid wallet policy type, it must be WalletPolicy")
key_info = wallet.keys_info[0]
try:
@@ -153,18 +154,20 @@ def get_wallet_address(
bech32 = addr_type == AddressType.WIT
output = self.app.getWalletPublicKey(f"{key_origin_path}/{change}/{address_index}", display, p2sh_p2wpkh or bech32, bech32)
assert isinstance(output["address"], str)
- return output['address'][12:-2] # HACK: A bug in getWalletPublicKey results in the address being returned as the string "bytearray(b'
')". This extracts the actual address to work around this.
+ return output['address'][12:-2] # HACK: A bug in getWalletPublicKey results in the address being returned as the string "bytearray(b'')". This extracts the actual address to work around this.
- def sign_psbt(self, psbt: PSBT, wallet: Wallet, wallet_hmac: Optional[bytes]) -> Mapping[int, bytes]:
- if wallet_hmac != None or wallet.n_keys != 1:
+ def sign_psbt(self, psbt: Union[PSBT, bytes, str], wallet: WalletPolicy, wallet_hmac: Optional[bytes]) -> List[Tuple[int, PartialSignature]]:
+ if wallet_hmac is not None or wallet.n_keys != 1:
raise NotImplementedError("Policy wallets are only supported from version 2.0.0. Please update your Ledger hardware wallet")
- if not isinstance(wallet, PolicyMapWallet):
- raise ValueError("Invalid wallet policy type, it must be PolicyMapWallet")
+ if not isinstance(wallet, WalletPolicy):
+ raise ValueError("Invalid wallet policy type, it must be WalletPolicy")
- if not wallet.policy_map in ['pkh(@0)', 'wpkh(@0)', 'sh(wpkh(@0))']:
+ if wallet.descriptor_template not in ["pkh(@0/**)", "pkh(@0/<0;1>/*)", "wpkh(@0/**)", "wpkh(@0/<0;1>/*)", "sh(wpkh(@0/**))", "sh(wpkh(@0/<0;1>/*))"]:
raise NotImplementedError("Unsupported policy")
+ psbt = normalize_psbt(psbt)
+
# the rest of the code is basically the HWI code, and it ignores wallet
tx = psbt
@@ -278,7 +281,7 @@ def sign_psbt(self, psbt: PSBT, wallet: Wallet, wallet_hmac: Optional[bytes]) ->
all_signature_attempts[i_num] = signature_attempts
- result = {}
+ result: List[Tuple[int, PartialSignature]] = []
# Sign any segwit inputs
if has_segwit:
@@ -296,22 +299,32 @@ def sign_psbt(self, psbt: PSBT, wallet: Wallet, wallet_hmac: Optional[bytes]) ->
self.app.startUntrustedTransaction(False, 0, [segwit_inputs[i]], script_codes[i], c_tx.nVersion)
# tx.inputs[i].partial_sigs[signature_attempt[1]] = self.app.untrustedHashSign(signature_attempt[0], "", c_tx.nLockTime, 0x01)
- result[i] = self.app.untrustedHashSign(signature_attempt[0], "", c_tx.nLockTime, 0x01)
+
+ partial_sig = PartialSignature(
+ signature=self.app.untrustedHashSign(signature_attempt[0], "", c_tx.nLockTime, 0x01),
+ pubkey=signature_attempt[1]
+ )
+ result.append((i, partial_sig))
elif has_legacy:
first_input = True
# Legacy signing if all inputs are legacy
for i in range(len(legacy_inputs)):
for signature_attempt in all_signature_attempts[i]:
- assert(tx.inputs[i].non_witness_utxo is not None)
+ assert (tx.inputs[i].non_witness_utxo is not None)
self.app.startUntrustedTransaction(first_input, i, legacy_inputs, script_codes[i], c_tx.nVersion)
self.app.finalizeInput(b"DUMMY", -1, -1, change_path, tx_bytes)
#tx.inputs[i].partial_sigs[signature_attempt[1]] = self.app.untrustedHashSign(signature_attempt[0], "", c_tx.nLockTime, 0x01)
- result[i] = self.app.untrustedHashSign(signature_attempt[0], "", c_tx.nLockTime, 0x01)
+
+ partial_sig = PartialSignature(
+ signature=self.app.untrustedHashSign(signature_attempt[0], "", c_tx.nLockTime, 0x01),
+ pubkey=signature_attempt[1]
+ )
+ result.append((i, partial_sig))
first_input = False
- # Send map of input signatures
+ # Send list of input signatures
return result
def get_master_fingerprint(self) -> bytes:
diff --git a/bitcoin_client/ledger_bitcoin/command_builder.py b/bitcoin_client/ledger_bitcoin/command_builder.py
index 2d062bab0..e79e111c0 100644
--- a/bitcoin_client/ledger_bitcoin/command_builder.py
+++ b/bitcoin_client/ledger_bitcoin/command_builder.py
@@ -1,10 +1,12 @@
import enum
from typing import List, Tuple, Mapping, Union, Iterator, Optional
-from .common import bip32_path_from_string, AddressType, sha256, hash256, write_varint
+from .common import bip32_path_from_string, write_varint
from .merkle import get_merkleized_map_commitment, MerkleTree, element_hash
-from .wallet import Wallet
+from .wallet import WalletPolicy
+# p2 encodes the protocol version implemented
+CURRENT_PROTOCOL_VERSION = 1
def chunkify(data: bytes, chunk_len: int) -> Iterator[Tuple[bool, bytes]]:
size: int = len(data)
@@ -54,7 +56,7 @@ def serialize(
cla: int,
ins: Union[int, enum.IntEnum],
p1: int = 0,
- p2: int = 0,
+ p2: int = CURRENT_PROTOCOL_VERSION,
cdata: bytes = b"",
) -> dict:
"""Serialize the whole APDU command (header + data).
@@ -96,7 +98,7 @@ def get_extended_pubkey(self, bip32_path: str, display: bool = False):
cdata=cdata,
)
- def register_wallet(self, wallet: Wallet):
+ def register_wallet(self, wallet: WalletPolicy):
wallet_bytes = wallet.serialize()
return self.serialize(
@@ -107,7 +109,7 @@ def register_wallet(self, wallet: Wallet):
def get_wallet_address(
self,
- wallet: Wallet,
+ wallet: WalletPolicy,
wallet_hmac: Optional[bytes],
address_index: int,
change: bool,
@@ -134,7 +136,7 @@ def sign_psbt(
global_mapping: Mapping[bytes, bytes],
input_mappings: List[Mapping[bytes, bytes]],
output_mappings: List[Mapping[bytes, bytes]],
- wallet: Wallet,
+ wallet: WalletPolicy,
wallet_hmac: Optional[bytes],
):
diff --git a/bitcoin_client/ledger_bitcoin/common.py b/bitcoin_client/ledger_bitcoin/common.py
index c46cda68b..66ad38f07 100644
--- a/bitcoin_client/ledger_bitcoin/common.py
+++ b/bitcoin_client/ledger_bitcoin/common.py
@@ -103,15 +103,6 @@ def read_varint(buf: BytesIO,
return int.from_bytes(b, byteorder="little")
-def read(buf: BytesIO, size: int) -> bytes:
- b: bytes = buf.read(size)
-
- if len(b) < size:
- raise ValueError(f"Cant read {size} bytes in buffer!")
-
- return b
-
-
def read_uint(buf: BytesIO,
bit_len: int,
byteorder: Literal['big', 'little'] = 'little') -> int:
@@ -125,14 +116,19 @@ def read_uint(buf: BytesIO,
def serialize_str(value: str) -> bytes:
- return len(value).to_bytes(1, byteorder="big") + value.encode("latin-1")
+ return len(value.encode()).to_bytes(1, byteorder="big") + value.encode()
def ripemd160(x: bytes) -> bytes:
- h = hashlib.new("ripemd160")
- h.update(x)
- return h.digest()
-
+ try:
+ h = hashlib.new("ripemd160")
+ h.update(x)
+ return h.digest()
+ except BaseException:
+ # ripemd160 is not always present in hashlib.
+ # Fallback to custom implementation if missing.
+ from . import ripemd
+ return ripemd.ripemd160(x)
def sha256(s: bytes) -> bytes:
return hashlib.new('sha256', s).digest()
diff --git a/bitcoin_client/ledger_bitcoin/descriptor.py b/bitcoin_client/ledger_bitcoin/descriptor.py
deleted file mode 100644
index ef85e8f55..000000000
--- a/bitcoin_client/ledger_bitcoin/descriptor.py
+++ /dev/null
@@ -1,633 +0,0 @@
-
-"""
-Original version: https://github.com/bitcoin-core/HWI/blob/3fe369d0379212fae1c72729a179d133b0adc872/hwilib/descriptor.py
-Distributed under the MIT License.
-
-Output Script Descriptors
-*************************
-
-
-HWI has a more limited implementation of descriptors.
-See `Bitcoin Core's documentation `_ for more details on descriptors.
-
-This implementation only supports ``sh()``, ``wsh()``, ``pkh()``, ``wpkh()``, ``multi()``, and ``sortedmulti()`` descriptors.
-Descriptors can be parsed, however the actual scripts are not generated.
-"""
-
-
-from .key import ExtendedKey, KeyOriginInfo, parse_path
-from .common import hash160, sha256
-
-from binascii import unhexlify
-from collections import namedtuple
-from enum import Enum
-from typing import (
- List,
- Optional,
- Tuple,
-)
-
-
-MAX_TAPROOT_NODES = 128
-
-
-ExpandedScripts = namedtuple("ExpandedScripts", ["output_script", "redeem_script", "witness_script"])
-
-def PolyMod(c: int, val: int) -> int:
- """
- :meta private:
- Function to compute modulo over the polynomial used for descriptor checksums
- From: https://github.com/bitcoin/bitcoin/blob/master/src/script/descriptor.cpp
- """
- c0 = c >> 35
- c = ((c & 0x7ffffffff) << 5) ^ val
- if (c0 & 1):
- c ^= 0xf5dee51989
- if (c0 & 2):
- c ^= 0xa9fdca3312
- if (c0 & 4):
- c ^= 0x1bab10e32d
- if (c0 & 8):
- c ^= 0x3706b1677a
- if (c0 & 16):
- c ^= 0x644d626ffd
- return c
-
-def DescriptorChecksum(desc: str) -> str:
- """
- Compute the checksum for a descriptor
- :param desc: The descriptor string to compute a checksum for
- :return: A checksum
- """
- INPUT_CHARSET = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ "
- CHECKSUM_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
-
- c = 1
- cls = 0
- clscount = 0
- for ch in desc:
- pos = INPUT_CHARSET.find(ch)
- if pos == -1:
- return ""
- c = PolyMod(c, pos & 31)
- cls = cls * 3 + (pos >> 5)
- clscount += 1
- if clscount == 3:
- c = PolyMod(c, cls)
- cls = 0
- clscount = 0
- if clscount > 0:
- c = PolyMod(c, cls)
- for j in range(0, 8):
- c = PolyMod(c, 0)
- c ^= 1
-
- ret = [''] * 8
- for j in range(0, 8):
- ret[j] = CHECKSUM_CHARSET[(c >> (5 * (7 - j))) & 31]
- return ''.join(ret)
-
-def AddChecksum(desc: str) -> str:
- """
- Compute and attach the checksum for a descriptor
- :param desc: The descriptor string to add a checksum to
- :return: Descriptor with checksum
- """
- return desc + "#" + DescriptorChecksum(desc)
-
-
-class PubkeyProvider(object):
- """
- A public key expression in a descriptor.
- Can contain the key origin info, the pubkey itself, and subsequent derivation paths for derivation from the pubkey
- The pubkey can be a typical pubkey or an extended pubkey.
- """
-
- def __init__(
- self,
- origin: Optional['KeyOriginInfo'],
- pubkey: str,
- deriv_path: Optional[str]
- ) -> None:
- """
- :param origin: The key origin if one is available
- :param pubkey: The public key. Either a hex string or a serialized extended pubkey
- :param deriv_path: Additional derivation path if the pubkey is an extended pubkey
- """
- self.origin = origin
- self.pubkey = pubkey
- self.deriv_path = deriv_path
-
- # Make ExtendedKey from pubkey if it isn't hex
- self.extkey = None
- try:
- unhexlify(self.pubkey)
- # Is hex, normal pubkey
- except Exception:
- # Not hex, maybe xpub
- self.extkey = ExtendedKey.deserialize(self.pubkey)
-
- @classmethod
- def parse(cls, s: str) -> 'PubkeyProvider':
- """
- Deserialize a key expression from the string into a ``PubkeyProvider``.
- :param s: String containing the key expression
- :return: A new ``PubkeyProvider`` containing the details given by ``s``
- """
- origin = None
- deriv_path = None
-
- if s[0] == "[":
- end = s.index("]")
- origin = KeyOriginInfo.from_string(s[1:end])
- s = s[end + 1:]
-
- pubkey = s
- slash_idx = s.find("/")
- if slash_idx != -1:
- pubkey = s[:slash_idx]
- deriv_path = s[slash_idx:]
-
- return cls(origin, pubkey, deriv_path)
-
- def to_string(self) -> str:
- """
- Serialize the pubkey expression to a string to be used in a descriptor
- :return: The pubkey expression as a string
- """
- s = ""
- if self.origin:
- s += "[{}]".format(self.origin.to_string())
- s += self.pubkey
- if self.deriv_path:
- s += self.deriv_path
- return s
-
- def get_pubkey_bytes(self, pos: int) -> bytes:
- if self.extkey is not None:
- if self.deriv_path is not None:
- path_str = self.deriv_path[1:]
- if path_str[-1] == "*":
- path_str = path_str[-1] + str(pos)
- path = parse_path(path_str)
- child_key = self.extkey.derive_pub_path(path)
- return child_key.pubkey
- else:
- return self.extkey.pubkey
- return unhexlify(self.pubkey)
-
- def get_full_derivation_path(self, pos: int) -> str:
- """
- Returns the full derivation path at the given position, including the origin
- """
- path = self.origin.get_derivation_path() if self.origin is not None else "m/"
- path += self.deriv_path if self.deriv_path is not None else ""
- if path[-1] == "*":
- path = path[:-1] + str(pos)
- return path
-
- def get_full_derivation_int_list(self, pos: int) -> List[int]:
- """
- Returns the full derivation path as an integer list at the given position.
- Includes the origin and master key fingerprint as an int
- """
- path: List[int] = self.origin.get_full_int_list() if self.origin is not None else []
- if self.deriv_path is not None:
- der_split = self.deriv_path.split("/")
- for p in der_split:
- if not p:
- continue
- if p == "*":
- i = pos
- elif p[-1] in "'phHP":
- assert len(p) >= 2
- i = int(p[:-1]) | 0x80000000
- else:
- i = int(p)
- path.append(i)
- return path
-
- def __lt__(self, other: 'PubkeyProvider') -> bool:
- return self.pubkey < other.pubkey
-
-
-class Descriptor(object):
- r"""
- An abstract class for Descriptors themselves.
- Descriptors can contain multiple :class:`PubkeyProvider`\ s and multiple ``Descriptor`` as subdescriptors.
- """
-
- def __init__(
- self,
- pubkeys: List['PubkeyProvider'],
- subdescriptors: List['Descriptor'],
- name: str
- ) -> None:
- r"""
- :param pubkeys: The :class:`PubkeyProvider`\ s that are part of this descriptor
- :param subdescriptor: The ``Descriptor``s that are part of this descriptor
- :param name: The name of the function for this descriptor
- """
- self.pubkeys = pubkeys
- self.subdescriptors = subdescriptors
- self.name = name
-
- def to_string_no_checksum(self) -> str:
- """
- Serializes the descriptor as a string without the descriptor checksum
- :return: The descriptor string
- """
- return "{}({}{})".format(
- self.name,
- ",".join([p.to_string() for p in self.pubkeys]),
- self.subdescriptors[0].to_string_no_checksum() if len(self.subdescriptors) > 0 else ""
- )
-
- def to_string(self) -> str:
- """
- Serializes the descriptor as a string with the checksum
- :return: The descriptor with a checksum
- """
- return AddChecksum(self.to_string_no_checksum())
-
- def expand(self, pos: int) -> "ExpandedScripts":
- """
- Returns the scripts for a descriptor at the given `pos` for ranged descriptors.
- """
- raise NotImplementedError("The Descriptor base class does not implement this method")
-
-
-class PKDescriptor(Descriptor):
- """
- A descriptor for ``pk()`` descriptors
- """
-
- def __init__(
- self,
- pubkey: 'PubkeyProvider'
- ) -> None:
- """
- :param pubkey: The :class:`PubkeyProvider` for this descriptor
- """
- super().__init__([pubkey], [], "pk")
-
-
-class PKHDescriptor(Descriptor):
- """
- A descriptor for ``pkh()`` descriptors
- """
-
- def __init__(
- self,
- pubkey: 'PubkeyProvider'
- ) -> None:
- """
- :param pubkey: The :class:`PubkeyProvider` for this descriptor
- """
- super().__init__([pubkey], [], "pkh")
-
- def expand(self, pos: int) -> "ExpandedScripts":
- script = b"\x76\xa9\x14" + hash160(self.pubkeys[0].get_pubkey_bytes(pos)) + b"\x88\xac"
- return ExpandedScripts(script, None, None)
-
-
-class WPKHDescriptor(Descriptor):
- """
- A descriptor for ``wpkh()`` descriptors
- """
-
- def __init__(
- self,
- pubkey: 'PubkeyProvider'
- ) -> None:
- """
- :param pubkey: The :class:`PubkeyProvider` for this descriptor
- """
- super().__init__([pubkey], [], "wpkh")
-
- def expand(self, pos: int) -> "ExpandedScripts":
- script = b"\x00\x14" + hash160(self.pubkeys[0].get_pubkey_bytes(pos))
- return ExpandedScripts(script, None, None)
-
-
-class MultisigDescriptor(Descriptor):
- """
- A descriptor for ``multi()`` and ``sortedmulti()`` descriptors
- """
-
- def __init__(
- self,
- pubkeys: List['PubkeyProvider'],
- thresh: int,
- is_sorted: bool
- ) -> None:
- r"""
- :param pubkeys: The :class:`PubkeyProvider`\ s for this descriptor
- :param thresh: The number of keys required to sign this multisig
- :param is_sorted: Whether this is a ``sortedmulti()`` descriptor
- """
- super().__init__(pubkeys, [], "sortedmulti" if is_sorted else "multi")
- self.thresh = thresh
- self.is_sorted = is_sorted
- if self.is_sorted:
- self.pubkeys.sort()
-
- def to_string_no_checksum(self) -> str:
- return "{}({},{})".format(self.name, self.thresh, ",".join([p.to_string() for p in self.pubkeys]))
-
- def expand(self, pos: int) -> "ExpandedScripts":
- if self.thresh > 16:
- m = b"\x01" + self.thresh.to_bytes(1, "big")
- else:
- m = (self.thresh + 0x50).to_bytes(1, "big") if self.thresh > 0 else b"\x00"
- n = (len(self.pubkeys) + 0x50).to_bytes(1, "big") if len(self.pubkeys) > 0 else b"\x00"
- script: bytes = m
- der_pks = [p.get_pubkey_bytes(pos) for p in self.pubkeys]
- if self.is_sorted:
- der_pks.sort()
- for pk in der_pks:
- script += len(pk).to_bytes(1, "big") + pk
- script += n + b"\xae"
-
- return ExpandedScripts(script, None, None)
-
-
-class SHDescriptor(Descriptor):
- """
- A descriptor for ``sh()`` descriptors
- """
-
- def __init__(
- self,
- subdescriptor: 'Descriptor'
- ) -> None:
- """
- :param subdescriptor: The :class:`Descriptor` that is a sub-descriptor for this descriptor
- """
- super().__init__([], [subdescriptor], "sh")
-
- def expand(self, pos: int) -> "ExpandedScripts":
- assert len(self.subdescriptors) == 1
- redeem_script, _, witness_script = self.subdescriptors[0].expand(pos)
- script = b"\xa9\x14" + hash160(redeem_script) + b"\x87"
- return ExpandedScripts(script, redeem_script, witness_script)
-
-
-class WSHDescriptor(Descriptor):
- """
- A descriptor for ``wsh()`` descriptors
- """
-
- def __init__(
- self,
- subdescriptor: 'Descriptor'
- ) -> None:
- """
- :param subdescriptor: The :class:`Descriptor` that is a sub-descriptor for this descriptor
- """
- super().__init__([], [subdescriptor], "wsh")
-
- def expand(self, pos: int) -> "ExpandedScripts":
- assert len(self.subdescriptors) == 1
- witness_script, _, _ = self.subdescriptors[0].expand(pos)
- script = b"\x00\x20" + sha256(witness_script)
- return ExpandedScripts(script, None, witness_script)
-
-
-class TRDescriptor(Descriptor):
- """
- A descriptor for ``tr()`` descriptors
- """
-
- def __init__(
- self,
- internal_key: 'PubkeyProvider',
- subdescriptors: List['Descriptor'] = [],
- depths: List[int] = []
- ) -> None:
- """
- :param internal_key: The :class:`PubkeyProvider` that is the internal key for this descriptor
- :param subdescriptors: The :class:`Descriptor`s that are the leaf scripts for this descriptor
- :param depths: The depths of the leaf scripts in the same order as `subdescriptors`
- """
- super().__init__([internal_key], subdescriptors, "tr")
- self.depths = depths
-
- def to_string_no_checksum(self) -> str:
- r = f"{self.name}({self.pubkeys[0].to_string()}"
- path: List[bool] = [] # Track left or right for each depth
- for p, depth in enumerate(self.depths):
- r += ","
- while len(path) <= depth:
- if len(path) > 0:
- r += "{"
- path.append(False)
- r += self.subdescriptors[p].to_string_no_checksum()
- while len(path) > 0 and path[-1]:
- if len(path) > 0:
- r += "}"
- path.pop()
- if len(path) > 0:
- path[-1] = True
- r += ")"
- return r
-
-def _get_func_expr(s: str) -> Tuple[str, str]:
- """
- Get the function name and then the expression inside
- :param s: The string that begins with a function name
- :return: The function name as the first element of the tuple, and the expression contained within the function as the second element
- :raises: ValueError: if a matching pair of parentheses cannot be found
- """
- start = s.index("(")
- end = s.rindex(")")
- return s[0:start], s[start + 1:end]
-
-
-def _get_const(s: str, const: str) -> str:
- """
- Get the first character of the string, make sure it is the expected character,
- and return the rest of the string
- :param s: The string that begins with a constant character
- :param const: The constant character
- :return: The remainder of the string without the constant character
- :raises: ValueError: if the first character is not the constant character
- """
- if s[0] != const:
- raise ValueError(f"Expected '{const}' but got '{s[0]}'")
- return s[1:]
-
-
-def _get_expr(s: str) -> Tuple[str, str]:
- """
- Extract the expression that ``s`` begins with.
- This will return the initial part of ``s``, up to the first comma or closing brace,
- skipping ones that are surrounded by braces.
- :param s: The string to extract the expression from
- :return: A pair with the first item being the extracted expression and the second the rest of the string
- """
- level: int = 0
- for i, c in enumerate(s):
- if c in ["(", "{"]:
- level += 1
- elif level > 0 and c in [")", "}"]:
- level -= 1
- elif level == 0 and c in [")", "}", ","]:
- break
- return s[0:i], s[i:]
-
-def parse_pubkey(expr: str) -> Tuple['PubkeyProvider', str]:
- """
- Parses an individual pubkey expression from a string that may contain more than one pubkey expression.
- :param expr: The expression to parse a pubkey expression from
- :return: The :class:`PubkeyProvider` that is parsed as the first item of a tuple, and the remainder of the expression as the second item.
- """
- end = len(expr)
- comma_idx = expr.find(",")
- next_expr = ""
- if comma_idx != -1:
- end = comma_idx
- next_expr = expr[end + 1:]
- return PubkeyProvider.parse(expr[:end]), next_expr
-
-
-class _ParseDescriptorContext(Enum):
- """
- :meta private:
- Enum representing the level that we are in when parsing a descriptor.
- Some expressions aren't allowed at certain levels, this helps us track those.
- """
-
- TOP = 1
- """The top level, not within any descriptor"""
-
- P2SH = 2
- """Within a ``sh()`` descriptor"""
-
- P2WSH = 3
- """Within a ``wsh()`` descriptor"""
-
- P2TR = 4
- """Within a ``tr()`` descriptor"""
-
-
-def _parse_descriptor(desc: str, ctx: '_ParseDescriptorContext') -> 'Descriptor':
- """
- :meta private:
- Parse a descriptor given the context level we are in.
- Used recursively to parse subdescriptors
- :param desc: The descriptor string to parse
- :param ctx: The :class:`_ParseDescriptorContext` indicating the level we are in
- :return: The parsed descriptor
- :raises: ValueError: if the descriptor is malformed
- """
- func, expr = _get_func_expr(desc)
- if func == "pk":
- pubkey, expr = parse_pubkey(expr)
- if expr:
- raise ValueError("more than one pubkey in pk descriptor")
- return PKDescriptor(pubkey)
- if func == "pkh":
- if not (ctx == _ParseDescriptorContext.TOP or ctx == _ParseDescriptorContext.P2SH or ctx == _ParseDescriptorContext.P2WSH):
- raise ValueError("Can only have pkh at top level, in sh(), or in wsh()")
- pubkey, expr = parse_pubkey(expr)
- if expr:
- raise ValueError("More than one pubkey in pkh descriptor")
- return PKHDescriptor(pubkey)
- if func == "sortedmulti" or func == "multi":
- if not (ctx == _ParseDescriptorContext.TOP or ctx == _ParseDescriptorContext.P2SH or ctx == _ParseDescriptorContext.P2WSH):
- raise ValueError("Can only have multi/sortedmulti at top level, in sh(), or in wsh()")
- is_sorted = func == "sortedmulti"
- comma_idx = expr.index(",")
- thresh = int(expr[:comma_idx])
- expr = expr[comma_idx + 1:]
- pubkeys = []
- while expr:
- pubkey, expr = parse_pubkey(expr)
- pubkeys.append(pubkey)
- if len(pubkeys) == 0 or len(pubkeys) > 16:
- raise ValueError("Cannot have {} keys in a multisig; must have between 1 and 16 keys, inclusive".format(len(pubkeys)))
- elif thresh < 1:
- raise ValueError("Multisig threshold cannot be {}, must be at least 1".format(thresh))
- elif thresh > len(pubkeys):
- raise ValueError("Multisig threshold cannot be larger than the number of keys; threshold is {} but only {} keys specified".format(thresh, len(pubkeys)))
- if ctx == _ParseDescriptorContext.TOP and len(pubkeys) > 3:
- raise ValueError("Cannot have {} pubkeys in bare multisig: only at most 3 pubkeys")
- return MultisigDescriptor(pubkeys, thresh, is_sorted)
- if func == "wpkh":
- if not (ctx == _ParseDescriptorContext.TOP or ctx == _ParseDescriptorContext.P2SH):
- raise ValueError("Can only have wpkh() at top level or inside sh()")
- pubkey, expr = parse_pubkey(expr)
- if expr:
- raise ValueError("More than one pubkey in pkh descriptor")
- return WPKHDescriptor(pubkey)
- if func == "sh":
- if ctx != _ParseDescriptorContext.TOP:
- raise ValueError("Can only have sh() at top level")
- subdesc = _parse_descriptor(expr, _ParseDescriptorContext.P2SH)
- return SHDescriptor(subdesc)
- if func == "wsh":
- if not (ctx == _ParseDescriptorContext.TOP or ctx == _ParseDescriptorContext.P2SH):
- raise ValueError("Can only have wsh() at top level or inside sh()")
- subdesc = _parse_descriptor(expr, _ParseDescriptorContext.P2WSH)
- return WSHDescriptor(subdesc)
- if func == "tr":
- if ctx != _ParseDescriptorContext.TOP:
- raise ValueError("Can only have tr at top level")
- internal_key, expr = parse_pubkey(expr)
- subscripts = []
- depths = []
- if expr:
- # Path from top of the tree to what we're currently processing.
- # branches[i] == False: left branch in the i'th step from the top
- # branches[i] == true: right branch
- branches = []
- while True:
- # Process open braces
- while True:
- try:
- expr = _get_const(expr, "{")
- branches.append(False)
- except ValueError:
- break
- if len(branches) > MAX_TAPROOT_NODES:
- raise ValueError("tr() supports at most {MAX_TAPROOT_NODES} nesting levels")
- # Process script expression
- sarg, expr = _get_expr(expr)
- subscripts.append(_parse_descriptor(sarg, _ParseDescriptorContext.P2TR))
- depths.append(len(branches))
- # Process closing braces
- while len(branches) > 0 and branches[-1]:
- expr = _get_const(expr, "}")
- branches.pop()
- # If we're at the end of a left branch, expect a comma
- if len(branches) > 0 and not branches[-1]:
- expr = _get_const(expr, ",")
- branches[-1] = True
-
- if len(branches) == 0:
- break
- return TRDescriptor(internal_key, subscripts, depths)
- if ctx == _ParseDescriptorContext.P2SH:
- raise ValueError("A function is needed within P2SH")
- elif ctx == _ParseDescriptorContext.P2WSH:
- raise ValueError("A function is needed within P2WSH")
- raise ValueError("{} is not a valid descriptor function".format(func))
-
-
-def parse_descriptor(desc: str) -> 'Descriptor':
- """
- Parse a descriptor string into a :class:`Descriptor`.
- Validates the checksum if one is provided in the string
- :param desc: The descriptor string
- :return: The parsed :class:`Descriptor`
- :raises: ValueError: if the descriptor string is malformed
- """
- i = desc.find("#")
- if i != -1:
- checksum = desc[i + 1:]
- desc = desc[:i]
- computed = DescriptorChecksum(desc)
- if computed != checksum:
- raise ValueError("The checksum does not match; Got {}, expected {}".format(checksum, computed))
- return _parse_descriptor(desc, _ParseDescriptorContext.TOP)
diff --git a/bitcoin_client/ledger_bitcoin/embit/LICENSE b/bitcoin_client/ledger_bitcoin/embit/LICENSE
new file mode 100644
index 000000000..db295028b
--- /dev/null
+++ b/bitcoin_client/ledger_bitcoin/embit/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2020 Stepan Snigirev
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/bitcoin_client/ledger_bitcoin/embit/README.md b/bitcoin_client/ledger_bitcoin/embit/README.md
new file mode 100644
index 000000000..66483faed
--- /dev/null
+++ b/bitcoin_client/ledger_bitcoin/embit/README.md
@@ -0,0 +1,5 @@
+This is a stripped down version of the embit library, cloned at commit [189efc45](https://github.com/diybitcoinhardware/embit/tree/189efc4583d497a2b97632646daf1531d00442b0).
+
+Support for the `0` and `1` miniscript fragments was added after cloning.
+
+All the content of this folder is released according to the [LICENSE](LICENSE), as per the original repository.
diff --git a/tests-legacy/bitcoin_client/__init__.py b/bitcoin_client/ledger_bitcoin/embit/__init__.py
similarity index 100%
rename from tests-legacy/bitcoin_client/__init__.py
rename to bitcoin_client/ledger_bitcoin/embit/__init__.py
diff --git a/bitcoin_client/ledger_bitcoin/embit/base.py b/bitcoin_client/ledger_bitcoin/embit/base.py
new file mode 100644
index 000000000..9dbac7398
--- /dev/null
+++ b/bitcoin_client/ledger_bitcoin/embit/base.py
@@ -0,0 +1,116 @@
+"""Base classes"""
+from io import BytesIO
+from binascii import hexlify, unhexlify
+
+
+class EmbitError(Exception):
+ """Generic Embit error"""
+
+ pass
+
+
+class EmbitBase:
+ @classmethod
+ def read_from(cls, stream, *args, **kwargs):
+ """All classes should be readable from stream"""
+ raise NotImplementedError(
+ "%s doesn't implement reading from stream" % cls.__name__
+ )
+
+ @classmethod
+ def parse(cls, s: bytes, *args, **kwargs):
+ """Parse raw bytes"""
+ stream = BytesIO(s)
+ res = cls.read_from(stream, *args, **kwargs)
+ if len(stream.read(1)) > 0:
+ raise EmbitError("Unexpected extra bytes")
+ return res
+
+ def write_to(self, stream, *args, **kwargs) -> int:
+ """All classes should be writable to stream"""
+ raise NotImplementedError(
+ "%s doesn't implement writing to stream" % type(self).__name__
+ )
+
+ def serialize(self, *args, **kwargs) -> bytes:
+ """Serialize instance to raw bytes"""
+ stream = BytesIO()
+ self.write_to(stream, *args, **kwargs)
+ return stream.getvalue()
+
+ def to_string(self, *args, **kwargs) -> str:
+ """
+ String representation.
+ If not implemented - uses hex or calls to_base58() method if defined.
+ """
+ if hasattr(self, "to_base58"):
+ res = self.to_base58(*args, **kwargs)
+ if not isinstance(res, str):
+ raise ValueError("to_base58() must return string")
+ return res
+ return hexlify(self.serialize(*args, **kwargs)).decode()
+
+ @classmethod
+ def from_string(cls, s, *args, **kwargs):
+ """Create class instance from string"""
+ if hasattr(cls, "from_base58"):
+ return cls.from_base58(s, *args, **kwargs)
+ return cls.parse(unhexlify(s))
+
+ def __str__(self):
+ """Internally calls `to_string()` method with no arguments"""
+ return self.to_string()
+
+ def __repr__(self):
+ try:
+ return type(self).__name__ + "(%s)" % str(self)
+ except:
+ return type(self).__name__ + "()"
+
+ def __eq__(self, other):
+ """Compare two objects by checking their serializations"""
+ if not hasattr(other, "serialize"):
+ return False
+ return self.serialize() == other.serialize()
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+ def __hash__(self):
+ return hash(self.serialize())
+
+
+class EmbitKey(EmbitBase):
+ def sec(self) -> bytes:
+ """
+ Any EmbitKey should implement sec() method that returns
+ a sec-serialized public key
+ """
+ raise NotImplementedError(
+ "%s doesn't implement sec() method" % type(self).__name__
+ )
+
+ def xonly(self) -> bytes:
+ """xonly representation of the key"""
+ return self.sec()[1:33]
+
+ @property
+ def is_private(self) -> bool:
+ """
+ Any EmbitKey should implement `is_private` property to distinguish
+ between private and public keys.
+ """
+ raise NotImplementedError(
+ "%s doesn't implement is_private property" % type(self).__name__
+ )
+
+ def __lt__(self, other):
+ # for lexagraphic ordering
+ return self.sec() < other.sec()
+
+ def __gt__(self, other):
+ # for lexagraphic ordering
+ return self.sec() > other.sec()
+
+ def __hash__(self):
+ return hash(self.serialize())
diff --git a/bitcoin_client/ledger_bitcoin/embit/base58.py b/bitcoin_client/ledger_bitcoin/embit/base58.py
new file mode 100644
index 000000000..196a79dbf
--- /dev/null
+++ b/bitcoin_client/ledger_bitcoin/embit/base58.py
@@ -0,0 +1,79 @@
+# Partially copy-pasted from python-bitcoinlib:
+# https://github.com/petertodd/python-bitcoinlib/blob/master/bitcoin/base58.py
+
+"""Base58 encoding and decoding"""
+
+import binascii
+from . import hashes
+
+B58_DIGITS = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
+
+
+def encode(b: bytes) -> str:
+ """Encode bytes to a base58-encoded string"""
+
+ # Convert big-endian bytes to integer
+ n = int("0x0" + binascii.hexlify(b).decode("utf8"), 16)
+
+ # Divide that integer into bas58
+ chars = []
+ while n > 0:
+ n, r = divmod(n, 58)
+ chars.append(B58_DIGITS[r])
+ result = "".join(chars[::-1])
+
+ pad = 0
+ for c in b:
+ if c == 0:
+ pad += 1
+ else:
+ break
+ return B58_DIGITS[0] * pad + result
+
+
+def decode(s: str) -> bytes:
+ """Decode a base58-encoding string, returning bytes"""
+ if not s:
+ return b""
+
+ # Convert the string to an integer
+ n = 0
+ for c in s:
+ n *= 58
+ if c not in B58_DIGITS:
+ raise ValueError("Character %r is not a valid base58 character" % c)
+ digit = B58_DIGITS.index(c)
+ n += digit
+
+ # Convert the integer to bytes
+ h = "%x" % n
+ if len(h) % 2:
+ h = "0" + h
+ res = binascii.unhexlify(h.encode("utf8"))
+
+ # Add padding back.
+ pad = 0
+ for c in s[:-1]:
+ if c == B58_DIGITS[0]:
+ pad += 1
+ else:
+ break
+ return b"\x00" * pad + res
+
+
+def encode_check(b: bytes) -> str:
+ """Encode bytes to a base58-encoded string with a checksum"""
+ return encode(b + hashes.double_sha256(b)[0:4])
+
+
+def decode_check(s: str) -> bytes:
+ """Decode a base58-encoding string with checksum check.
+ Returns bytes without checksum
+ """
+ b = decode(s)
+ checksum = hashes.double_sha256(b[:-4])[:4]
+ if b[-4:] != checksum:
+ raise ValueError(
+ "Checksum mismatch: expected %r, calculated %r" % (b[-4:], checksum)
+ )
+ return b[:-4]
diff --git a/bitcoin_client/ledger_bitcoin/embit/bech32.py b/bitcoin_client/ledger_bitcoin/embit/bech32.py
new file mode 100644
index 000000000..24ee7fa72
--- /dev/null
+++ b/bitcoin_client/ledger_bitcoin/embit/bech32.py
@@ -0,0 +1,146 @@
+# Copyright (c) 2017 Pieter Wuille
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+"""Reference implementation for Bech32 and segwit addresses."""
+from .misc import const
+
+CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
+BECH32_CONST = const(1)
+BECH32M_CONST = const(0x2BC830A3)
+
+
+class Encoding:
+ """Enumeration type to list the various supported encodings."""
+
+ BECH32 = 1
+ BECH32M = 2
+
+
+def bech32_polymod(values):
+ """Internal function that computes the Bech32 checksum."""
+ generator = [0x3B6A57B2, 0x26508E6D, 0x1EA119FA, 0x3D4233DD, 0x2A1462B3]
+ chk = 1
+ for value in values:
+ top = chk >> 25
+ chk = (chk & 0x1FFFFFF) << 5 ^ value
+ for i in range(5):
+ chk ^= generator[i] if ((top >> i) & 1) else 0
+ return chk
+
+
+def bech32_hrp_expand(hrp: str):
+ """Expand the HRP into values for checksum computation."""
+ return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp]
+
+
+def bech32_verify_checksum(hrp, data):
+ """Verify a checksum given HRP and converted data characters."""
+ check = bech32_polymod(bech32_hrp_expand(hrp) + data)
+ if check == BECH32_CONST:
+ return Encoding.BECH32
+ elif check == BECH32M_CONST:
+ return Encoding.BECH32M
+ else:
+ return None
+
+
+def bech32_create_checksum(encoding, hrp, data):
+ """Compute the checksum values given HRP and data."""
+ values = bech32_hrp_expand(hrp) + data
+ const = BECH32M_CONST if encoding == Encoding.BECH32M else BECH32_CONST
+ polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ const
+ return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)]
+
+
+def bech32_encode(encoding, hrp, data):
+ """Compute a Bech32 or Bech32m string given HRP and data values."""
+ combined = data + bech32_create_checksum(encoding, hrp, data)
+ return hrp + "1" + "".join([CHARSET[d] for d in combined])
+
+
+def bech32_decode(bech):
+ """Validate a Bech32/Bech32m string, and determine HRP and data."""
+ if (any(ord(x) < 33 or ord(x) > 126 for x in bech)) or (
+ bech.lower() != bech and bech.upper() != bech
+ ):
+ return (None, None, None)
+ bech = bech.lower()
+ pos = bech.rfind("1")
+ if pos < 1 or pos + 7 > len(bech) or len(bech) > 90:
+ return (None, None, None)
+ if not all(x in CHARSET for x in bech[pos + 1 :]):
+ return (None, None, None)
+ hrp = bech[:pos]
+ data = [CHARSET.find(x) for x in bech[pos + 1 :]]
+ encoding = bech32_verify_checksum(hrp, data)
+ if encoding is None:
+ return (None, None, None)
+ return (encoding, hrp, data[:-6])
+
+
+def convertbits(data, frombits, tobits, pad=True):
+ """General power-of-2 base conversion."""
+ acc = 0
+ bits = 0
+ ret = []
+ maxv = (1 << tobits) - 1
+ max_acc = (1 << (frombits + tobits - 1)) - 1
+ for value in data:
+ if value < 0 or (value >> frombits):
+ return None
+ acc = ((acc << frombits) | value) & max_acc
+ bits += frombits
+ while bits >= tobits:
+ bits -= tobits
+ ret.append((acc >> bits) & maxv)
+ if pad:
+ if bits:
+ ret.append((acc << (tobits - bits)) & maxv)
+ elif bits >= frombits or ((acc << (tobits - bits)) & maxv):
+ return None
+ return ret
+
+
+def decode(hrp, addr):
+ """Decode a segwit address."""
+ encoding, hrpgot, data = bech32_decode(addr)
+ if hrpgot != hrp:
+ return (None, None)
+ decoded = convertbits(data[1:], 5, 8, False)
+ if decoded is None or len(decoded) < 2 or len(decoded) > 40:
+ return (None, None)
+ if data[0] > 16:
+ return (None, None)
+ if data[0] == 0 and len(decoded) != 20 and len(decoded) != 32:
+ return (None, None)
+ if (data[0] == 0 and encoding != Encoding.BECH32) or (
+ data[0] != 0 and encoding != Encoding.BECH32M
+ ):
+ return (None, None)
+ return (data[0], decoded)
+
+
+def encode(hrp, witver, witprog):
+ """Encode a segwit address."""
+ encoding = Encoding.BECH32 if witver == 0 else Encoding.BECH32M
+ ret = bech32_encode(encoding, hrp, [witver] + convertbits(witprog, 8, 5))
+ if decode(hrp, ret) == (None, None):
+ return None
+ return ret
diff --git a/bitcoin_client/ledger_bitcoin/embit/bip32.py b/bitcoin_client/ledger_bitcoin/embit/bip32.py
new file mode 100644
index 000000000..e31cb7a26
--- /dev/null
+++ b/bitcoin_client/ledger_bitcoin/embit/bip32.py
@@ -0,0 +1,309 @@
+from . import ec
+from .base import EmbitKey, EmbitError
+from .misc import copy, const, secp256k1
+from .networks import NETWORKS
+from . import base58
+from . import hashes
+import hmac
+from binascii import hexlify
+
+HARDENED_INDEX = const(0x80000000)
+
+
+class HDError(EmbitError):
+ pass
+
+
+class HDKey(EmbitKey):
+ """HD Private or Public key"""
+
+ def __init__(
+ self,
+ key: EmbitKey, # more specifically, PrivateKey or PublicKey
+ chain_code: bytes,
+ version=None,
+ depth: int = 0,
+ fingerprint: bytes = b"\x00\x00\x00\x00",
+ child_number: int = 0,
+ ):
+ self.key = key
+ if len(key.serialize()) != 32 and len(key.serialize()) != 33:
+ raise HDError("Invalid key. Should be private or compressed public")
+ if version is not None:
+ self.version = version
+ else:
+ if len(key.serialize()) == 32:
+ self.version = NETWORKS["main"]["xprv"]
+ else:
+ self.version = NETWORKS["main"]["xpub"]
+ self.chain_code = chain_code
+ self.depth = depth
+ self.fingerprint = fingerprint
+ self._my_fingerprint = b""
+ self.child_number = child_number
+ # check that base58[1:4] is "prv" or "pub"
+ if self.is_private and self.to_base58()[1:4] != "prv":
+ raise HDError("Invalid version")
+ if not self.is_private and self.to_base58()[1:4] != "pub":
+ raise HDError("Invalid version")
+
+ @classmethod
+ def from_seed(cls, seed: bytes, version=NETWORKS["main"]["xprv"]):
+ """Creates a root private key from 64-byte seed"""
+ raw = hmac.new(b"Bitcoin seed", seed, digestmod="sha512").digest()
+ private_key = ec.PrivateKey(raw[:32])
+ chain_code = raw[32:]
+ return cls(private_key, chain_code, version=version)
+
+ @classmethod
+ def from_base58(cls, s: str):
+ b = base58.decode_check(s)
+ return cls.parse(b)
+
+ @property
+ def my_fingerprint(self) -> bytes:
+ if not self._my_fingerprint:
+ sec = self.sec()
+ self._my_fingerprint = hashes.hash160(sec)[:4]
+ return self._my_fingerprint
+
+ @property
+ def is_private(self) -> bool:
+ """checks if the HDKey is private or public"""
+ return self.key.is_private
+
+ @property
+ def secret(self):
+ if not self.is_private:
+ raise HDError("Key is not private")
+ return self.key.secret
+
+ def write_to(self, stream, version=None) -> int:
+ if version is None:
+ version = self.version
+ res = stream.write(version)
+ res += stream.write(bytes([self.depth]))
+ res += stream.write(self.fingerprint)
+ res += stream.write(self.child_number.to_bytes(4, "big"))
+ res += stream.write(self.chain_code)
+ if self.is_private:
+ res += stream.write(b"\x00")
+ res += stream.write(self.key.serialize())
+ return res
+
+ def to_base58(self, version=None) -> str:
+ b = self.serialize(version)
+ res = base58.encode_check(b)
+ if res[1:4] == "prv" and not self.is_private:
+ raise HDError("Invalid version for private key")
+ if res[1:4] == "pub" and self.is_private:
+ raise HDError("Invalid version for public key")
+ return res
+
+ @classmethod
+ def from_string(cls, s: str):
+ return cls.from_base58(s)
+
+ def to_string(self, version=None):
+ return self.to_base58(version)
+
+ @classmethod
+ def read_from(cls, stream):
+ version = stream.read(4)
+ depth = stream.read(1)[0]
+ fingerprint = stream.read(4)
+ child_number = int.from_bytes(stream.read(4), "big")
+ chain_code = stream.read(32)
+ k = stream.read(33)
+ if k[0] == 0:
+ key = ec.PrivateKey.parse(k[1:])
+ else:
+ key = ec.PublicKey.parse(k)
+
+ if len(version) < 4 or len(fingerprint) < 4 or len(chain_code) < 32:
+ raise HDError("Not enough bytes")
+ hd = cls(
+ key,
+ chain_code,
+ version=version,
+ depth=depth,
+ fingerprint=fingerprint,
+ child_number=child_number,
+ )
+ subver = hd.to_base58()[1:4]
+ if subver != "prv" and subver != "pub":
+ raise HDError("Invalid version")
+ if depth == 0 and child_number != 0:
+ raise HDError("zero depth with non-zero index")
+ if depth == 0 and fingerprint != b"\x00\x00\x00\x00":
+ raise HDError("zero depth with non-zero parent")
+ return hd
+
+ def to_public(self, version=None):
+ if not self.is_private:
+ raise HDError("Already public")
+ if version is None:
+ # detect network
+ for net in NETWORKS:
+ for k in NETWORKS[net]:
+ if "prv" in k and NETWORKS[net][k] == self.version:
+ # xprv -> xpub, zprv -> zpub etc
+ version = NETWORKS[net][k.replace("prv", "pub")]
+ break
+ if version is None:
+ raise HDError("Can't find proper version. Provide it with version keyword")
+ return self.__class__(
+ self.key.get_public_key(),
+ self.chain_code,
+ version=version,
+ depth=self.depth,
+ fingerprint=self.fingerprint,
+ child_number=self.child_number,
+ )
+
+ def get_public_key(self):
+ return self.key.get_public_key() if self.is_private else self.key
+
+ def sec(self) -> bytes:
+ """Returns SEC serialization of the public key"""
+ return self.key.sec()
+
+ def xonly(self) -> bytes:
+ return self.key.xonly()
+
+ def taproot_tweak(self, h=b""):
+ return HDKey(
+ self.key.taproot_tweak(h),
+ self.chain_code,
+ version=self.version,
+ depth=self.depth,
+ fingerprint=self.fingerprint,
+ child_number=self.child_number,
+ )
+
+ def child(self, index: int, hardened: bool = False):
+ """Derives a child HDKey"""
+ if index > 0xFFFFFFFF:
+ raise HDError("Index should be less then 2^32")
+ if hardened and index < HARDENED_INDEX:
+ index += HARDENED_INDEX
+ if index >= HARDENED_INDEX:
+ hardened = True
+ if hardened and not self.is_private:
+ raise HDError("Can't do hardened with public key")
+
+ # we need pubkey for fingerprint anyways
+ sec = self.sec()
+ fingerprint = hashes.hash160(sec)[:4]
+ if hardened:
+ data = b"\x00" + self.key.serialize() + index.to_bytes(4, "big")
+ else:
+ data = sec + index.to_bytes(4, "big")
+ raw = hmac.new(self.chain_code, data, digestmod="sha512").digest()
+ secret = raw[:32]
+ chain_code = raw[32:]
+ if self.is_private:
+ secret = secp256k1.ec_privkey_add(secret, self.key.serialize())
+ key = ec.PrivateKey(secret)
+ else:
+ # copy of internal secp256k1 point structure
+ point = copy(self.key._point)
+ point = secp256k1.ec_pubkey_add(point, secret)
+ key = ec.PublicKey(point)
+ return HDKey(
+ key,
+ chain_code,
+ version=self.version,
+ depth=self.depth + 1,
+ fingerprint=fingerprint,
+ child_number=index,
+ )
+
+ def derive(self, path):
+ """path: int array or a string starting with m/"""
+ if isinstance(path, str):
+ # string of the form m/44h/0'/ind
+ path = parse_path(path)
+ child = self
+ for idx in path:
+ child = child.child(idx)
+ return child
+
+ def sign(self, msg_hash: bytes) -> ec.Signature:
+ """signs a hash of the message with the private key"""
+ if not self.is_private:
+ raise HDError("HD public key can't sign")
+ return self.key.sign(msg_hash)
+
+ def schnorr_sign(self, msg_hash):
+ if not self.is_private:
+ raise HDError("HD public key can't sign")
+ return self.key.schnorr_sign(msg_hash)
+
+ def verify(self, sig, msg_hash) -> bool:
+ return self.key.verify(sig, msg_hash)
+
+ def schnorr_verify(self, sig, msg_hash) -> bool:
+ return self.key.schnorr_verify(sig, msg_hash)
+
+ def __eq__(self, other):
+ # skip version
+ return self.serialize()[4:] == other.serialize()[4:]
+
+ def __hash__(self):
+ return hash(self.serialize())
+
+
+def detect_version(path, default="xprv", network=None) -> bytes:
+ """
+ Detects slip-132 version from the path for certain network.
+ Trying to be smart, use if you want, but with care.
+ """
+ key = default
+ net = network
+ if network is None:
+ net = NETWORKS["main"]
+ if isinstance(path, str):
+ path = parse_path(path)
+ if len(path) == 0:
+ return network[key]
+ if path[0] == HARDENED_INDEX + 84:
+ key = "z" + default[1:]
+ elif path[0] == HARDENED_INDEX + 49:
+ key = "y" + default[1:]
+ elif path[0] == HARDENED_INDEX + 48:
+ if len(path) >= 4:
+ if path[3] == HARDENED_INDEX + 1:
+ key = "Y" + default[1:]
+ elif path[3] == HARDENED_INDEX + 2:
+ key = "Z" + default[1:]
+ if network is None and len(path) > 1 and path[1] == HARDENED_INDEX + 1:
+ net = NETWORKS["test"]
+ return net[key]
+
+
+def _parse_der_item(e: str) -> int:
+ if e[-1] in {"h", "H", "'"}:
+ return int(e[:-1]) + HARDENED_INDEX
+ else:
+ return int(e)
+
+
+def parse_path(path: str) -> list:
+ """converts derivation path of the form m/44h/1'/0'/0/32 to int array"""
+ arr = path.rstrip("/").split("/")
+ if arr[0] == "m":
+ arr = arr[1:]
+ if len(arr) == 0:
+ return []
+ return [_parse_der_item(e) for e in arr]
+
+
+def path_to_str(path: list, fingerprint=None) -> str:
+ s = "m" if fingerprint is None else hexlify(fingerprint).decode()
+ for el in path:
+ if el >= HARDENED_INDEX:
+ s += "/%dh" % (el - HARDENED_INDEX)
+ else:
+ s += "/%d" % el
+ return s
diff --git a/bitcoin_client/ledger_bitcoin/embit/compact.py b/bitcoin_client/ledger_bitcoin/embit/compact.py
new file mode 100644
index 000000000..0138f394e
--- /dev/null
+++ b/bitcoin_client/ledger_bitcoin/embit/compact.py
@@ -0,0 +1,41 @@
+""" Compact Int parsing / serialization """
+import io
+
+
+def to_bytes(i: int) -> bytes:
+ """encodes an integer as a compact int"""
+ if i < 0:
+ raise ValueError("integer can't be negative: {}".format(i))
+ order = 0
+ while i >> (8 * (2**order)):
+ order += 1
+ if order == 0:
+ if i < 0xFD:
+ return bytes([i])
+ order = 1
+ if order > 3:
+ raise ValueError("integer too large: {}".format(i))
+ return bytes([0xFC + order]) + i.to_bytes(2**order, "little")
+
+
+def from_bytes(b: bytes) -> int:
+ s = io.BytesIO(b)
+ res = read_from(s)
+ if len(s.read(1)) > 0:
+ raise ValueError("Too many bytes")
+ return res
+
+
+def read_from(stream) -> int:
+ """reads a compact integer from a stream"""
+ c = stream.read(1)
+ if not isinstance(c, bytes):
+ raise TypeError("Bytes must be returned from stream.read()")
+ if len(c) != 1:
+ raise RuntimeError("Can't read one byte from the stream")
+ i = c[0]
+ if i >= 0xFD:
+ bytes_to_read = 2 ** (i - 0xFC)
+ return int.from_bytes(stream.read(bytes_to_read), "little")
+ else:
+ return i
diff --git a/bitcoin_client/ledger_bitcoin/embit/descriptor/__init__.py b/bitcoin_client/ledger_bitcoin/embit/descriptor/__init__.py
new file mode 100644
index 000000000..600296d3c
--- /dev/null
+++ b/bitcoin_client/ledger_bitcoin/embit/descriptor/__init__.py
@@ -0,0 +1,3 @@
+from . import miniscript
+from .descriptor import Descriptor
+from .arguments import Key
diff --git a/bitcoin_client/ledger_bitcoin/embit/descriptor/arguments.py b/bitcoin_client/ledger_bitcoin/embit/descriptor/arguments.py
new file mode 100644
index 000000000..3f92500a7
--- /dev/null
+++ b/bitcoin_client/ledger_bitcoin/embit/descriptor/arguments.py
@@ -0,0 +1,515 @@
+from binascii import hexlify, unhexlify
+from .base import DescriptorBase
+from .errors import ArgumentError
+from .. import bip32, ec, compact, hashes
+from ..bip32 import HARDENED_INDEX
+from ..misc import read_until
+
+
+class KeyOrigin:
+ def __init__(self, fingerprint: bytes, derivation: list):
+ self.fingerprint = fingerprint
+ self.derivation = derivation
+
+ @classmethod
+ def from_string(cls, s: str):
+ arr = s.split("/")
+ mfp = unhexlify(arr[0])
+ assert len(mfp) == 4
+ arr[0] = "m"
+ path = "/".join(arr)
+ derivation = bip32.parse_path(path)
+ return cls(mfp, derivation)
+
+ def __str__(self):
+ return bip32.path_to_str(self.derivation, fingerprint=self.fingerprint)
+
+
+class AllowedDerivation(DescriptorBase):
+ # xpub/<0;1>/* - <0;1> is a set of allowed branches, wildcard * is stored as None
+ def __init__(self, indexes=[[0, 1], None]):
+ # check only one wildcard
+ if (
+ len(
+ [i for i in indexes if i is None or (isinstance(i, list) and None in i)]
+ )
+ > 1
+ ):
+ raise ArgumentError("Only one wildcard is allowed")
+ # check only one set is in the derivation
+ if len([i for i in indexes if isinstance(i, list)]) > 1:
+ raise ArgumentError("Only one set of branches is allowed")
+ self.indexes = indexes
+
+ @property
+ def is_wildcard(self):
+ return None in self.indexes
+
+ def fill(self, idx, branch_index=None):
+ # None is ok
+ if idx is not None and (idx < 0 or idx >= HARDENED_INDEX):
+ raise ArgumentError("Hardened indexes are not allowed in wildcard")
+ arr = [i for i in self.indexes]
+ for i, el in enumerate(arr):
+ if el is None:
+ arr[i] = idx
+ if isinstance(el, list):
+ if branch_index is None:
+ arr[i] = el[0]
+ else:
+ if branch_index < 0 or branch_index >= len(el):
+ raise ArgumentError("Invalid branch index")
+ arr[i] = el[branch_index]
+ return arr
+
+ def branch(self, branch_index):
+ arr = self.fill(None, branch_index)
+ return type(self)(arr)
+
+ def check_derivation(self, derivation: list):
+ if len(derivation) != len(self.indexes):
+ return None
+ branch_idx = 0 # default branch if no branches in descriptor
+ idx = None
+ for i, el in enumerate(self.indexes):
+ der = derivation[i]
+ if isinstance(el, int):
+ if el != der:
+ return None
+ # branch
+ elif isinstance(el, list):
+ if der not in el:
+ return None
+ branch_idx = el.index(der)
+ # wildcard
+ elif el is None:
+ idx = der
+ # shouldn't happen
+ else:
+ raise ArgumentError("Strange derivation index...")
+ if branch_idx is not None and idx is not None:
+ return idx, branch_idx
+
+ @classmethod
+ def default(cls):
+ return AllowedDerivation([[0, 1], None])
+
+ @property
+ def branches(self):
+ for el in self.indexes:
+ if isinstance(el, list):
+ return el
+ return None
+
+ @property
+ def has_hardend(self):
+ for idx in self.indexes:
+ if isinstance(idx, int) and idx >= HARDENED_INDEX:
+ return True
+ if (
+ isinstance(idx, list)
+ and len([i for i in idx if i >= HARDENED_INDEX]) > 0
+ ):
+ return True
+ return False
+
+ @classmethod
+ def from_string(cls, der: str, allow_hardened=False, allow_set=True):
+ if len(der) == 0:
+ return None
+ indexes = [
+ cls.parse_element(d, allow_hardened, allow_set) for d in der.split("/")
+ ]
+ return cls(indexes)
+
+ @classmethod
+ def parse_element(cls, d: str, allow_hardened=False, allow_set=True):
+ # wildcard
+ if d == "*":
+ return None
+ # branch set - legacy `{m,n}`
+ if d[0] == "{" and d[-1] == "}":
+ if not allow_set:
+ raise ArgumentError("Set is not allowed in derivation %s" % d)
+ return [
+ cls.parse_element(dd, allow_hardened, allow_set=False)
+ for dd in d[1:-1].split(",")
+ ]
+ # branch set - multipart ``
+ if d[0] == "<" and d[-1] == ">":
+ if not allow_set:
+ raise ArgumentError("Set is not allowed in derivation %s" % d)
+ return [
+ cls.parse_element(dd, allow_hardened, allow_set=False)
+ for dd in d[1:-1].split(";")
+ ]
+ idx = 0
+ if d[-1] in ["h", "H", "'"]:
+ if not allow_hardened:
+ raise ArgumentError("Hardened derivation is not allowed in %s" % d)
+ idx = HARDENED_INDEX
+ d = d[:-1]
+ i = int(d)
+ if i < 0 or i >= HARDENED_INDEX:
+ raise ArgumentError(
+ "Derivation index can be in a range [0, %d)" % HARDENED_INDEX
+ )
+ return idx + i
+
+ def __str__(self):
+ r = ""
+ for idx in self.indexes:
+ if idx is None:
+ r += "/*"
+ if isinstance(idx, int):
+ if idx >= HARDENED_INDEX:
+ r += "/%dh" % (idx - HARDENED_INDEX)
+ else:
+ r += "/%d" % idx
+ if isinstance(idx, list):
+ r += "/<"
+ r += ";".join(
+ [
+ str(i) if i < HARDENED_INDEX else str(i - HARDENED_INDEX) + "h"
+ for i in idx
+ ]
+ )
+ r += ">"
+ return r
+
+
+class Key(DescriptorBase):
+ def __init__(
+ self,
+ key,
+ origin=None,
+ derivation=None,
+ taproot=False,
+ xonly_repr=False,
+ ):
+ self.origin = origin
+ self.key = key
+ self.taproot = taproot
+ self.xonly_repr = xonly_repr and taproot
+ if not hasattr(key, "derive") and derivation:
+ raise ArgumentError("Key %s doesn't support derivation" % key)
+ self.allowed_derivation = derivation
+
+ def __len__(self):
+ return 34 - int(self.taproot) # <33:sec> or <32:xonly>
+
+ @property
+ def my_fingerprint(self):
+ if self.is_extended:
+ return self.key.my_fingerprint
+ return None
+
+ @property
+ def fingerprint(self):
+ if self.origin is not None:
+ return self.origin.fingerprint
+ else:
+ if self.is_extended:
+ return self.key.my_fingerprint
+ return None
+
+ @property
+ def derivation(self):
+ return [] if self.origin is None else self.origin.derivation
+
+ @classmethod
+ def read_from(cls, s, taproot: bool = False):
+ """
+ Reads key argument from stream.
+ If taproot is set to True - allows both x-only and sec pubkeys.
+ If taproot is False - will raise when finds xonly pubkey.
+ """
+ first = s.read(1)
+ origin = None
+ if first == b"[":
+ prefix, char = read_until(s, b"]")
+ if char != b"]":
+ raise ArgumentError("Invalid key - missing ]")
+ origin = KeyOrigin.from_string(prefix.decode())
+ else:
+ s.seek(-1, 1)
+ k, char = read_until(s, b",)/")
+ der = b""
+ # there is a following derivation
+ if char == b"/":
+ der, char = read_until(s, b"<{,)")
+ # legacy branches: {a,b,c...}
+ if char == b"{":
+ der += b"{"
+ branch, char = read_until(s, b"}")
+ if char is None:
+ raise ArgumentError("Failed reading the key, missing }")
+ der += branch + b"}"
+ rest, char = read_until(s, b",)")
+ der += rest
+ # multipart descriptor:
+ elif char == b"<":
+ der += b"<"
+ branch, char = read_until(s, b">")
+ if char is None:
+ raise ArgumentError("Failed reading the key, missing >")
+ der += branch + b">"
+ rest, char = read_until(s, b",)")
+ der += rest
+ if char is not None:
+ s.seek(-1, 1)
+ # parse key
+ k, xonly_repr = cls.parse_key(k, taproot)
+ # parse derivation
+ allow_hardened = isinstance(k, bip32.HDKey) and isinstance(k.key, ec.PrivateKey)
+ derivation = AllowedDerivation.from_string(
+ der.decode(), allow_hardened=allow_hardened
+ )
+ return cls(k, origin, derivation, taproot, xonly_repr)
+
+ @classmethod
+ def parse_key(cls, key: bytes, taproot: bool = False):
+ # convert to string
+ k = key.decode()
+ if len(k) in [66, 130] and k[:2] in ["02", "03", "04"]:
+ # bare public key
+ return ec.PublicKey.parse(unhexlify(k)), False
+ elif taproot and len(k) == 64:
+ # x-only pubkey
+ return ec.PublicKey.parse(b"\x02" + unhexlify(k)), True
+ elif k[1:4] in ["pub", "prv"]:
+ # bip32 key
+ return bip32.HDKey.from_base58(k), False
+ else:
+ return ec.PrivateKey.from_wif(k), False
+
+ @property
+ def is_extended(self):
+ return isinstance(self.key, bip32.HDKey)
+
+ def check_derivation(self, derivation_path):
+ rest = None
+ # full derivation path
+ if self.fingerprint == derivation_path.fingerprint:
+ origin = self.derivation
+ if origin == derivation_path.derivation[: len(origin)]:
+ rest = derivation_path.derivation[len(origin) :]
+ # short derivation path
+ if self.my_fingerprint == derivation_path.fingerprint:
+ rest = derivation_path.derivation
+ if self.allowed_derivation is None or rest is None:
+ return None
+ return self.allowed_derivation.check_derivation(rest)
+
+ def get_public_key(self):
+ return (
+ self.key.get_public_key()
+ if (self.is_extended or self.is_private)
+ else self.key
+ )
+
+ def sec(self):
+ return self.key.sec()
+
+ def xonly(self):
+ return self.key.xonly()
+
+ def taproot_tweak(self, h=b""):
+ assert self.taproot
+ return self.key.taproot_tweak(h)
+
+ def serialize(self):
+ if self.taproot:
+ return self.sec()[1:33]
+ return self.sec()
+
+ def compile(self):
+ d = self.serialize()
+ return compact.to_bytes(len(d)) + d
+
+ @property
+ def prefix(self):
+ if self.origin:
+ return "[%s]" % self.origin
+ return ""
+
+ @property
+ def suffix(self):
+ return "" if self.allowed_derivation is None else str(self.allowed_derivation)
+
+ @property
+ def can_derive(self):
+ return self.allowed_derivation is not None and hasattr(self.key, "derive")
+
+ @property
+ def branches(self):
+ return self.allowed_derivation.branches if self.allowed_derivation else None
+
+ @property
+ def num_branches(self):
+ return 1 if self.branches is None else len(self.branches)
+
+ def branch(self, branch_index=None):
+ der = (
+ self.allowed_derivation.branch(branch_index)
+ if self.allowed_derivation is not None
+ else None
+ )
+ return type(self)(self.key, self.origin, der, self.taproot)
+
+ @property
+ def is_wildcard(self):
+ return self.allowed_derivation.is_wildcard if self.allowed_derivation else False
+
+ def derive(self, idx, branch_index=None):
+ # nothing to derive
+ if self.allowed_derivation is None:
+ return self
+ der = self.allowed_derivation.fill(idx, branch_index=branch_index)
+ k = self.key.derive(der)
+ if self.origin:
+ origin = KeyOrigin(self.origin.fingerprint, self.origin.derivation + der)
+ else:
+ origin = KeyOrigin(self.key.child(0).fingerprint, der)
+ # empty derivation
+ derivation = None
+ return type(self)(k, origin, derivation, self.taproot)
+
+ @property
+ def is_private(self):
+ return isinstance(self.key, ec.PrivateKey) or (
+ self.is_extended and self.key.is_private
+ )
+
+ def to_public(self):
+ if not self.is_private:
+ return self
+ if isinstance(self.key, ec.PrivateKey):
+ return type(self)(
+ self.key.get_public_key(),
+ self.origin,
+ self.allowed_derivation,
+ self.taproot,
+ )
+ else:
+ return type(self)(
+ self.key.to_public(), self.origin, self.allowed_derivation, self.taproot
+ )
+
+ @property
+ def private_key(self):
+ if not self.is_private:
+ raise ArgumentError("Key is not private")
+ # either HDKey.key or just the key
+ return self.key.key if self.is_extended else self.key
+
+ @property
+ def secret(self):
+ return self.private_key.secret
+
+ def to_string(self, version=None):
+ if isinstance(self.key, ec.PublicKey):
+ k = self.key.sec() if not self.xonly_repr else self.key.xonly()
+ return self.prefix + hexlify(k).decode()
+ if isinstance(self.key, bip32.HDKey):
+ return self.prefix + self.key.to_base58(version) + self.suffix
+ if isinstance(self.key, ec.PrivateKey):
+ return self.prefix + self.key.wif()
+ return self.prefix + self.key
+
+ @classmethod
+ def from_string(cls, s, taproot=False):
+ return cls.parse(s.encode(), taproot)
+
+
+class KeyHash(Key):
+ @classmethod
+ def parse_key(cls, k: bytes, *args, **kwargs):
+ # convert to string
+ kd = k.decode()
+ # raw 20-byte hash
+ if len(kd) == 40:
+ return kd, False
+ return super().parse_key(k, *args, **kwargs)
+
+ def serialize(self, *args, **kwargs):
+ if isinstance(self.key, str):
+ return unhexlify(self.key)
+ # TODO: should it be xonly?
+ if self.taproot:
+ return hashes.hash160(self.key.sec()[1:33])
+ return hashes.hash160(self.key.sec())
+
+ def __len__(self):
+ return 21 # <20:pkh>
+
+ def compile(self):
+ d = self.serialize()
+ return compact.to_bytes(len(d)) + d
+
+
+class Number(DescriptorBase):
+ def __init__(self, num):
+ self.num = num
+
+ @classmethod
+ def read_from(cls, s, taproot=False):
+ num = 0
+ char = s.read(1)
+ while char in b"0123456789":
+ num = 10 * num + int(char.decode())
+ char = s.read(1)
+ s.seek(-1, 1)
+ return cls(num)
+
+ def compile(self):
+ if self.num == 0:
+ return b"\x00"
+ if self.num <= 16:
+ return bytes([80 + self.num])
+ b = self.num.to_bytes(32, "little").rstrip(b"\x00")
+ if b[-1] >= 128:
+ b += b"\x00"
+ return bytes([len(b)]) + b
+
+ def __len__(self):
+ return len(self.compile())
+
+ def __str__(self):
+ return "%d" % self.num
+
+
+class Raw(DescriptorBase):
+ LEN = 32
+
+ def __init__(self, raw):
+ if len(raw) != self.LEN * 2:
+ raise ArgumentError("Invalid raw element length: %d" % len(raw))
+ self.raw = unhexlify(raw)
+
+ @classmethod
+ def read_from(cls, s, taproot=False):
+ return cls(s.read(2 * cls.LEN).decode())
+
+ def __str__(self):
+ return hexlify(self.raw).decode()
+
+ def compile(self):
+ return compact.to_bytes(len(self.raw)) + self.raw
+
+ def __len__(self):
+ return len(compact.to_bytes(self.LEN)) + self.LEN
+
+
+class Raw32(Raw):
+ LEN = 32
+
+ def __len__(self):
+ return 33
+
+
+class Raw20(Raw):
+ LEN = 20
+
+ def __len__(self):
+ return 21
diff --git a/bitcoin_client/ledger_bitcoin/embit/descriptor/base.py b/bitcoin_client/ledger_bitcoin/embit/descriptor/base.py
new file mode 100644
index 000000000..0c203b21b
--- /dev/null
+++ b/bitcoin_client/ledger_bitcoin/embit/descriptor/base.py
@@ -0,0 +1,22 @@
+from io import BytesIO
+from ..base import EmbitBase
+
+
+class DescriptorBase(EmbitBase):
+ """
+ Descriptor is purely text-based, so parse/serialize do
+ the same as from/to_string, just returning ascii bytes
+ instead of ascii string.
+ """
+
+ @classmethod
+ def from_string(cls, s: str, *args, **kwargs):
+ return cls.parse(s.encode(), *args, **kwargs)
+
+ def serialize(self, *args, **kwargs) -> bytes:
+ stream = BytesIO()
+ self.write_to(stream)
+ return stream.getvalue()
+
+ def to_string(self, *args, **kwargs) -> str:
+ return self.serialize().decode()
diff --git a/bitcoin_client/ledger_bitcoin/embit/descriptor/checksum.py b/bitcoin_client/ledger_bitcoin/embit/descriptor/checksum.py
new file mode 100644
index 000000000..1e487ea51
--- /dev/null
+++ b/bitcoin_client/ledger_bitcoin/embit/descriptor/checksum.py
@@ -0,0 +1,56 @@
+from .errors import DescriptorError
+
+
+def polymod(c: int, val: int) -> int:
+ c0 = c >> 35
+ c = ((c & 0x7FFFFFFFF) << 5) ^ val
+ if c0 & 1:
+ c ^= 0xF5DEE51989
+ if c0 & 2:
+ c ^= 0xA9FDCA3312
+ if c0 & 4:
+ c ^= 0x1BAB10E32D
+ if c0 & 8:
+ c ^= 0x3706B1677A
+ if c0 & 16:
+ c ^= 0x644D626FFD
+ return c
+
+
+def checksum(desc: str) -> str:
+ """Calculate checksum of desciptor string"""
+ INPUT_CHARSET = (
+ "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVW"
+ 'XYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#"\\ '
+ )
+ CHECKSUM_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
+
+ c = 1
+ cls = 0
+ clscount = 0
+ for ch in desc:
+ pos = INPUT_CHARSET.find(ch)
+ if pos == -1:
+ raise DescriptorError("Invalid character '%s' in the input string" % ch)
+ c = polymod(c, pos & 31)
+ cls = cls * 3 + (pos >> 5)
+ clscount += 1
+ if clscount == 3:
+ c = polymod(c, cls)
+ cls = 0
+ clscount = 0
+ if clscount > 0:
+ c = polymod(c, cls)
+ for j in range(0, 8):
+ c = polymod(c, 0)
+ c ^= 1
+
+ ret = [CHECKSUM_CHARSET[(c >> (5 * (7 - j))) & 31] for j in range(0, 8)]
+ return "".join(ret)
+
+
+def add_checksum(desc: str) -> str:
+ """Add checksum to descriptor string"""
+ if "#" in desc:
+ desc = desc.split("#")[0]
+ return desc + "#" + checksum(desc)
diff --git a/bitcoin_client/ledger_bitcoin/embit/descriptor/descriptor.py b/bitcoin_client/ledger_bitcoin/embit/descriptor/descriptor.py
new file mode 100644
index 000000000..9f585dc5e
--- /dev/null
+++ b/bitcoin_client/ledger_bitcoin/embit/descriptor/descriptor.py
@@ -0,0 +1,384 @@
+from io import BytesIO
+from .. import script
+from ..networks import NETWORKS
+from .errors import DescriptorError
+from .base import DescriptorBase
+from .miniscript import Miniscript, Multi, Sortedmulti
+from .arguments import Key
+from .taptree import TapTree
+
+
+class Descriptor(DescriptorBase):
+ def __init__(
+ self,
+ miniscript=None,
+ sh=False,
+ wsh=True,
+ key=None,
+ wpkh=True,
+ taproot=False,
+ taptree=None,
+ ):
+ # TODO: add support for taproot scripts
+ # Should:
+ # - accept taptree without a key
+ # - accept key without taptree
+ # - raise if miniscript is not None, but taproot=True
+ # - raise if taptree is not None, but taproot=False
+ if key is None and miniscript is None and taptree is None:
+ raise DescriptorError("Provide a key, miniscript or taptree")
+ if miniscript is not None:
+ # will raise if can't verify
+ miniscript.verify()
+ if miniscript.type != "B":
+ raise DescriptorError("Top level miniscript should be 'B'")
+ # check all branches have the same length
+ branches = {
+ len(k.branches) for k in miniscript.keys if k.branches is not None
+ }
+ if len(branches) > 1:
+ raise DescriptorError("All branches should have the same length")
+ self.sh = sh
+ self.wsh = wsh
+ self.key = key
+ self.miniscript = miniscript
+ self.wpkh = wpkh
+ self.taproot = taproot
+ self.taptree = taptree or TapTree()
+ # make sure all keys are either taproot or not
+ for k in self.keys:
+ k.taproot = taproot
+
+ @property
+ def script_len(self):
+ if self.taproot:
+ return 34 # OP_1 <32:xonly>
+ if self.miniscript:
+ return len(self.miniscript)
+ if self.wpkh:
+ return 22 # 00 <20:pkh>
+ return 25 # OP_DUP OP_HASH160 <20:pkh> OP_EQUALVERIFY OP_CHECKSIG
+
+ @property
+ def num_branches(self):
+ return max([k.num_branches for k in self.keys])
+
+ def branch(self, branch_index=None):
+ if self.miniscript:
+ return type(self)(
+ self.miniscript.branch(branch_index),
+ self.sh,
+ self.wsh,
+ None,
+ self.wpkh,
+ self.taproot,
+ )
+ else:
+ return type(self)(
+ None,
+ self.sh,
+ self.wsh,
+ self.key.branch(branch_index),
+ self.wpkh,
+ self.taproot,
+ self.taptree.branch(branch_index),
+ )
+
+ @property
+ def is_wildcard(self):
+ return any([key.is_wildcard for key in self.keys])
+
+ @property
+ def is_wrapped(self):
+ return self.sh and self.is_segwit
+
+ @property
+ def is_legacy(self):
+ return not (self.is_segwit or self.is_taproot)
+
+ @property
+ def is_segwit(self):
+ return (
+ (self.wsh and self.miniscript) or (self.wpkh and self.key) or self.taproot
+ )
+
+ @property
+ def is_pkh(self):
+ return self.key is not None and not self.taproot
+
+ @property
+ def is_taproot(self):
+ return self.taproot
+
+ @property
+ def is_basic_multisig(self) -> bool:
+ # TODO: should be true for taproot basic multisig with NUMS as internal key
+ # Sortedmulti is subclass of Multi
+ return bool(self.miniscript and isinstance(self.miniscript, Multi))
+
+ @property
+ def is_sorted(self) -> bool:
+ return bool(self.is_basic_multisig and isinstance(self.miniscript, Sortedmulti))
+
+ def scriptpubkey_type(self):
+ if self.is_taproot:
+ return "p2tr"
+ if self.sh:
+ return "p2sh"
+ if self.is_pkh:
+ if self.is_legacy:
+ return "p2pkh"
+ if self.is_segwit:
+ return "p2wpkh"
+ else:
+ return "p2wsh"
+
+ @property
+ def brief_policy(self):
+ if self.taptree:
+ return "taptree"
+ if self.key:
+ return "single key"
+ if self.is_basic_multisig:
+ return (
+ str(self.miniscript.args[0])
+ + " of "
+ + str(len(self.keys))
+ + " multisig"
+ + (" (sorted)" if self.is_sorted else "")
+ )
+ return "miniscript"
+
+ @property
+ def full_policy(self):
+ if (self.key and not self.taptree) or self.is_basic_multisig:
+ return self.brief_policy
+ s = str(self.miniscript or self)
+ for i, k in enumerate(self.keys):
+ s = s.replace(str(k), chr(65 + i))
+ return s
+
+ def derive(self, idx, branch_index=None):
+ if self.miniscript:
+ return type(self)(
+ self.miniscript.derive(idx, branch_index),
+ self.sh,
+ self.wsh,
+ None,
+ self.wpkh,
+ self.taproot,
+ )
+ else:
+ return type(self)(
+ None,
+ self.sh,
+ self.wsh,
+ self.key.derive(idx, branch_index),
+ self.wpkh,
+ self.taproot,
+ self.taptree.derive(idx, branch_index),
+ )
+
+ def to_public(self):
+ if self.miniscript:
+ return type(self)(
+ self.miniscript.to_public(),
+ self.sh,
+ self.wsh,
+ None,
+ self.wpkh,
+ self.taproot,
+ )
+ else:
+ return type(self)(
+ None,
+ self.sh,
+ self.wsh,
+ self.key.to_public(),
+ self.wpkh,
+ self.taproot,
+ self.taptree.to_public(),
+ )
+
+ def owns(self, psbt_scope):
+ """Checks if psbt input or output belongs to this descriptor"""
+ # we can't check if we don't know script_pubkey
+ if psbt_scope.script_pubkey is None:
+ return False
+ # quick check of script_pubkey type
+ if psbt_scope.script_pubkey.script_type() != self.scriptpubkey_type():
+ return False
+ for pub, der in psbt_scope.bip32_derivations.items():
+ # check of the fingerprints
+ for k in self.keys:
+ if not k.is_extended:
+ continue
+ res = k.check_derivation(der)
+ if res:
+ idx, branch_idx = res
+ sc = self.derive(idx, branch_index=branch_idx).script_pubkey()
+ # if derivation is found but scriptpubkey doesn't match - fail
+ return sc == psbt_scope.script_pubkey
+ for pub, (leafs, der) in psbt_scope.taproot_bip32_derivations.items():
+ # check of the fingerprints
+ for k in self.keys:
+ if not k.is_extended:
+ continue
+ res = k.check_derivation(der)
+ if res:
+ idx, branch_idx = res
+ sc = self.derive(idx, branch_index=branch_idx).script_pubkey()
+ # if derivation is found but scriptpubkey doesn't match - fail
+ return sc == psbt_scope.script_pubkey
+ return False
+
+ def check_derivation(self, derivation_path):
+ for k in self.keys:
+ # returns a tuple branch_idx, idx
+ der = k.check_derivation(derivation_path)
+ if der is not None:
+ return der
+ return None
+
+ def witness_script(self):
+ if self.wsh and self.miniscript is not None:
+ return script.Script(self.miniscript.compile())
+
+ def redeem_script(self):
+ if not self.sh:
+ return None
+ if self.miniscript:
+ if not self.wsh:
+ return script.Script(self.miniscript.compile())
+ else:
+ return script.p2wsh(script.Script(self.miniscript.compile()))
+ else:
+ return script.p2wpkh(self.key)
+
+ def script_pubkey(self):
+ # covers sh-wpkh, sh and sh-wsh
+ if self.taproot:
+ return script.p2tr(self.key, self.taptree)
+ if self.sh:
+ return script.p2sh(self.redeem_script())
+ if self.wsh:
+ return script.p2wsh(self.witness_script())
+ if self.miniscript:
+ return script.Script(self.miniscript.compile())
+ if self.wpkh:
+ return script.p2wpkh(self.key)
+ return script.p2pkh(self.key)
+
+ def address(self, network=NETWORKS["main"]):
+ return self.script_pubkey().address(network)
+
+ @property
+ def keys(self):
+ if self.taptree and self.key:
+ return [self.key] + self.taptree.keys
+ elif self.taptree:
+ return self.taptree.keys
+ elif self.key:
+ return [self.key]
+ return self.miniscript.keys
+
+ @classmethod
+ def from_string(cls, desc):
+ s = BytesIO(desc.encode())
+ res = cls.read_from(s)
+ left = s.read()
+ if len(left) > 0 and not left.startswith(b"#"):
+ raise DescriptorError("Unexpected characters after descriptor: %r" % left)
+ return res
+
+ @classmethod
+ def read_from(cls, s):
+ # starts with sh(wsh()), sh() or wsh()
+ start = s.read(7)
+ sh = False
+ wsh = False
+ wpkh = False
+ is_miniscript = True
+ taproot = False
+ taptree = TapTree()
+ if start.startswith(b"tr("):
+ taproot = True
+ s.seek(-4, 1)
+ elif start.startswith(b"sh(wsh("):
+ sh = True
+ wsh = True
+ elif start.startswith(b"wsh("):
+ sh = False
+ wsh = True
+ s.seek(-3, 1)
+ elif start.startswith(b"sh(wpkh"):
+ is_miniscript = False
+ sh = True
+ wpkh = True
+ assert s.read(1) == b"("
+ elif start.startswith(b"wpkh("):
+ is_miniscript = False
+ wpkh = True
+ s.seek(-2, 1)
+ elif start.startswith(b"pkh("):
+ is_miniscript = False
+ s.seek(-3, 1)
+ elif start.startswith(b"sh("):
+ sh = True
+ wsh = False
+ s.seek(-4, 1)
+ else:
+ raise ValueError("Invalid descriptor (starts with '%s')" % start.decode())
+ # taproot always has a key, and may have taptree miniscript
+ if taproot:
+ miniscript = None
+ key = Key.read_from(s, taproot=True)
+ nbrackets = 1
+ c = s.read(1)
+ # TODO: should it be ok to pass just taptree without a key?
+ # check if we have taptree after the key
+ if c != b",":
+ s.seek(-1, 1)
+ else:
+ taptree = TapTree.read_from(s)
+ elif is_miniscript:
+ miniscript = Miniscript.read_from(s)
+ key = None
+ nbrackets = int(sh) + int(wsh)
+ # single key for sure
+ else:
+ miniscript = None
+ key = Key.read_from(s, taproot=taproot)
+ nbrackets = 1 + int(sh)
+ end = s.read(nbrackets)
+ if end != b")" * nbrackets:
+ raise ValueError(
+ "Invalid descriptor (expected ')' but ends with '%s')" % end.decode()
+ )
+ return cls(
+ miniscript,
+ sh=sh,
+ wsh=wsh,
+ key=key,
+ wpkh=wpkh,
+ taproot=taproot,
+ taptree=taptree,
+ )
+
+ def to_string(self):
+ if self.taproot:
+ if self.taptree:
+ return "tr(%s,%s)" % (self.key, self.taptree)
+ return "tr(%s)" % self.key
+ if self.miniscript is not None:
+ res = str(self.miniscript)
+ if self.wsh:
+ res = "wsh(%s)" % res
+ else:
+ if self.wpkh:
+ res = "wpkh(%s)" % self.key
+ else:
+ res = "pkh(%s)" % self.key
+ if self.sh:
+ res = "sh(%s)" % res
+ return res
diff --git a/bitcoin_client/ledger_bitcoin/embit/descriptor/errors.py b/bitcoin_client/ledger_bitcoin/embit/descriptor/errors.py
new file mode 100644
index 000000000..b125f9f6c
--- /dev/null
+++ b/bitcoin_client/ledger_bitcoin/embit/descriptor/errors.py
@@ -0,0 +1,17 @@
+from ..base import EmbitError
+
+
+class DescriptorError(EmbitError):
+ pass
+
+
+class MiniscriptError(DescriptorError):
+ pass
+
+
+class ArgumentError(MiniscriptError):
+ pass
+
+
+class KeyError(ArgumentError):
+ pass
diff --git a/bitcoin_client/ledger_bitcoin/embit/descriptor/miniscript.py b/bitcoin_client/ledger_bitcoin/embit/descriptor/miniscript.py
new file mode 100644
index 000000000..397317f52
--- /dev/null
+++ b/bitcoin_client/ledger_bitcoin/embit/descriptor/miniscript.py
@@ -0,0 +1,1070 @@
+from ..misc import read_until
+from .errors import MiniscriptError
+from .base import DescriptorBase
+from .arguments import Key, KeyHash, Number, Raw32, Raw20
+
+
+class Miniscript(DescriptorBase):
+ def __init__(self, *args, **kwargs):
+ self.args = args
+ self.taproot = kwargs.get("taproot", False)
+
+ def compile(self):
+ return self.inner_compile()
+
+ def verify(self):
+ for arg in self.args:
+ if isinstance(arg, Miniscript):
+ arg.verify()
+
+ @property
+ def keys(self):
+ return sum(
+ [arg.keys for arg in self.args if isinstance(arg, Miniscript)],
+ [k for k in self.args if isinstance(k, Key) or isinstance(k, KeyHash)],
+ )
+
+ def derive(self, idx, branch_index=None):
+ args = [
+ arg.derive(idx, branch_index) if hasattr(arg, "derive") else arg
+ for arg in self.args
+ ]
+ return type(self)(*args, taproot=self.taproot)
+
+ def to_public(self):
+ args = [
+ arg.to_public() if hasattr(arg, "to_public") else arg for arg in self.args
+ ]
+ return type(self)(*args, taproot=self.taproot)
+
+ def branch(self, branch_index):
+ args = [
+ arg.branch(branch_index) if hasattr(arg, "branch") else arg
+ for arg in self.args
+ ]
+ return type(self)(*args, taproot=self.taproot)
+
+ @property
+ def properties(self):
+ return self.PROPS
+
+ @property
+ def type(self):
+ return self.TYPE
+
+ @classmethod
+ def read_from(cls, s, taproot=False):
+ op, char = read_until(s, b"(,)")
+ op = op.decode()
+ wrappers = ""
+ if ":" in op:
+ wrappers, op = op.split(":")
+ if op not in OPERATOR_NAMES:
+ raise MiniscriptError("Unknown operator '%s'" % op)
+ # number of arguments, classes of args, compile fn, type, validity checker
+ MiniscriptCls = OPERATORS[OPERATOR_NAMES.index(op)]
+ if MiniscriptCls.NARGS != 0 and char != b"(":
+ raise MiniscriptError("Missing operator")
+
+ if MiniscriptCls.NARGS is None or MiniscriptCls.NARGS > 0:
+ args = MiniscriptCls.read_arguments(s, taproot=taproot)
+ else:
+ s.seek(-1, 1)
+ args = []
+ miniscript = MiniscriptCls(*args, taproot=taproot)
+ for w in reversed(wrappers):
+ if w not in WRAPPER_NAMES:
+ raise MiniscriptError("Unknown wrapper")
+ WrapperCls = WRAPPERS[WRAPPER_NAMES.index(w)]
+ miniscript = WrapperCls(miniscript, taproot=taproot)
+ return miniscript
+
+ @classmethod
+ def read_arguments(cls, s, taproot=False):
+ args = []
+ if cls.NARGS is None:
+ if type(cls.ARGCLS) == tuple:
+ firstcls, nextcls = cls.ARGCLS
+ else:
+ firstcls, nextcls = cls.ARGCLS, cls.ARGCLS
+ args.append(firstcls.read_from(s, taproot=taproot))
+ while True:
+ char = s.read(1)
+ if char == b",":
+ args.append(nextcls.read_from(s, taproot=taproot))
+ elif char == b")":
+ break
+ else:
+ raise MiniscriptError(
+ "Expected , or ), got: %s" % (char + s.read())
+ )
+ else:
+ for i in range(cls.NARGS):
+ args.append(cls.ARGCLS.read_from(s, taproot=taproot))
+ if i < cls.NARGS - 1:
+ char = s.read(1)
+ if char != b",":
+ raise MiniscriptError("Missing arguments, %s" % char)
+ char = s.read(1)
+ if char != b")":
+ raise MiniscriptError("Expected ) got %s" % (char + s.read()))
+ return args
+
+ def __str__(self):
+ return type(self).NAME + "(" + ",".join([str(arg) for arg in self.args]) + ")"
+
+ def __len__(self):
+ """Length of the compiled script, override this if you know the length"""
+ return len(self.compile())
+
+ def len_args(self):
+ return sum([len(arg) for arg in self.args])
+
+
+########### Known fragments (miniscript operators) ##############
+
+
+class OneArg(Miniscript):
+ NARGS = 1
+
+ # small handy functions
+ @property
+ def arg(self):
+ return self.args[0]
+
+ @property
+ def carg(self):
+ return self.arg.compile()
+
+
+class NumberZero(Miniscript):
+ # 0
+
+ NARGS = 0
+ NAME = "0"
+ TYPE = "B"
+ PROPS = "zud"
+
+ def inner_compile(self):
+ return b"\x00"
+
+ def __len__(self):
+ return 1
+
+
+class NumberOne(Miniscript):
+ # 1
+
+ NARGS = 0
+ NAME = "1"
+ TYPE = "B"
+ PROPS = "zu"
+
+ def inner_compile(self):
+ return b"\x51"
+
+ def __len__(self):
+ return 1
+
+
+class PkK(OneArg):
+ #
+ NAME = "pk_k"
+ ARGCLS = Key
+ TYPE = "K"
+ PROPS = "ondu"
+
+ def inner_compile(self):
+ return self.carg
+
+ def __len__(self):
+ return self.len_args()
+
+
+class PkH(OneArg):
+ # DUP HASH160 EQUALVERIFY
+ NAME = "pk_h"
+ ARGCLS = KeyHash
+ TYPE = "K"
+ PROPS = "ndu"
+
+ def inner_compile(self):
+ return b"\x76\xa9" + self.carg + b"\x88"
+
+ def __len__(self):
+ return self.len_args() + 3
+
+
+class Older(OneArg):
+ # CHECKSEQUENCEVERIFY
+ NAME = "older"
+ ARGCLS = Number
+ TYPE = "B"
+ PROPS = "z"
+
+ def inner_compile(self):
+ return self.carg + b"\xb2"
+
+ def verify(self):
+ super().verify()
+ if (self.arg.num < 1) or (self.arg.num >= 0x80000000):
+ raise MiniscriptError(
+ "%s should have an argument in range [1, 0x80000000)" % self.NAME
+ )
+
+ def __len__(self):
+ return self.len_args() + 1
+
+
+class After(Older):
+ # CHECKLOCKTIMEVERIFY
+ NAME = "after"
+
+ def inner_compile(self):
+ return self.carg + b"\xb1"
+
+
+class Sha256(OneArg):
+ # SIZE <32> EQUALVERIFY SHA256 EQUAL
+ NAME = "sha256"
+ ARGCLS = Raw32
+ TYPE = "B"
+ PROPS = "ondu"
+
+ def inner_compile(self):
+ return b"\x82" + Number(32).compile() + b"\x88\xa8" + self.carg + b"\x87"
+
+ def __len__(self):
+ return self.len_args() + 6
+
+
+class Hash256(Sha256):
+ # SIZE <32> EQUALVERIFY HASH256 EQUAL
+ NAME = "hash256"
+
+ def inner_compile(self):
+ return b"\x82" + Number(32).compile() + b"\x88\xaa" + self.carg + b"\x87"
+
+
+class Ripemd160(Sha256):
+ # SIZE <32> EQUALVERIFY RIPEMD160 EQUAL
+ NAME = "ripemd160"
+ ARGCLS = Raw20
+
+ def inner_compile(self):
+ return b"\x82" + Number(32).compile() + b"\x88\xa6" + self.carg + b"\x87"
+
+
+class Hash160(Ripemd160):
+ # SIZE <32> EQUALVERIFY HASH160 EQUAL
+ NAME = "hash160"
+
+ def inner_compile(self):
+ return b"\x82" + Number(32).compile() + b"\x88\xa9" + self.carg + b"\x87"
+
+
+class AndOr(Miniscript):
+ # [X] NOTIF [Z] ELSE [Y] ENDIF
+ NAME = "andor"
+ NARGS = 3
+ ARGCLS = Miniscript
+
+ @property
+ def type(self):
+ # same as Y/Z
+ return self.args[1].type
+
+ def verify(self):
+ # requires: X is Bdu; Y and Z are both B, K, or V
+ super().verify()
+ if self.args[0].type != "B":
+ raise MiniscriptError("andor: X should be 'B'")
+ px = self.args[0].properties
+ if "d" not in px and "u" not in px:
+ raise MiniscriptError("andor: X should be 'du'")
+ if self.args[1].type != self.args[2].type:
+ raise MiniscriptError("andor: Y and Z should have the same types")
+ if self.args[1].type not in "BKV":
+ raise MiniscriptError("andor: Y and Z should be B K or V")
+
+ @property
+ def properties(self):
+ # props: z=zXzYzZ; o=zXoYoZ or oXzYzZ; u=uYuZ; d=dZ
+ props = ""
+ px, py, pz = [arg.properties for arg in self.args]
+ if "z" in px and "z" in py and "z" in pz:
+ props += "z"
+ if ("z" in px and "o" in py and "o" in pz) or (
+ "o" in px and "z" in py and "z" in pz
+ ):
+ props += "o"
+ if "u" in py and "u" in pz:
+ props += "u"
+ if "d" in pz:
+ props += "d"
+ return props
+
+ def inner_compile(self):
+ return (
+ self.args[0].compile()
+ + b"\x64"
+ + self.args[2].compile()
+ + b"\x67"
+ + self.args[1].compile()
+ + b"\x68"
+ )
+
+ def __len__(self):
+ return self.len_args() + 3
+
+
+class AndV(Miniscript):
+ # [X] [Y]
+ NAME = "and_v"
+ NARGS = 2
+ ARGCLS = Miniscript
+
+ def inner_compile(self):
+ return self.args[0].compile() + self.args[1].compile()
+
+ def __len__(self):
+ return self.len_args()
+
+ def verify(self):
+ # X is V; Y is B, K, or V
+ super().verify()
+ if self.args[0].type != "V":
+ raise MiniscriptError("and_v: X should be 'V'")
+ if self.args[1].type not in "BKV":
+ raise MiniscriptError("and_v: Y should be B K or V")
+
+ @property
+ def type(self):
+ # same as Y
+ return self.args[1].type
+
+ @property
+ def properties(self):
+ # z=zXzY; o=zXoY or zYoX; n=nX or zXnY; u=uY
+ px, py = [arg.properties for arg in self.args]
+ props = ""
+ if "z" in px and "z" in py:
+ props += "z"
+ if ("z" in px and "o" in py) or ("z" in py and "o" in px):
+ props += "o"
+ if "n" in px or ("z" in px and "n" in py):
+ props += "n"
+ if "u" in py:
+ props += "u"
+ return props
+
+
+class AndB(Miniscript):
+ # [X] [Y] BOOLAND
+ NAME = "and_b"
+ NARGS = 2
+ ARGCLS = Miniscript
+ TYPE = "B"
+
+ def inner_compile(self):
+ return self.args[0].compile() + self.args[1].compile() + b"\x9a"
+
+ def __len__(self):
+ return self.len_args() + 1
+
+ def verify(self):
+ # X is B; Y is W
+ super().verify()
+ if self.args[0].type != "B":
+ raise MiniscriptError("and_b: X should be B")
+ if self.args[1].type != "W":
+ raise MiniscriptError("and_b: Y should be W")
+
+ @property
+ def properties(self):
+ # z=zXzY; o=zXoY or zYoX; n=nX or zXnY; d=dXdY; u
+ px, py = [arg.properties for arg in self.args]
+ props = ""
+ if "z" in px and "z" in py:
+ props += "z"
+ if ("z" in px and "o" in py) or ("z" in py and "o" in px):
+ props += "o"
+ if "n" in px or ("z" in px and "n" in py):
+ props += "n"
+ if "d" in px and "d" in py:
+ props += "d"
+ props += "u"
+ return props
+
+
+class AndN(Miniscript):
+ # [X] NOTIF 0 ELSE [Y] ENDIF
+ # andor(X,Y,0)
+ NAME = "and_n"
+ NARGS = 2
+ ARGCLS = Miniscript
+
+ def inner_compile(self):
+ return (
+ self.args[0].compile()
+ + b"\x64"
+ + Number(0).compile()
+ + b"\x67"
+ + self.args[1].compile()
+ + b"\x68"
+ )
+
+ def __len__(self):
+ return self.len_args() + 4
+
+ @property
+ def type(self):
+ # same as Y/Z
+ return self.args[1].type
+
+ def verify(self):
+ # requires: X is Bdu; Y and Z are both B, K, or V
+ super().verify()
+ if self.args[0].type != "B":
+ raise MiniscriptError("and_n: X should be 'B'")
+ px = self.args[0].properties
+ if "d" not in px and "u" not in px:
+ raise MiniscriptError("and_n: X should be 'du'")
+ if self.args[1].type != "B":
+ raise MiniscriptError("and_n: Y should be B")
+
+ @property
+ def properties(self):
+ # props: z=zXzYzZ; o=zXoYoZ or oXzYzZ; u=uYuZ; d=dZ
+ props = ""
+ px, py = [arg.properties for arg in self.args]
+ pz = "zud"
+ if "z" in px and "z" in py and "z" in pz:
+ props += "z"
+ if ("z" in px and "o" in py and "o" in pz) or (
+ "o" in px and "z" in py and "z" in pz
+ ):
+ props += "o"
+ if "u" in py and "u" in pz:
+ props += "u"
+ if "d" in pz:
+ props += "d"
+ return props
+
+
+class OrB(Miniscript):
+ # [X] [Z] BOOLOR
+ NAME = "or_b"
+ NARGS = 2
+ ARGCLS = Miniscript
+ TYPE = "B"
+
+ def inner_compile(self):
+ return self.args[0].compile() + self.args[1].compile() + b"\x9b"
+
+ def __len__(self):
+ return self.len_args() + 1
+
+ def verify(self):
+ # X is Bd; Z is Wd
+ super().verify()
+ if self.args[0].type != "B":
+ raise MiniscriptError("or_b: X should be B")
+ if "d" not in self.args[0].properties:
+ raise MiniscriptError("or_b: X should be d")
+ if self.args[1].type != "W":
+ raise MiniscriptError("or_b: Z should be W")
+ if "d" not in self.args[1].properties:
+ raise MiniscriptError("or_b: Z should be d")
+
+ @property
+ def properties(self):
+ # z=zXzZ; o=zXoZ or zZoX; d; u
+ props = ""
+ px, pz = [arg.properties for arg in self.args]
+ if "z" in px and "z" in pz:
+ props += "z"
+ if ("z" in px and "o" in pz) or ("z" in pz and "o" in px):
+ props += "o"
+ props += "du"
+ return props
+
+
+class OrC(Miniscript):
+ # [X] NOTIF [Z] ENDIF
+ NAME = "or_c"
+ NARGS = 2
+ ARGCLS = Miniscript
+ TYPE = "V"
+
+ def inner_compile(self):
+ return self.args[0].compile() + b"\x64" + self.args[1].compile() + b"\x68"
+
+ def __len__(self):
+ return self.len_args() + 2
+
+ def verify(self):
+ # X is Bdu; Z is V
+ super().verify()
+ if self.args[0].type != "B":
+ raise MiniscriptError("or_c: X should be B")
+ if self.args[1].type != "V":
+ raise MiniscriptError("or_c: Z should be V")
+ px = self.args[0].properties
+ if "d" not in px or "u" not in px:
+ raise MiniscriptError("or_c: X should be du")
+
+ @property
+ def properties(self):
+ # z=zXzZ; o=oXzZ
+ props = ""
+ px, pz = [arg.properties for arg in self.args]
+ if "z" in px and "z" in pz:
+ props += "z"
+ if "o" in px and "z" in pz:
+ props += "o"
+ return props
+
+
+class OrD(Miniscript):
+ # [X] IFDUP NOTIF [Z] ENDIF
+ NAME = "or_d"
+ NARGS = 2
+ ARGCLS = Miniscript
+ TYPE = "B"
+
+ def inner_compile(self):
+ return self.args[0].compile() + b"\x73\x64" + self.args[1].compile() + b"\x68"
+
+ def __len__(self):
+ return self.len_args() + 3
+
+ def verify(self):
+ # X is Bdu; Z is B
+ super().verify()
+ if self.args[0].type != "B":
+ raise MiniscriptError("or_d: X should be B")
+ if self.args[1].type != "B":
+ raise MiniscriptError("or_d: Z should be B")
+ px = self.args[0].properties
+ if "d" not in px or "u" not in px:
+ raise MiniscriptError("or_d: X should be du")
+
+ @property
+ def properties(self):
+ # z=zXzZ; o=oXzZ; d=dZ; u=uZ
+ props = ""
+ px, pz = [arg.properties for arg in self.args]
+ if "z" in px and "z" in pz:
+ props += "z"
+ if "o" in px and "z" in pz:
+ props += "o"
+ if "d" in pz:
+ props += "d"
+ if "u" in pz:
+ props += "u"
+ return props
+
+
+class OrI(Miniscript):
+ # IF [X] ELSE [Z] ENDIF
+ NAME = "or_i"
+ NARGS = 2
+ ARGCLS = Miniscript
+
+ def inner_compile(self):
+ return (
+ b"\x63"
+ + self.args[0].compile()
+ + b"\x67"
+ + self.args[1].compile()
+ + b"\x68"
+ )
+
+ def __len__(self):
+ return self.len_args() + 3
+
+ def verify(self):
+ # both are B, K, or V
+ super().verify()
+ if self.args[0].type != self.args[1].type:
+ raise MiniscriptError("or_i: X and Z should be the same type")
+ if self.args[0].type not in "BKV":
+ raise MiniscriptError("or_i: X and Z should be B K or V")
+
+ @property
+ def type(self):
+ return self.args[0].type
+
+ @property
+ def properties(self):
+ # o=zXzZ; u=uXuZ; d=dX or dZ
+ props = ""
+ px, pz = [arg.properties for arg in self.args]
+ if "z" in px and "z" in pz:
+ props += "o"
+ if "u" in px and "u" in pz:
+ props += "u"
+ if "d" in px or "d" in pz:
+ props += "d"
+ return props
+
+
+class Thresh(Miniscript):
+ # [X1] [X2] ADD ... [Xn] ADD ... EQUAL
+ NAME = "thresh"
+ NARGS = None
+ ARGCLS = (Number, Miniscript)
+ TYPE = "B"
+
+ def inner_compile(self):
+ return (
+ self.args[1].compile()
+ + b"".join([arg.compile() + b"\x93" for arg in self.args[2:]])
+ + self.args[0].compile()
+ + b"\x87"
+ )
+
+ def __len__(self):
+ return self.len_args() + len(self.args) - 1
+
+ def verify(self):
+ # 1 <= k <= n; X1 is Bdu; others are Wdu
+ super().verify()
+ if self.args[0].num < 1 or self.args[0].num >= len(self.args):
+ raise MiniscriptError(
+ "thresh: Invalid k! Should be 1 <= k <= %d, got %d"
+ % (len(self.args) - 1, self.args[0].num)
+ )
+ if self.args[1].type != "B":
+ raise MiniscriptError("thresh: X1 should be B")
+ px = self.args[1].properties
+ if "d" not in px or "u" not in px:
+ raise MiniscriptError("thresh: X1 should be du")
+ for i, arg in enumerate(self.args[2:]):
+ if arg.type != "W":
+ raise MiniscriptError("thresh: X%d should be W" % (i + 1))
+ p = arg.properties
+ if "d" not in p or "u" not in p:
+ raise MiniscriptError("thresh: X%d should be du" % (i + 1))
+
+ @property
+ def properties(self):
+ # z=all are z; o=all are z except one is o; d; u
+ props = ""
+ parr = [arg.properties for arg in self.args[1:]]
+ zarr = ["z" for p in parr if "z" in p]
+ if len(zarr) == len(parr):
+ props += "z"
+ noz = [p for p in parr if "z" not in p]
+ if len(noz) == 1 and "o" in noz[0]:
+ props += "o"
+ props += "du"
+ return props
+
+
+class Multi(Miniscript):
+ # ... CHECKMULTISIG
+ NAME = "multi"
+ NARGS = None
+ ARGCLS = (Number, Key)
+ TYPE = "B"
+ PROPS = "ndu"
+ _expected_taproot = False
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ if self.taproot is not self._expected_taproot:
+ raise MiniscriptError(
+ "%s can't be used if taproot is %s" % (self.NAME, self.taproot)
+ )
+
+ def inner_compile(self):
+ return (
+ b"".join([arg.compile() for arg in self.args])
+ + Number(len(self.args) - 1).compile()
+ + b"\xae"
+ )
+
+ def __len__(self):
+ return self.len_args() + 2
+
+ def verify(self):
+ super().verify()
+ if self.args[0].num < 1 or self.args[0].num > (len(self.args) - 1):
+ raise MiniscriptError(
+ "multi: 1 <= k <= %d, got %d" % ((len(self.args) - 1), self.args[0].num)
+ )
+
+
+class Sortedmulti(Multi):
+ # ... CHECKMULTISIG
+ NAME = "sortedmulti"
+
+ def inner_compile(self):
+ return (
+ self.args[0].compile()
+ + b"".join(sorted([arg.compile() for arg in self.args[1:]]))
+ + Number(len(self.args) - 1).compile()
+ + b"\xae"
+ )
+
+
+class MultiA(Multi):
+ # CHECKSIG CHECKSIGADD ... CHECKSIGNADD NUMEQUAL
+ NAME = "multi_a"
+ _expected_taproot = True
+
+ def inner_compile(self):
+ return (
+ self.args[1].compile()
+ + b"\xac"
+ + b"".join([arg.compile() + b"\xba" for arg in self.args[2:]])
+ + self.args[0].compile()
+ + b"\x9c"
+ )
+
+ def __len__(self):
+ return self.len_args() + len(self.args)
+
+
+class SortedmultiA(MultiA):
+ # CHECKSIG CHECKSIGADD ... CHECKSIGNADD NUMEQUAL
+ NAME = "sortedmulti_a"
+
+ def inner_compile(self):
+ keys = list(sorted([k.compile() for k in self.args[1:]]))
+ return (
+ keys[0]
+ + b"\xac"
+ + b"".join([k + b"\xba" for k in keys[1:]])
+ + self.args[0].compile()
+ + b"\x9c"
+ )
+
+
+class Pk(OneArg):
+ # CHECKSIG
+ NAME = "pk"
+ ARGCLS = Key
+ TYPE = "B"
+ PROPS = "ondu"
+
+ def inner_compile(self):
+ return self.carg + b"\xac"
+
+ def __len__(self):
+ return self.len_args() + 1
+
+
+class Pkh(OneArg):
+ # DUP HASH160 EQUALVERIFY CHECKSIG
+ NAME = "pkh"
+ ARGCLS = KeyHash
+ TYPE = "B"
+ PROPS = "ndu"
+
+ def inner_compile(self):
+ return b"\x76\xa9" + self.carg + b"\x88\xac"
+
+ def __len__(self):
+ return self.len_args() + 4
+
+ # TODO: 0, 1 - they are without brackets, so it should be different...
+
+
+OPERATORS = [
+ NumberZero,
+ NumberOne,
+ PkK,
+ PkH,
+ Older,
+ After,
+ Sha256,
+ Hash256,
+ Ripemd160,
+ Hash160,
+ AndOr,
+ AndV,
+ AndB,
+ AndN,
+ OrB,
+ OrC,
+ OrD,
+ OrI,
+ Thresh,
+ Multi,
+ Sortedmulti,
+ MultiA,
+ SortedmultiA,
+ Pk,
+ Pkh,
+]
+OPERATOR_NAMES = [cls.NAME for cls in OPERATORS]
+
+
+class Wrapper(OneArg):
+ ARGCLS = Miniscript
+
+ @property
+ def op(self):
+ return type(self).__name__.lower()
+
+ def __str__(self):
+ # more wrappers follow
+ if isinstance(self.arg, Wrapper):
+ return self.op + str(self.arg)
+ # we are the last wrapper
+ return self.op + ":" + str(self.arg)
+
+
+class A(Wrapper):
+ # TOALTSTACK [X] FROMALTSTACK
+ TYPE = "W"
+
+ def inner_compile(self):
+ return b"\x6b" + self.carg + b"\x6c"
+
+ def __len__(self):
+ return len(self.arg) + 2
+
+ def verify(self):
+ super().verify()
+ if self.arg.type != "B":
+ raise MiniscriptError("a: X should be B")
+
+ @property
+ def properties(self):
+ props = ""
+ px = self.arg.properties
+ if "d" in px:
+ props += "d"
+ if "u" in px:
+ props += "u"
+ return props
+
+
+class S(Wrapper):
+ # SWAP [X]
+ TYPE = "W"
+
+ def inner_compile(self):
+ return b"\x7c" + self.carg
+
+ def __len__(self):
+ return len(self.arg) + 1
+
+ def verify(self):
+ super().verify()
+ if self.arg.type != "B":
+ raise MiniscriptError("s: X should be B")
+ if "o" not in self.arg.properties:
+ raise MiniscriptError("s: X should be o")
+
+ @property
+ def properties(self):
+ props = ""
+ px = self.arg.properties
+ if "d" in px:
+ props += "d"
+ if "u" in px:
+ props += "u"
+ return props
+
+
+class C(Wrapper):
+ # [X] CHECKSIG
+ TYPE = "B"
+
+ def inner_compile(self):
+ return self.carg + b"\xac"
+
+ def __len__(self):
+ return len(self.arg) + 1
+
+ def verify(self):
+ super().verify()
+ if self.arg.type != "K":
+ raise MiniscriptError("c: X should be K")
+
+ @property
+ def properties(self):
+ props = ""
+ px = self.arg.properties
+ for p in ["o", "n", "d"]:
+ if p in px:
+ props += p
+ props += "u"
+ return props
+
+
+class T(Wrapper):
+ # [X] 1
+ TYPE = "B"
+
+ def inner_compile(self):
+ return self.carg + Number(1).compile()
+
+ def __len__(self):
+ return len(self.arg) + 1
+
+ @property
+ def properties(self):
+ # z=zXzY; o=zXoY or zYoX; n=nX or zXnY; u=uY
+ px = self.arg.properties
+ py = "zu"
+ props = ""
+ if "z" in px and "z" in py:
+ props += "z"
+ if ("z" in px and "o" in py) or ("z" in py and "o" in px):
+ props += "o"
+ if "n" in px or ("z" in px and "n" in py):
+ props += "n"
+ if "u" in py:
+ props += "u"
+ return props
+
+
+class D(Wrapper):
+ # DUP IF [X] ENDIF
+ TYPE = "B"
+
+ def inner_compile(self):
+ return b"\x76\x63" + self.carg + b"\x68"
+
+ def __len__(self):
+ return len(self.arg) + 3
+
+ def verify(self):
+ super().verify()
+ if self.arg.type != "V":
+ raise MiniscriptError("d: X should be V")
+ if "z" not in self.arg.properties:
+ raise MiniscriptError("d: X should be z")
+
+ @property
+ def properties(self):
+ # https://github.com/bitcoin/bitcoin/pull/24906
+ if self.taproot:
+ props = "ndu"
+ else:
+ props = "nd"
+ px = self.arg.properties
+ if "z" in px:
+ props += "o"
+ return props
+
+
+class V(Wrapper):
+ # [X] VERIFY (or VERIFY version of last opcode in [X])
+ TYPE = "V"
+
+ def inner_compile(self):
+ """Checks last check code and makes it verify"""
+ if self.carg[-1] in [0xAC, 0xAE, 0x9C, 0x87]:
+ return self.carg[:-1] + bytes([self.carg[-1] + 1])
+ return self.carg + b"\x69"
+
+ def verify(self):
+ super().verify()
+ if self.arg.type != "B":
+ raise MiniscriptError("v: X should be B")
+
+ @property
+ def properties(self):
+ props = ""
+ px = self.arg.properties
+ for p in ["z", "o", "n"]:
+ if p in px:
+ props += p
+ return props
+
+
+class J(Wrapper):
+ # SIZE 0NOTEQUAL IF [X] ENDIF
+ TYPE = "B"
+
+ def inner_compile(self):
+ return b"\x82\x92\x63" + self.carg + b"\x68"
+
+ def verify(self):
+ super().verify()
+ if self.arg.type != "B":
+ raise MiniscriptError("j: X should be B")
+ if "n" not in self.arg.properties:
+ raise MiniscriptError("j: X should be n")
+
+ @property
+ def properties(self):
+ props = "nd"
+ px = self.arg.properties
+ for p in ["o", "u"]:
+ if p in px:
+ props += p
+ return props
+
+
+class N(Wrapper):
+ # [X] 0NOTEQUAL
+ TYPE = "B"
+
+ def inner_compile(self):
+ return self.carg + b"\x92"
+
+ def __len__(self):
+ return len(self.arg) + 1
+
+ def verify(self):
+ super().verify()
+ if self.arg.type != "B":
+ raise MiniscriptError("n: X should be B")
+
+ @property
+ def properties(self):
+ props = "u"
+ px = self.arg.properties
+ for p in ["z", "o", "n", "d"]:
+ if p in px:
+ props += p
+ return props
+
+
+class L(Wrapper):
+ # IF 0 ELSE [X] ENDIF
+ TYPE = "B"
+
+ def inner_compile(self):
+ return b"\x63" + Number(0).compile() + b"\x67" + self.carg + b"\x68"
+
+ def __len__(self):
+ return len(self.arg) + 4
+
+ def verify(self):
+ # both are B, K, or V
+ super().verify()
+ if self.arg.type != "B":
+ raise MiniscriptError("or_i: X and Z should be the same type")
+
+ @property
+ def properties(self):
+ # o=zXzZ; u=uXuZ; d=dX or dZ
+ props = "d"
+ pz = self.arg.properties
+ if "z" in pz:
+ props += "o"
+ if "u" in pz:
+ props += "u"
+ return props
+
+
+class U(L):
+ # IF [X] ELSE 0 ENDIF
+ def inner_compile(self):
+ return b"\x63" + self.carg + b"\x67" + Number(0).compile() + b"\x68"
+
+ def __len__(self):
+ return len(self.arg) + 4
+
+
+WRAPPERS = [A, S, C, T, D, V, J, N, L, U]
+WRAPPER_NAMES = [w.__name__.lower() for w in WRAPPERS]
diff --git a/bitcoin_client/ledger_bitcoin/embit/descriptor/taptree.py b/bitcoin_client/ledger_bitcoin/embit/descriptor/taptree.py
new file mode 100644
index 000000000..7f611e5ec
--- /dev/null
+++ b/bitcoin_client/ledger_bitcoin/embit/descriptor/taptree.py
@@ -0,0 +1,151 @@
+from .errors import MiniscriptError
+from .base import DescriptorBase
+from .miniscript import Miniscript
+from ..hashes import tagged_hash
+from ..script import Script
+
+
+class TapLeaf(DescriptorBase):
+ def __init__(self, miniscript=None, version=0xC0):
+ self.miniscript = miniscript
+ self.version = version
+
+ def __str__(self):
+ return str(self.miniscript)
+
+ @classmethod
+ def read_from(cls, s):
+ ms = Miniscript.read_from(s, taproot=True)
+ return cls(ms)
+
+ def serialize(self):
+ if self.miniscript is None:
+ return b""
+ return bytes([self.version]) + Script(self.miniscript.compile()).serialize()
+
+ @property
+ def keys(self):
+ return self.miniscript.keys
+
+ def derive(self, *args, **kwargs):
+ if self.miniscript is None:
+ return type(self)(None, version=self.version)
+ return type(self)(
+ self.miniscript.derive(*args, **kwargs),
+ self.version,
+ )
+
+ def branch(self, *args, **kwargs):
+ if self.miniscript is None:
+ return type(self)(None, version=self.version)
+ return type(self)(
+ self.miniscript.branch(*args, **kwargs),
+ self.version,
+ )
+
+ def to_public(self, *args, **kwargs):
+ if self.miniscript is None:
+ return type(self)(None, version=self.version)
+ return type(self)(
+ self.miniscript.to_public(*args, **kwargs),
+ self.version,
+ )
+
+
+def _tweak_helper(tree):
+ # https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#constructing-and-spending-taproot-outputs
+ if isinstance(tree, TapTree):
+ tree = tree.tree
+ if isinstance(tree, TapLeaf):
+ # one leaf on this branch
+ h = tagged_hash("TapLeaf", tree.serialize())
+ return ([(tree, b"")], h)
+ left, left_h = _tweak_helper(tree[0])
+ right, right_h = _tweak_helper(tree[1])
+ ret = [(leaf, c + right_h) for leaf, c in left] + [
+ (leaf, c + left_h) for leaf, c in right
+ ]
+ if right_h < left_h:
+ left_h, right_h = right_h, left_h
+ return (ret, tagged_hash("TapBranch", left_h + right_h))
+
+
+class TapTree(DescriptorBase):
+ def __init__(self, tree=None):
+ """tree can be None, TapLeaf or a tuple (taptree, taptree)"""
+ self.tree = tree
+ # make sure all keys are taproot
+ for k in self.keys:
+ k.taproot = True
+
+ def __bool__(self):
+ return bool(self.tree)
+
+ def tweak(self):
+ if self.tree is None:
+ return b""
+ _, h = _tweak_helper(self.tree)
+ return h
+
+ @property
+ def keys(self):
+ if self.tree is None:
+ return []
+ if isinstance(self.tree, TapLeaf):
+ return self.tree.keys
+ left, right = self.tree
+ return left.keys + right.keys
+
+ @classmethod
+ def read_from(cls, s):
+ c = s.read(1)
+ if len(c) == 0:
+ return cls()
+ if c == b"{": # more than one miniscript
+ left = cls.read_from(s)
+ c = s.read(1)
+ if c == b"}":
+ return left
+ if c != b",":
+ raise MiniscriptError("Invalid taptree syntax: expected ','")
+ right = cls.read_from(s)
+ if s.read(1) != b"}":
+ raise MiniscriptError("Invalid taptree syntax: expected '}'")
+ return cls((left, right))
+ s.seek(-1, 1)
+ ms = TapLeaf.read_from(s)
+ return cls(ms)
+
+ def derive(self, *args, **kwargs):
+ if self.tree is None:
+ return type(self)(None)
+ if isinstance(self.tree, TapLeaf):
+ return type(self)(self.tree.derive(*args, **kwargs))
+ left, right = self.tree
+ return type(self)((left.derive(*args, **kwargs), right.derive(*args, **kwargs)))
+
+ def branch(self, *args, **kwargs):
+ if self.tree is None:
+ return type(self)(None)
+ if isinstance(self.tree, TapLeaf):
+ return type(self)(self.tree.branch(*args, **kwargs))
+ left, right = self.tree
+ return type(self)((left.branch(*args, **kwargs), right.branch(*args, **kwargs)))
+
+ def to_public(self, *args, **kwargs):
+ if self.tree is None:
+ return type(self)(None)
+ if isinstance(self.tree, TapLeaf):
+ return type(self)(self.tree.to_public(*args, **kwargs))
+ left, right = self.tree
+ return type(self)(
+ (left.to_public(*args, **kwargs), right.to_public(*args, **kwargs))
+ )
+
+ def __str__(self):
+ if self.tree is None:
+ return ""
+ if isinstance(self.tree, TapLeaf):
+ return str(self.tree)
+ (left, right) = self.tree
+ return "{%s,%s}" % (left, right)
diff --git a/bitcoin_client/ledger_bitcoin/embit/ec.py b/bitcoin_client/ledger_bitcoin/embit/ec.py
new file mode 100644
index 000000000..a93fc7143
--- /dev/null
+++ b/bitcoin_client/ledger_bitcoin/embit/ec.py
@@ -0,0 +1,263 @@
+from . import base58
+from . import hashes
+from .misc import secp256k1
+from .networks import NETWORKS
+from .base import EmbitBase, EmbitError, EmbitKey
+from binascii import hexlify, unhexlify
+
+
+class ECError(EmbitError):
+ pass
+
+
+class Signature(EmbitBase):
+ def __init__(self, sig):
+ self._sig = sig
+
+ def write_to(self, stream) -> int:
+ return stream.write(secp256k1.ecdsa_signature_serialize_der(self._sig))
+
+ @classmethod
+ def read_from(cls, stream):
+ der = stream.read(2)
+ der += stream.read(der[1])
+ return cls(secp256k1.ecdsa_signature_parse_der(der))
+
+
+class SchnorrSig(EmbitBase):
+ def __init__(self, sig):
+ assert len(sig) == 64
+ self._sig = sig
+
+ def write_to(self, stream) -> int:
+ return stream.write(self._sig)
+
+ @classmethod
+ def read_from(cls, stream):
+ return cls(stream.read(64))
+
+
+class PublicKey(EmbitKey):
+ def __init__(self, point: bytes, compressed: bool = True):
+ self._point = point
+ self.compressed = compressed
+
+ @classmethod
+ def read_from(cls, stream):
+ b = stream.read(1)
+ if b not in [b"\x02", b"\x03", b"\x04"]:
+ raise ECError("Invalid public key")
+ if b == b"\x04":
+ b += stream.read(64)
+ else:
+ b += stream.read(32)
+ try:
+ point = secp256k1.ec_pubkey_parse(b)
+ except Exception as e:
+ raise ECError(str(e))
+ compressed = b[0] != 0x04
+ return cls(point, compressed)
+
+ def sec(self) -> bytes:
+ """Sec representation of the key"""
+ flag = secp256k1.EC_COMPRESSED if self.compressed else secp256k1.EC_UNCOMPRESSED
+ return secp256k1.ec_pubkey_serialize(self._point, flag)
+
+ def xonly(self) -> bytes:
+ return self.sec()[1:33]
+
+ def taproot_tweak(self, h=b""):
+ """Returns a tweaked public key"""
+ x = self.xonly()
+ tweak = hashes.tagged_hash("TapTweak", x + h)
+ if not secp256k1.ec_seckey_verify(tweak):
+ raise EmbitError("Tweak is too large")
+ point = secp256k1.ec_pubkey_parse(b"\x02" + x)
+ pub = secp256k1.ec_pubkey_add(point, tweak)
+ sec = secp256k1.ec_pubkey_serialize(pub)
+ return PublicKey.from_xonly(sec[1:33])
+
+ def write_to(self, stream) -> int:
+ return stream.write(self.sec())
+
+ def serialize(self) -> bytes:
+ return self.sec()
+
+ def verify(self, sig, msg_hash) -> bool:
+ return bool(secp256k1.ecdsa_verify(sig._sig, msg_hash, self._point))
+
+ def _xonly(self):
+ """Returns internal representation of the xonly-pubkey (64 bytes)"""
+ pub, _ = secp256k1.xonly_pubkey_from_pubkey(self._point)
+ return pub
+
+ @classmethod
+ def from_xonly(cls, data: bytes):
+ assert len(data) == 32
+ return cls.parse(b"\x02" + data)
+
+ def schnorr_verify(self, sig, msg_hash) -> bool:
+ return bool(secp256k1.schnorrsig_verify(sig._sig, msg_hash, self._xonly()))
+
+ @classmethod
+ def from_string(cls, s):
+ return cls.parse(unhexlify(s))
+
+ @property
+ def is_private(self) -> bool:
+ return False
+
+ def to_string(self):
+ return hexlify(self.sec()).decode()
+
+ def __lt__(self, other):
+ # for lexagraphic ordering
+ return self.sec() < other.sec()
+
+ def __gt__(self, other):
+ # for lexagraphic ordering
+ return self.sec() > other.sec()
+
+ def __eq__(self, other):
+ return self.sec() == other.sec()
+
+ def __hash__(self):
+ return hash(self._point)
+
+
+class PrivateKey(EmbitKey):
+ def __init__(self, secret, compressed: bool = True, network=NETWORKS["main"]):
+ """Creates a private key from 32-byte array"""
+ if len(secret) != 32:
+ raise ECError("Secret should be 32-byte array")
+ if not secp256k1.ec_seckey_verify(secret):
+ raise ECError("Secret is not valid (larger then N?)")
+ self.compressed = compressed
+ self._secret = secret
+ self.network = network
+
+ def wif(self, network=None) -> str:
+ """Export private key as Wallet Import Format string.
+ Prefix 0x80 is used for mainnet, 0xEF for testnet.
+ This class doesn't store this information though.
+ """
+ if network is None:
+ network = self.network
+ prefix = network["wif"]
+ b = prefix + self._secret
+ if self.compressed:
+ b += bytes([0x01])
+ return base58.encode_check(b)
+
+ @property
+ def secret(self):
+ return self._secret
+
+ def sec(self) -> bytes:
+ """Sec representation of the corresponding public key"""
+ return self.get_public_key().sec()
+
+ def xonly(self) -> bytes:
+ return self.sec()[1:]
+
+ def taproot_tweak(self, h=b""):
+ """Returns a tweaked private key"""
+ sec = self.sec()
+ negate = sec[0] != 0x02
+ x = sec[1:33]
+ tweak = hashes.tagged_hash("TapTweak", x + h)
+ if not secp256k1.ec_seckey_verify(tweak):
+ raise EmbitError("Tweak is too large")
+ if negate:
+ secret = secp256k1.ec_privkey_negate(self._secret)
+ else:
+ secret = self._secret
+ res = secp256k1.ec_privkey_add(secret, tweak)
+ pk = PrivateKey(res)
+ if pk.sec()[0] == 0x03:
+ pk = PrivateKey(secp256k1.ec_privkey_negate(res))
+ return pk
+
+ @classmethod
+ def from_wif(cls, s):
+ """Import private key from Wallet Import Format string."""
+ b = base58.decode_check(s)
+ prefix = b[:1]
+ network = None
+ for net in NETWORKS:
+ if NETWORKS[net]["wif"] == prefix:
+ network = NETWORKS[net]
+ secret = b[1:33]
+ compressed = False
+ if len(b) not in [33, 34]:
+ raise ECError("Wrong WIF length")
+ if len(b) == 34:
+ if b[-1] == 0x01:
+ compressed = True
+ else:
+ raise ECError("Wrong WIF compressed flag")
+ return cls(secret, compressed, network)
+
+ # to unify API
+ def to_base58(self, network=None) -> str:
+ return self.wif(network)
+
+ @classmethod
+ def from_base58(cls, s):
+ return cls.from_wif(s)
+
+ def get_public_key(self) -> PublicKey:
+ return PublicKey(secp256k1.ec_pubkey_create(self._secret), self.compressed)
+
+ def to_public(self) -> PublicKey:
+ """Alias to get_public_key for API consistency"""
+ return self.get_public_key()
+
+ def sign(self, msg_hash, grind=True) -> Signature:
+ sig = Signature(secp256k1.ecdsa_sign(msg_hash, self._secret))
+ if grind:
+ counter = 1
+ while len(sig.serialize()) > 70:
+ sig = Signature(
+ secp256k1.ecdsa_sign(
+ msg_hash, self._secret, None, counter.to_bytes(32, "little")
+ )
+ )
+ counter += 1
+ # just in case we get in infinite loop for some reason
+ if counter > 200:
+ break
+ return sig
+
+ def schnorr_sign(self, msg_hash) -> SchnorrSig:
+ return SchnorrSig(secp256k1.schnorrsig_sign(msg_hash, self._secret))
+
+ def verify(self, sig, msg_hash) -> bool:
+ return self.get_public_key().verify(sig, msg_hash)
+
+ def schnorr_verify(self, sig, msg_hash) -> bool:
+ return self.get_public_key().schnorr_verify(sig, msg_hash)
+
+ def write_to(self, stream) -> int:
+ # return a copy of the secret
+ return stream.write(self._secret)
+
+ def ecdh(self, public_key: PublicKey, hashfn=None, data=None) -> bytes:
+ pubkey_point = secp256k1.ec_pubkey_parse(public_key.sec())
+ return secp256k1.ecdh(pubkey_point, self._secret, hashfn, data)
+
+ @classmethod
+ def read_from(cls, stream):
+ # just to unify the API
+ return cls(stream.read(32))
+
+ @property
+ def is_private(self) -> bool:
+ return True
+
+
+# Nothing up my sleeve point for no-internal-key taproot
+# see https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#constructing-and-spending-taproot-outputs
+NUMS_PUBKEY = PublicKey.from_string(
+ "0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0"
+)
diff --git a/bitcoin_client/ledger_bitcoin/embit/hashes.py b/bitcoin_client/ledger_bitcoin/embit/hashes.py
new file mode 100644
index 000000000..c5edd081f
--- /dev/null
+++ b/bitcoin_client/ledger_bitcoin/embit/hashes.py
@@ -0,0 +1,41 @@
+import hashlib
+
+try:
+ # this will work with micropython and python < 3.10
+ # but will raise and exception if ripemd is not supported (python3.10, openssl 3)
+ hashlib.new("ripemd160")
+
+ def ripemd160(msg: bytes) -> bytes:
+ return hashlib.new("ripemd160", msg).digest()
+
+except:
+ # otherwise use pure python implementation
+ from .util.py_ripemd160 import ripemd160
+
+
+def double_sha256(msg: bytes) -> bytes:
+ """sha256(sha256(msg)) -> bytes"""
+ return hashlib.sha256(hashlib.sha256(msg).digest()).digest()
+
+
+def hash160(msg: bytes) -> bytes:
+ """ripemd160(sha256(msg)) -> bytes"""
+ return ripemd160(hashlib.sha256(msg).digest())
+
+
+def sha256(msg: bytes) -> bytes:
+ """one-line sha256(msg) -> bytes"""
+ return hashlib.sha256(msg).digest()
+
+
+def tagged_hash(tag: str, data: bytes) -> bytes:
+ """BIP-Schnorr tag-specific key derivation"""
+ hashtag = hashlib.sha256(tag.encode()).digest()
+ return hashlib.sha256(hashtag + hashtag + data).digest()
+
+
+def tagged_hash_init(tag: str, data: bytes = b""):
+ """Prepares a tagged hash function to digest extra data"""
+ hashtag = hashlib.sha256(tag.encode()).digest()
+ h = hashlib.sha256(hashtag + hashtag + data)
+ return h
diff --git a/bitcoin_client/ledger_bitcoin/embit/misc.py b/bitcoin_client/ledger_bitcoin/embit/misc.py
new file mode 100644
index 000000000..fc2c8046d
--- /dev/null
+++ b/bitcoin_client/ledger_bitcoin/embit/misc.py
@@ -0,0 +1,70 @@
+"""Misc utility functions used across embit"""
+import sys
+
+# implementation-specific functions and libraries:
+if sys.implementation.name == "micropython":
+ from micropython import const
+ import secp256k1
+else:
+ from .util import secp256k1
+
+ def const(x):
+ return x
+
+
+try:
+ # if urandom is available from os module:
+ from os import urandom as urandom
+except ImportError:
+ # otherwise - try reading from /dev/urandom
+ def urandom(n: int) -> bytes:
+ with open("/dev/urandom", "rb") as f:
+ return f.read(n)
+
+
+def getrandbits(k: int) -> int:
+ b = urandom(k // 8 + 1)
+ return int.from_bytes(b, "big") % (2**k)
+
+
+def secure_randint(vmin: int, vmax: int) -> int:
+ """
+ Normal random.randint uses PRNG that is not suitable
+ for cryptographic applications.
+ This one uses os.urandom for randomness.
+ """
+ import math
+
+ assert vmax > vmin
+ delta = vmax - vmin
+ nbits = math.ceil(math.log2(delta + 1))
+ randn = getrandbits(nbits)
+ while randn > delta:
+ randn = getrandbits(nbits)
+ return vmin + randn
+
+
+def copy(a: bytes) -> bytes:
+ """Ugly copy that works everywhere incl micropython"""
+ if len(a) == 0:
+ return b""
+ return a[:1] + a[1:]
+
+
+def read_until(s, chars=b",)(#"):
+ """Read from stream until one of `char` characters.
+ By default `chars=,)(#`.
+
+ Return a tuple (result: bytes, char: bytes | None)
+ where result is bytes read from the stream until char,
+ char contains this character or None if the end of stream reached.
+ """
+ res = b""
+ chunk = b""
+ while True:
+ chunk = s.read(1)
+ if len(chunk) == 0:
+ return res, None
+ if chunk in chars:
+ return res, chunk
+ res += chunk
diff --git a/bitcoin_client/ledger_bitcoin/embit/networks.py b/bitcoin_client/ledger_bitcoin/embit/networks.py
new file mode 100644
index 000000000..6f1a54180
--- /dev/null
+++ b/bitcoin_client/ledger_bitcoin/embit/networks.py
@@ -0,0 +1,76 @@
+from .misc import const
+
+NETWORKS = {
+ "main": {
+ "name": "Mainnet",
+ "wif": b"\x80",
+ "p2pkh": b"\x00",
+ "p2sh": b"\x05",
+ "bech32": "bc",
+ "xprv": b"\x04\x88\xad\xe4",
+ "xpub": b"\x04\x88\xb2\x1e",
+ "yprv": b"\x04\x9d\x78\x78",
+ "zprv": b"\x04\xb2\x43\x0c",
+ "Yprv": b"\x02\x95\xb0\x05",
+ "Zprv": b"\x02\xaa\x7a\x99",
+ "ypub": b"\x04\x9d\x7c\xb2",
+ "zpub": b"\x04\xb2\x47\x46",
+ "Ypub": b"\x02\x95\xb4\x3f",
+ "Zpub": b"\x02\xaa\x7e\xd3",
+ "bip32": const(0), # coin type for bip32 derivation
+ },
+ "test": {
+ "name": "Testnet",
+ "wif": b"\xEF",
+ "p2pkh": b"\x6F",
+ "p2sh": b"\xC4",
+ "bech32": "tb",
+ "xprv": b"\x04\x35\x83\x94",
+ "xpub": b"\x04\x35\x87\xcf",
+ "yprv": b"\x04\x4a\x4e\x28",
+ "zprv": b"\x04\x5f\x18\xbc",
+ "Yprv": b"\x02\x42\x85\xb5",
+ "Zprv": b"\x02\x57\x50\x48",
+ "ypub": b"\x04\x4a\x52\x62",
+ "zpub": b"\x04\x5f\x1c\xf6",
+ "Ypub": b"\x02\x42\x89\xef",
+ "Zpub": b"\x02\x57\x54\x83",
+ "bip32": const(1),
+ },
+ "regtest": {
+ "name": "Regtest",
+ "wif": b"\xEF",
+ "p2pkh": b"\x6F",
+ "p2sh": b"\xC4",
+ "bech32": "bcrt",
+ "xprv": b"\x04\x35\x83\x94",
+ "xpub": b"\x04\x35\x87\xcf",
+ "yprv": b"\x04\x4a\x4e\x28",
+ "zprv": b"\x04\x5f\x18\xbc",
+ "Yprv": b"\x02\x42\x85\xb5",
+ "Zprv": b"\x02\x57\x50\x48",
+ "ypub": b"\x04\x4a\x52\x62",
+ "zpub": b"\x04\x5f\x1c\xf6",
+ "Ypub": b"\x02\x42\x89\xef",
+ "Zpub": b"\x02\x57\x54\x83",
+ "bip32": const(1),
+ },
+ "signet": {
+ "name": "Signet",
+ "wif": b"\xEF",
+ "p2pkh": b"\x6F",
+ "p2sh": b"\xC4",
+ "bech32": "tb",
+ "xprv": b"\x04\x35\x83\x94",
+ "xpub": b"\x04\x35\x87\xcf",
+ "yprv": b"\x04\x4a\x4e\x28",
+ "zprv": b"\x04\x5f\x18\xbc",
+ "Yprv": b"\x02\x42\x85\xb5",
+ "Zprv": b"\x02\x57\x50\x48",
+ "ypub": b"\x04\x4a\x52\x62",
+ "zpub": b"\x04\x5f\x1c\xf6",
+ "Ypub": b"\x02\x42\x89\xef",
+ "Zpub": b"\x02\x57\x54\x83",
+ "bip32": const(1),
+ },
+}
diff --git a/bitcoin_client/ledger_bitcoin/embit/script.py b/bitcoin_client/ledger_bitcoin/embit/script.py
new file mode 100644
index 000000000..5cea7f98f
--- /dev/null
+++ b/bitcoin_client/ledger_bitcoin/embit/script.py
@@ -0,0 +1,212 @@
+from .networks import NETWORKS
+from . import base58
+from . import bech32
+from . import hashes
+from . import compact
+from .base import EmbitBase, EmbitError
+
+SIGHASH_ALL = 1
+
+
+class Script(EmbitBase):
+ def __init__(self, data=b""):
+ self.data = data
+
+ def address(self, network=NETWORKS["main"]):
+ script_type = self.script_type()
+ data = self.data
+
+ if script_type is None:
+ raise ValueError("This type of script doesn't have address representation")
+
+ if script_type == "p2pkh":
+ d = network["p2pkh"] + data[3:23]
+ return base58.encode_check(d)
+
+ if script_type == "p2sh":
+ d = network["p2sh"] + data[2:22]
+ return base58.encode_check(d)
+
+ if script_type in ["p2wpkh", "p2wsh", "p2tr"]:
+ ver = data[0]
+ # FIXME: should be one of OP_N
+ if ver > 0:
+ ver = ver % 0x50
+ return bech32.encode(network["bech32"], ver, data[2:])
+
+ # we should never get here
+ raise ValueError("Unsupported script type")
+
+ def push(self, data):
+ self.data += compact.to_bytes(len(data)) + data
+
+ def script_type(self):
+ data = self.data
+ # OP_DUP OP_HASH160 <20:hash160(pubkey)> OP_EQUALVERIFY OP_CHECKSIG
+ if len(data) == 25 and data[:3] == b"\x76\xa9\x14" and data[-2:] == b"\x88\xac":
+ return "p2pkh"
+ # OP_HASH160 <20:hash160(script)> OP_EQUAL
+ if len(data) == 23 and data[:2] == b"\xa9\x14" and data[-1] == 0x87:
+ return "p2sh"
+ # 0 <20:hash160(pubkey)>
+ if len(data) == 22 and data[:2] == b"\x00\x14":
+ return "p2wpkh"
+ # 0 <32:sha256(script)>
+ if len(data) == 34 and data[:2] == b"\x00\x20":
+ return "p2wsh"
+ # OP_1
+ if len(data) == 34 and data[:2] == b"\x51\x20":
+ return "p2tr"
+ # unknown type
+ return None
+
+ def write_to(self, stream):
+ res = stream.write(compact.to_bytes(len(self.data)))
+ res += stream.write(self.data)
+ return res
+
+ @classmethod
+ def read_from(cls, stream):
+ l = compact.read_from(stream)
+ data = stream.read(l)
+ if len(data) != l:
+ raise ValueError("Cant read %d bytes" % l)
+ return cls(data)
+
+ @classmethod
+ def from_address(cls, addr: str):
+ """
+ Decodes a bitcoin address and returns corresponding scriptpubkey.
+ """
+ return address_to_scriptpubkey(addr)
+
+ def __eq__(self, other):
+ return self.data == other.data
+
+ def __ne__(self, other):
+ return self.data != other.data
+
+ def __hash__(self):
+ return hash(self.data)
+
+ def __len__(self):
+ return len(self.data)
+
+
+class Witness(EmbitBase):
+ def __init__(self, items=[]):
+ self.items = items[:]
+
+ def write_to(self, stream):
+ res = stream.write(compact.to_bytes(len(self.items)))
+ for item in self.items:
+ res += stream.write(compact.to_bytes(len(item)))
+ res += stream.write(item)
+ return res
+
+ @classmethod
+ def read_from(cls, stream):
+ num = compact.read_from(stream)
+ items = []
+ for i in range(num):
+ l = compact.read_from(stream)
+ data = stream.read(l)
+ items.append(data)
+ return cls(items)
+
+ def __hash__(self):
+ return hash(self.items)
+
+ def __len__(self):
+ return len(self.items)
+
+
+def p2pkh(pubkey):
+ """Return Pay-To-Pubkey-Hash ScriptPubkey"""
+ return Script(b"\x76\xa9\x14" + hashes.hash160(pubkey.sec()) + b"\x88\xac")
+
+
+def p2sh(script):
+ """Return Pay-To-Script-Hash ScriptPubkey"""
+ return Script(b"\xa9\x14" + hashes.hash160(script.data) + b"\x87")
+
+
+def p2wpkh(pubkey):
+ """Return Pay-To-Witness-Pubkey-Hash ScriptPubkey"""
+ return Script(b"\x00\x14" + hashes.hash160(pubkey.sec()))
+
+
+def p2wsh(script):
+ """Return Pay-To-Witness-Pubkey-Hash ScriptPubkey"""
+ return Script(b"\x00\x20" + hashes.sha256(script.data))
+
+
+def p2tr(pubkey, script_tree=None):
+ """Return Pay-To-Taproot ScriptPubkey"""
+ if script_tree is None:
+ h = b""
+ else:
+ h = script_tree.tweak()
+ output_pubkey = pubkey.taproot_tweak(h)
+ return Script(b"\x51\x20" + output_pubkey.xonly())
+
+
+def p2pkh_from_p2wpkh(script):
+ """Convert p2wpkh to p2pkh script"""
+ return Script(b"\x76\xa9" + script.serialize()[2:] + b"\x88\xac")
+
+
+def multisig(m: int, pubkeys):
+ if m <= 0 or m > 16:
+ raise ValueError("m must be between 1 and 16")
+ n = len(pubkeys)
+ if n < m or n > 16:
+ raise ValueError("Number of pubkeys must be between %d and 16" % m)
+ data = bytes([80 + m])
+ for pubkey in pubkeys:
+ sec = pubkey.sec()
+ data += bytes([len(sec)]) + sec
+ # OP_m ... OP_n OP_CHECKMULTISIG
+ data += bytes([80 + n, 0xAE])
+ return Script(data)
+
+
+def address_to_scriptpubkey(addr):
+ # try with base58 address
+ try:
+ data = base58.decode_check(addr)
+ prefix = data[:1]
+ for net in NETWORKS.values():
+ if prefix == net["p2pkh"]:
+ return Script(b"\x76\xa9\x14" + data[1:] + b"\x88\xac")
+ elif prefix == net["p2sh"]:
+ return Script(b"\xa9\x14" + data[1:] + b"\x87")
+ except:
+ # fail - then it's bech32 address
+ hrp = addr.split("1")[0]
+ ver, data = bech32.decode(hrp, addr)
+ if ver not in [0, 1] or len(data) not in [20, 32]:
+ raise EmbitError("Invalid bech32 address")
+ if ver == 1 and len(data) != 32:
+ raise EmbitError("Invalid bech32 address")
+ # OP_1..OP_N
+ if ver > 0:
+ ver += 0x50
+ return Script(bytes([ver, len(data)] + data))
+
+
+def script_sig_p2pkh(signature, pubkey, sighash=SIGHASH_ALL):
+ sec = pubkey.sec()
+ der = signature.serialize() + bytes([sighash])
+ data = compact.to_bytes(len(der)) + der + compact.to_bytes(len(sec)) + sec
+ return Script(data)
+
+
+def script_sig_p2sh(redeem_script):
+ """Creates scriptsig for p2sh"""
+ # FIXME: implement for legacy p2sh as well
+ return Script(redeem_script.serialize())
+
+
+def witness_p2wpkh(signature, pubkey, sighash=SIGHASH_ALL):
+ return Witness([signature.serialize() + bytes([sighash]), pubkey.sec()])
diff --git a/bitcoin_client/ledger_bitcoin/embit/util/__init__.py b/bitcoin_client/ledger_bitcoin/embit/util/__init__.py
new file mode 100644
index 000000000..d2f2564a6
--- /dev/null
+++ b/bitcoin_client/ledger_bitcoin/embit/util/__init__.py
@@ -0,0 +1,6 @@
+from . import secp256k1
+
+try:
+ from micropython import const
+except:
+ const = lambda x: x
diff --git a/bitcoin_client/ledger_bitcoin/embit/util/ctypes_secp256k1.py b/bitcoin_client/ledger_bitcoin/embit/util/ctypes_secp256k1.py
new file mode 100644
index 000000000..232abda18
--- /dev/null
+++ b/bitcoin_client/ledger_bitcoin/embit/util/ctypes_secp256k1.py
@@ -0,0 +1,1202 @@
+import ctypes, os
+import ctypes.util
+import platform
+import threading
+
+from ctypes import (
+ cast,
+ byref,
+ c_char,
+ c_byte,
+ c_int,
+ c_uint,
+ c_char_p,
+ c_size_t,
+ c_void_p,
+ c_uint64,
+ create_string_buffer,
+ CFUNCTYPE,
+ POINTER,
+)
+
+_lock = threading.Lock()
+
+
+# @locked decorator
+def locked(func):
+ def wrapper(*args, **kwargs):
+ with _lock:
+ return func(*args, **kwargs)
+
+ return wrapper
+
+
+# Flags to pass to context_create.
+CONTEXT_VERIFY = 0b0100000001
+CONTEXT_SIGN = 0b1000000001
+CONTEXT_NONE = 0b0000000001
+
+# Flags to pass to ec_pubkey_serialize
+EC_COMPRESSED = 0b0100000010
+EC_UNCOMPRESSED = 0b0000000010
+
+
+def _copy(a: bytes) -> bytes:
+ """Ugly copy that works everywhere incl micropython"""
+ if len(a) == 0:
+ return b""
+ return a[:1] + a[1:]
+
+
+def _find_library():
+ library_path = None
+ extension = ""
+ if platform.system() == "Darwin":
+ extension = ".dylib"
+ elif platform.system() == "Linux":
+ extension = ".so"
+ elif platform.system() == "Windows":
+ extension = ".dll"
+
+ path = os.path.join(
+ os.path.dirname(__file__),
+ "prebuilt/libsecp256k1_%s_%s%s"
+ % (platform.system().lower(), platform.machine().lower(), extension),
+ )
+ if os.path.isfile(path):
+ return path
+ # try searching
+ if not library_path:
+ library_path = ctypes.util.find_library("libsecp256k1")
+ if not library_path:
+ library_path = ctypes.util.find_library("secp256k1")
+ # library search failed
+ if not library_path:
+ if platform.system() == "Linux" and os.path.isfile(
+ "/usr/local/lib/libsecp256k1.so.0"
+ ):
+ library_path = "/usr/local/lib/libsecp256k1.so.0"
+ return library_path
+
+
+@locked
+def _init(flags=(CONTEXT_SIGN | CONTEXT_VERIFY)):
+ library_path = _find_library()
+ # meh, can't find library
+ if not library_path:
+ raise RuntimeError(
+ "Can't find libsecp256k1 library. Make sure to compile and install it."
+ )
+
+ secp256k1 = ctypes.cdll.LoadLibrary(library_path)
+
+ secp256k1.secp256k1_context_create.argtypes = [c_uint]
+ secp256k1.secp256k1_context_create.restype = c_void_p
+
+ secp256k1.secp256k1_context_randomize.argtypes = [c_void_p, c_char_p]
+ secp256k1.secp256k1_context_randomize.restype = c_int
+
+ secp256k1.secp256k1_ec_seckey_verify.argtypes = [c_void_p, c_char_p]
+ secp256k1.secp256k1_ec_seckey_verify.restype = c_int
+
+ secp256k1.secp256k1_ec_privkey_negate.argtypes = [c_void_p, c_char_p]
+ secp256k1.secp256k1_ec_privkey_negate.restype = c_int
+
+ secp256k1.secp256k1_ec_pubkey_negate.argtypes = [c_void_p, c_char_p]
+ secp256k1.secp256k1_ec_pubkey_negate.restype = c_int
+
+ secp256k1.secp256k1_ec_privkey_tweak_add.argtypes = [c_void_p, c_char_p, c_char_p]
+ secp256k1.secp256k1_ec_privkey_tweak_add.restype = c_int
+
+ secp256k1.secp256k1_ec_privkey_tweak_mul.argtypes = [c_void_p, c_char_p, c_char_p]
+ secp256k1.secp256k1_ec_privkey_tweak_mul.restype = c_int
+
+ secp256k1.secp256k1_ec_pubkey_create.argtypes = [c_void_p, c_void_p, c_char_p]
+ secp256k1.secp256k1_ec_pubkey_create.restype = c_int
+
+ secp256k1.secp256k1_ec_pubkey_parse.argtypes = [c_void_p, c_char_p, c_char_p, c_int]
+ secp256k1.secp256k1_ec_pubkey_parse.restype = c_int
+
+ secp256k1.secp256k1_ec_pubkey_serialize.argtypes = [
+ c_void_p,
+ c_char_p,
+ c_void_p,
+ c_char_p,
+ c_uint,
+ ]
+ secp256k1.secp256k1_ec_pubkey_serialize.restype = c_int
+
+ secp256k1.secp256k1_ec_pubkey_tweak_add.argtypes = [c_void_p, c_char_p, c_char_p]
+ secp256k1.secp256k1_ec_pubkey_tweak_add.restype = c_int
+
+ secp256k1.secp256k1_ec_pubkey_tweak_mul.argtypes = [c_void_p, c_char_p, c_char_p]
+ secp256k1.secp256k1_ec_pubkey_tweak_mul.restype = c_int
+
+ secp256k1.secp256k1_ecdsa_signature_parse_compact.argtypes = [
+ c_void_p,
+ c_char_p,
+ c_char_p,
+ ]
+ secp256k1.secp256k1_ecdsa_signature_parse_compact.restype = c_int
+
+ secp256k1.secp256k1_ecdsa_signature_serialize_compact.argtypes = [
+ c_void_p,
+ c_char_p,
+ c_char_p,
+ ]
+ secp256k1.secp256k1_ecdsa_signature_serialize_compact.restype = c_int
+
+ secp256k1.secp256k1_ecdsa_signature_parse_der.argtypes = [
+ c_void_p,
+ c_char_p,
+ c_char_p,
+ c_uint,
+ ]
+ secp256k1.secp256k1_ecdsa_signature_parse_der.restype = c_int
+
+ secp256k1.secp256k1_ecdsa_signature_serialize_der.argtypes = [
+ c_void_p,
+ c_char_p,
+ c_void_p,
+ c_char_p,
+ ]
+ secp256k1.secp256k1_ecdsa_signature_serialize_der.restype = c_int
+
+ secp256k1.secp256k1_ecdsa_sign.argtypes = [
+ c_void_p,
+ c_char_p,
+ c_char_p,
+ c_char_p,
+ c_void_p,
+ c_char_p,
+ ]
+ secp256k1.secp256k1_ecdsa_sign.restype = c_int
+
+ secp256k1.secp256k1_ecdsa_verify.argtypes = [c_void_p, c_char_p, c_char_p, c_char_p]
+ secp256k1.secp256k1_ecdsa_verify.restype = c_int
+
+ secp256k1.secp256k1_ec_pubkey_combine.argtypes = [
+ c_void_p,
+ c_char_p,
+ c_void_p,
+ c_size_t,
+ ]
+ secp256k1.secp256k1_ec_pubkey_combine.restype = c_int
+
+ # ecdh
+ try:
+ secp256k1.secp256k1_ecdh.argtypes = [
+ c_void_p, # ctx
+ c_char_p, # output
+ c_char_p, # point
+ c_char_p, # scalar
+ CFUNCTYPE, # hashfp
+ c_void_p, # data
+ ]
+ secp256k1.secp256k1_ecdh.restype = c_int
+ except:
+ pass
+
+ # schnorr sig
+ try:
+ secp256k1.secp256k1_xonly_pubkey_from_pubkey.argtypes = [
+ c_void_p, # ctx
+ c_char_p, # xonly pubkey
+ POINTER(c_int), # parity
+ c_char_p, # pubkey
+ ]
+ secp256k1.secp256k1_xonly_pubkey_from_pubkey.restype = c_int
+
+ secp256k1.secp256k1_schnorrsig_verify.argtypes = [
+ c_void_p, # ctx
+ c_char_p, # sig
+ c_char_p, # msg
+ c_char_p, # pubkey
+ ]
+ secp256k1.secp256k1_schnorrsig_verify.restype = c_int
+
+ secp256k1.secp256k1_schnorrsig_sign.argtypes = [
+ c_void_p, # ctx
+ c_char_p, # sig
+ c_char_p, # msg
+ c_char_p, # keypair
+ c_void_p, # nonce_function
+ c_char_p, # extra data
+ ]
+ secp256k1.secp256k1_schnorrsig_sign.restype = c_int
+
+ secp256k1.secp256k1_keypair_create.argtypes = [
+ c_void_p, # ctx
+ c_char_p, # keypair
+ c_char_p, # secret
+ ]
+ secp256k1.secp256k1_keypair_create.restype = c_int
+ except:
+ pass
+
+ # recoverable module
+ try:
+ secp256k1.secp256k1_ecdsa_sign_recoverable.argtypes = [
+ c_void_p,
+ c_char_p,
+ c_char_p,
+ c_char_p,
+ c_void_p,
+ c_void_p,
+ ]
+ secp256k1.secp256k1_ecdsa_sign_recoverable.restype = c_int
+
+ secp256k1.secp256k1_ecdsa_recoverable_signature_parse_compact.argtypes = [
+ c_void_p,
+ c_char_p,
+ c_char_p,
+ c_int,
+ ]
+ secp256k1.secp256k1_ecdsa_recoverable_signature_parse_compact.restype = c_int
+
+ secp256k1.secp256k1_ecdsa_recoverable_signature_serialize_compact.argtypes = [
+ c_void_p,
+ c_char_p,
+ c_char_p,
+ c_char_p,
+ ]
+ secp256k1.secp256k1_ecdsa_recoverable_signature_serialize_compact.restype = (
+ c_int
+ )
+
+ secp256k1.secp256k1_ecdsa_recoverable_signature_convert.argtypes = [
+ c_void_p,
+ c_char_p,
+ c_char_p,
+ ]
+ secp256k1.secp256k1_ecdsa_recoverable_signature_convert.restype = c_int
+
+ secp256k1.secp256k1_ecdsa_recover.argtypes = [
+ c_void_p,
+ c_char_p,
+ c_char_p,
+ c_char_p,
+ ]
+ secp256k1.secp256k1_ecdsa_recover.restype = c_int
+ except:
+ pass
+
+ # zkp modules
+ try:
+ # generator module
+ secp256k1.secp256k1_generator_parse.argtypes = [c_void_p, c_char_p, c_char_p]
+ secp256k1.secp256k1_generator_parse.restype = c_int
+
+ secp256k1.secp256k1_generator_serialize.argtypes = [
+ c_void_p,
+ c_char_p,
+ c_char_p,
+ ]
+ secp256k1.secp256k1_generator_serialize.restype = c_int
+
+ secp256k1.secp256k1_generator_generate.argtypes = [c_void_p, c_char_p, c_char_p]
+ secp256k1.secp256k1_generator_generate.restype = c_int
+
+ secp256k1.secp256k1_generator_generate_blinded.argtypes = [
+ c_void_p,
+ c_char_p,
+ c_char_p,
+ c_char_p,
+ ]
+ secp256k1.secp256k1_generator_generate_blinded.restype = c_int
+
+ # pederson commitments
+ secp256k1.secp256k1_pedersen_commitment_parse.argtypes = [
+ c_void_p,
+ c_char_p,
+ c_char_p,
+ ]
+ secp256k1.secp256k1_pedersen_commitment_parse.restype = c_int
+
+ secp256k1.secp256k1_pedersen_commitment_serialize.argtypes = [
+ c_void_p,
+ c_char_p,
+ c_char_p,
+ ]
+ secp256k1.secp256k1_pedersen_commitment_serialize.restype = c_int
+
+ secp256k1.secp256k1_pedersen_commit.argtypes = [
+ c_void_p,
+ c_char_p,
+ c_char_p,
+ c_uint64,
+ c_char_p,
+ ]
+ secp256k1.secp256k1_pedersen_commit.restype = c_int
+
+ secp256k1.secp256k1_pedersen_blind_generator_blind_sum.argtypes = [
+ c_void_p, # const secp256k1_context* ctx,
+ POINTER(c_uint64), # const uint64_t *value,
+ c_void_p, # const unsigned char* const* generator_blind,
+ c_void_p, # unsigned char* const* blinding_factor,
+ c_size_t, # size_t n_total,
+ c_size_t, # size_t n_inputs
+ ]
+ secp256k1.secp256k1_pedersen_blind_generator_blind_sum.restype = c_int
+
+ secp256k1.secp256k1_pedersen_verify_tally.argtypes = [
+ c_void_p,
+ c_void_p,
+ c_size_t,
+ c_void_p,
+ c_size_t,
+ ]
+ secp256k1.secp256k1_pedersen_verify_tally.restype = c_int
+
+ # rangeproof
+ secp256k1.secp256k1_rangeproof_rewind.argtypes = [
+ c_void_p, # ctx
+ c_char_p, # vbf out
+ POINTER(c_uint64), # value out
+ c_char_p, # message out
+ POINTER(c_uint64), # msg out len
+ c_char_p, # nonce
+ POINTER(c_uint64), # min value
+ POINTER(c_uint64), # max value
+ c_char_p, # pedersen commitment
+ c_char_p, # range proof
+ c_uint64, # proof len
+ c_char_p, # extra commitment (scriptpubkey)
+ c_uint64, # extra len
+ c_char_p, # generator
+ ]
+ secp256k1.secp256k1_rangeproof_rewind.restype = c_int
+
+ secp256k1.secp256k1_rangeproof_verify.argtypes = [
+ c_void_p, # ctx
+ POINTER(c_uint64), # min value
+ POINTER(c_uint64), # max value
+ c_char_p, # pedersen commitment
+ c_char_p, # proof
+ c_uint64, # proof len
+ c_char_p, # extra
+ c_uint64, # extra len
+ c_char_p, # generator
+ ]
+ secp256k1.secp256k1_rangeproof_verify.restype = c_int
+
+ secp256k1.secp256k1_rangeproof_sign.argtypes = [
+ c_void_p, # ctx
+ c_char_p, # proof
+ POINTER(c_uint64), # plen
+ c_uint64, # min_value
+ c_char_p, # commit
+ c_char_p, # blind
+ c_char_p, # nonce
+ c_int, # exp
+ c_int, # min_bits
+ c_uint64, # value
+ c_char_p, # message
+ c_uint64, # msg_len
+ c_char_p, # extra_commit
+ c_uint64, # extra_commit_len
+ c_char_p, # gen
+ ]
+ secp256k1.secp256k1_rangeproof_sign.restype = c_int
+
+ # musig
+ secp256k1.secp256k1_musig_pubkey_combine.argtypes = [
+ c_void_p,
+ c_void_p,
+ c_char_p,
+ c_void_p,
+ c_void_p,
+ c_size_t,
+ ]
+ secp256k1.secp256k1_musig_pubkey_combine.restype = c_int
+
+ # surjection proofs
+ secp256k1.secp256k1_surjectionproof_initialize.argtypes = [
+ c_void_p, # const secp256k1_context* ctx,
+ c_char_p, # secp256k1_surjectionproof* proof,
+ POINTER(c_size_t), # size_t *input_index,
+ c_void_p, # c_char_p, # const secp256k1_fixed_asset_tag* fixed_input_tags,
+ c_size_t, # const size_t n_input_tags,
+ c_size_t, # const size_t n_input_tags_to_use,
+ c_char_p, # const secp256k1_fixed_asset_tag* fixed_output_tag,
+ c_size_t, # const size_t n_max_iterations,
+ c_char_p, # const unsigned char *random_seed32
+ ]
+ secp256k1.secp256k1_surjectionproof_initialize.restype = c_int
+
+ secp256k1.secp256k1_surjectionproof_generate.argtypes = [
+ c_void_p, # const secp256k1_context* ctx,
+ c_char_p, # secp256k1_surjectionproof* proof,
+ c_char_p, # const secp256k1_generator* ephemeral_input_tags,
+ c_size_t, # size_t n_ephemeral_input_tags,
+ c_char_p, # const secp256k1_generator* ephemeral_output_tag,
+ c_size_t, # size_t input_index,
+ c_char_p, # const unsigned char *input_blinding_key,
+ c_char_p, # const unsigned char *output_blinding_key
+ ]
+ secp256k1.secp256k1_surjectionproof_generate.restype = c_int
+
+ secp256k1.secp256k1_surjectionproof_verify.argtypes = [
+ c_void_p, # const secp256k1_context* ctx,
+ c_char_p, # const secp256k1_surjectionproof* proof,
+ c_char_p, # const secp256k1_generator* ephemeral_input_tags,
+ c_size_t, # size_t n_ephemeral_input_tags,
+ c_char_p, # const secp256k1_generator* ephemeral_output_tag
+ ]
+ secp256k1.secp256k1_surjectionproof_verify.restype = c_int
+
+ secp256k1.secp256k1_surjectionproof_serialize.argtypes = [
+ c_void_p, # const secp256k1_context* ctx,
+ c_char_p, # unsigned char *output,
+ POINTER(c_size_t), # size_t *outputlen,
+ c_char_p, # const secp256k1_surjectionproof *proof
+ ]
+ secp256k1.secp256k1_surjectionproof_serialize.restype = c_int
+
+ secp256k1.secp256k1_surjectionproof_serialized_size.argtypes = [
+ c_void_p, # const secp256k1_context* ctx,
+ c_char_p, # const secp256k1_surjectionproof* proof
+ ]
+ secp256k1.secp256k1_surjectionproof_serialized_size.restype = c_size_t
+
+ secp256k1.secp256k1_surjectionproof_parse.argtypes = [
+ c_void_p,
+ c_char_p,
+ c_char_p,
+ c_size_t,
+ ]
+ secp256k1.secp256k1_surjectionproof_parse.restype = c_int
+
+ except:
+ pass
+
+ secp256k1.ctx = secp256k1.secp256k1_context_create(flags)
+
+ r = secp256k1.secp256k1_context_randomize(secp256k1.ctx, os.urandom(32))
+
+ return secp256k1
+
+
+_secp = _init()
+
+
+# bindings equal to ones in micropython
+@locked
+def context_randomize(seed, context=_secp.ctx):
+ if len(seed) != 32:
+ raise ValueError("Seed should be 32 bytes long")
+ if _secp.secp256k1_context_randomize(context, seed) == 0:
+ raise RuntimeError("Failed to randomize context")
+
+
+@locked
+def ec_pubkey_create(secret, context=_secp.ctx):
+ if len(secret) != 32:
+ raise ValueError("Private key should be 32 bytes long")
+ pub = bytes(64)
+ r = _secp.secp256k1_ec_pubkey_create(context, pub, secret)
+ if r == 0:
+ raise ValueError("Invalid private key")
+ return pub
+
+
+@locked
+def ec_pubkey_parse(sec, context=_secp.ctx):
+ if len(sec) != 33 and len(sec) != 65:
+ raise ValueError("Serialized pubkey should be 33 or 65 bytes long")
+ if len(sec) == 33:
+ if sec[0] != 0x02 and sec[0] != 0x03:
+ raise ValueError("Compressed pubkey should start with 0x02 or 0x03")
+ else:
+ if sec[0] != 0x04:
+ raise ValueError("Uncompressed pubkey should start with 0x04")
+ pub = bytes(64)
+ r = _secp.secp256k1_ec_pubkey_parse(context, pub, sec, len(sec))
+ if r == 0:
+ raise ValueError("Failed parsing public key")
+ return pub
+
+
+@locked
+def ec_pubkey_serialize(pubkey, flag=EC_COMPRESSED, context=_secp.ctx):
+ if len(pubkey) != 64:
+ raise ValueError("Pubkey should be 64 bytes long")
+ if flag not in [EC_COMPRESSED, EC_UNCOMPRESSED]:
+ raise ValueError("Invalid flag")
+ sec = bytes(33) if (flag == EC_COMPRESSED) else bytes(65)
+ sz = c_size_t(len(sec))
+ r = _secp.secp256k1_ec_pubkey_serialize(context, sec, byref(sz), pubkey, flag)
+ if r == 0:
+ raise ValueError("Failed to serialize pubkey")
+ return sec
+
+
+@locked
+def ecdsa_signature_parse_compact(compact_sig, context=_secp.ctx):
+ if len(compact_sig) != 64:
+ raise ValueError("Compact signature should be 64 bytes long")
+ sig = bytes(64)
+ r = _secp.secp256k1_ecdsa_signature_parse_compact(context, sig, compact_sig)
+ if r == 0:
+ raise ValueError("Failed parsing compact signature")
+ return sig
+
+
+@locked
+def ecdsa_signature_parse_der(der, context=_secp.ctx):
+ sig = bytes(64)
+ r = _secp.secp256k1_ecdsa_signature_parse_der(context, sig, der, len(der))
+ if r == 0:
+ raise ValueError("Failed parsing compact signature")
+ return sig
+
+
+@locked
+def ecdsa_signature_serialize_der(sig, context=_secp.ctx):
+ if len(sig) != 64:
+ raise ValueError("Signature should be 64 bytes long")
+ der = bytes(78) # max
+ sz = c_size_t(len(der))
+ r = _secp.secp256k1_ecdsa_signature_serialize_der(context, der, byref(sz), sig)
+ if r == 0:
+ raise ValueError("Failed serializing der signature")
+ return der[: sz.value]
+
+
+@locked
+def ecdsa_signature_serialize_compact(sig, context=_secp.ctx):
+ if len(sig) != 64:
+ raise ValueError("Signature should be 64 bytes long")
+ ser = bytes(64)
+ r = _secp.secp256k1_ecdsa_signature_serialize_compact(context, ser, sig)
+ if r == 0:
+ raise ValueError("Failed serializing der signature")
+ return ser
+
+
+@locked
+def ecdsa_signature_normalize(sig, context=_secp.ctx):
+ if len(sig) != 64:
+ raise ValueError("Signature should be 64 bytes long")
+ sig2 = bytes(64)
+ r = _secp.secp256k1_ecdsa_signature_normalize(context, sig2, sig)
+ return sig2
+
+
+@locked
+def ecdsa_verify(sig, msg, pub, context=_secp.ctx):
+ if len(sig) != 64:
+ raise ValueError("Signature should be 64 bytes long")
+ if len(msg) != 32:
+ raise ValueError("Message should be 32 bytes long")
+ if len(pub) != 64:
+ raise ValueError("Public key should be 64 bytes long")
+ r = _secp.secp256k1_ecdsa_verify(context, sig, msg, pub)
+ return bool(r)
+
+
+@locked
+def ecdsa_sign(msg, secret, nonce_function=None, extra_data=None, context=_secp.ctx):
+ if len(msg) != 32:
+ raise ValueError("Message should be 32 bytes long")
+ if len(secret) != 32:
+ raise ValueError("Secret key should be 32 bytes long")
+ if extra_data and len(extra_data) != 32:
+ raise ValueError("Extra data should be 32 bytes long")
+ sig = bytes(64)
+ r = _secp.secp256k1_ecdsa_sign(
+ context, sig, msg, secret, nonce_function, extra_data
+ )
+ if r == 0:
+ raise ValueError("Failed to sign")
+ return sig
+
+
+@locked
+def ec_seckey_verify(secret, context=_secp.ctx):
+ if len(secret) != 32:
+ raise ValueError("Secret should be 32 bytes long")
+ return bool(_secp.secp256k1_ec_seckey_verify(context, secret))
+
+
+@locked
+def ec_privkey_negate(secret, context=_secp.ctx):
+ if len(secret) != 32:
+ raise ValueError("Secret should be 32 bytes long")
+ b = _copy(secret)
+ _secp.secp256k1_ec_privkey_negate(context, b)
+ return b
+
+
+@locked
+def ec_pubkey_negate(pubkey, context=_secp.ctx):
+ if len(pubkey) != 64:
+ raise ValueError("Pubkey should be a 64-byte structure")
+ pub = _copy(pubkey)
+ r = _secp.secp256k1_ec_pubkey_negate(context, pub)
+ if r == 0:
+ raise ValueError("Failed to negate pubkey")
+ return pub
+
+
+@locked
+def ec_privkey_tweak_add(secret, tweak, context=_secp.ctx):
+ if len(secret) != 32 or len(tweak) != 32:
+ raise ValueError("Secret and tweak should both be 32 bytes long")
+ t = _copy(tweak)
+ if _secp.secp256k1_ec_privkey_tweak_add(context, secret, tweak) == 0:
+ raise ValueError("Failed to tweak the secret")
+ return None
+
+
+@locked
+def ec_pubkey_tweak_add(pub, tweak, context=_secp.ctx):
+ if len(pub) != 64:
+ raise ValueError("Public key should be 64 bytes long")
+ if len(tweak) != 32:
+ raise ValueError("Tweak should be 32 bytes long")
+ t = _copy(tweak)
+ if _secp.secp256k1_ec_pubkey_tweak_add(context, pub, tweak) == 0:
+ raise ValueError("Failed to tweak the public key")
+ return None
+
+
+@locked
+def ec_privkey_add(secret, tweak, context=_secp.ctx):
+ if len(secret) != 32 or len(tweak) != 32:
+ raise ValueError("Secret and tweak should both be 32 bytes long")
+ # ugly copy that works in mpy and py
+ s = _copy(secret)
+ t = _copy(tweak)
+ if _secp.secp256k1_ec_privkey_tweak_add(context, s, t) == 0:
+ raise ValueError("Failed to tweak the secret")
+ return s
+
+
+@locked
+def ec_pubkey_add(pub, tweak, context=_secp.ctx):
+ if len(pub) != 64:
+ raise ValueError("Public key should be 64 bytes long")
+ if len(tweak) != 32:
+ raise ValueError("Tweak should be 32 bytes long")
+ p = _copy(pub)
+ if _secp.secp256k1_ec_pubkey_tweak_add(context, p, tweak) == 0:
+ raise ValueError("Failed to tweak the public key")
+ return p
+
+
+@locked
+def ec_privkey_tweak_mul(secret, tweak, context=_secp.ctx):
+ if len(secret) != 32 or len(tweak) != 32:
+ raise ValueError("Secret and tweak should both be 32 bytes long")
+ if _secp.secp256k1_ec_privkey_tweak_mul(context, secret, tweak) == 0:
+ raise ValueError("Failed to tweak the secret")
+
+
+@locked
+def ec_pubkey_tweak_mul(pub, tweak, context=_secp.ctx):
+ if len(pub) != 64:
+ raise ValueError("Public key should be 64 bytes long")
+ if len(tweak) != 32:
+ raise ValueError("Tweak should be 32 bytes long")
+ if _secp.secp256k1_ec_pubkey_tweak_mul(context, pub, tweak) == 0:
+ raise ValueError("Failed to tweak the public key")
+
+
+@locked
+def ec_pubkey_combine(*args, context=_secp.ctx):
+ pub = bytes(64)
+ pubkeys = (c_char_p * len(args))(*args)
+ r = _secp.secp256k1_ec_pubkey_combine(context, pub, pubkeys, len(args))
+ if r == 0:
+ raise ValueError("Failed to combine pubkeys")
+ return pub
+
+
+# ecdh
+@locked
+def ecdh(pubkey, scalar, hashfn=None, data=None, context=_secp.ctx):
+ if not len(pubkey) == 64:
+ raise ValueError("Pubkey should be 64 bytes long")
+ if not len(scalar) == 32:
+ raise ValueError("Scalar should be 32 bytes long")
+ secret = bytes(32)
+ if hashfn is None:
+ res = _secp.secp256k1_ecdh(context, secret, pubkey, scalar, None, None)
+ else:
+
+ def _hashfn(out, x, y):
+ x = ctypes.string_at(x, 32)
+ y = ctypes.string_at(y, 32)
+ try:
+ res = hashfn(x, y, data)
+ except Exception as e:
+ return 0
+ out = cast(out, POINTER(c_char * 32))
+ out.contents.value = res
+ return 1
+
+ HASHFN = CFUNCTYPE(c_int, c_void_p, c_void_p, c_void_p)
+ res = _secp.secp256k1_ecdh(
+ context, secret, pubkey, scalar, HASHFN(_hashfn), data
+ )
+ if res != 1:
+ raise RuntimeError("Failed to compute the shared secret")
+ return secret
+
+
+# schnorrsig
+@locked
+def xonly_pubkey_from_pubkey(pubkey, context=_secp.ctx):
+ if len(pubkey) != 64:
+ raise ValueError("Pubkey should be 64 bytes long")
+ pointer = POINTER(c_int)
+ parity = pointer(c_int(0))
+ xonly_pub = bytes(64)
+ res = _secp.secp256k1_xonly_pubkey_from_pubkey(context, xonly_pub, parity, pubkey)
+ if res != 1:
+ raise RuntimeError("Failed to convert the pubkey")
+ return xonly_pub, bool(parity.contents.value)
+
+
+@locked
+def schnorrsig_verify(sig, msg, pubkey, context=_secp.ctx):
+ assert len(sig) == 64
+ assert len(msg) == 32
+ assert len(pubkey) == 64
+ res = _secp.secp256k1_schnorrsig_verify(context, sig, msg, pubkey)
+ return bool(res)
+
+
+@locked
+def keypair_create(secret, context=_secp.ctx):
+ assert len(secret) == 32
+ keypair = bytes(96)
+ r = _secp.secp256k1_keypair_create(context, keypair, secret)
+ if r == 0:
+ raise ValueError("Failed to create keypair")
+ return keypair
+
+
+# not @locked because it uses keypair_create inside
+def schnorrsig_sign(
+ msg, keypair, nonce_function=None, extra_data=None, context=_secp.ctx
+):
+ assert len(msg) == 32
+ if len(keypair) == 32:
+ keypair = keypair_create(keypair, context=context)
+ with _lock:
+ assert len(keypair) == 96
+ sig = bytes(64)
+ r = _secp.secp256k1_schnorrsig_sign(
+ context, sig, msg, keypair, nonce_function, extra_data
+ )
+ if r == 0:
+ raise ValueError("Failed to sign")
+ return sig
+
+
+# recoverable
+@locked
+def ecdsa_sign_recoverable(msg, secret, context=_secp.ctx):
+ if len(msg) != 32:
+ raise ValueError("Message should be 32 bytes long")
+ if len(secret) != 32:
+ raise ValueError("Secret key should be 32 bytes long")
+ sig = bytes(65)
+ r = _secp.secp256k1_ecdsa_sign_recoverable(context, sig, msg, secret, None, None)
+ if r == 0:
+ raise ValueError("Failed to sign")
+ return sig
+
+
+@locked
+def ecdsa_recoverable_signature_serialize_compact(sig, context=_secp.ctx):
+ if len(sig) != 65:
+ raise ValueError("Recoverable signature should be 65 bytes long")
+ ser = bytes(64)
+ idx = bytes(1)
+ r = _secp.secp256k1_ecdsa_recoverable_signature_serialize_compact(
+ context, ser, idx, sig
+ )
+ if r == 0:
+ raise ValueError("Failed serializing der signature")
+ return ser, idx[0]
+
+
+@locked
+def ecdsa_recoverable_signature_parse_compact(compact_sig, recid, context=_secp.ctx):
+ if len(compact_sig) != 64:
+ raise ValueError("Signature should be 64 bytes long")
+ sig = bytes(65)
+ r = _secp.secp256k1_ecdsa_recoverable_signature_parse_compact(
+ context, sig, compact_sig, recid
+ )
+ if r == 0:
+ raise ValueError("Failed parsing compact signature")
+ return sig
+
+
+@locked
+def ecdsa_recoverable_signature_convert(sigin, context=_secp.ctx):
+ if len(sigin) != 65:
+ raise ValueError("Recoverable signature should be 65 bytes long")
+ sig = bytes(64)
+ r = _secp.secp256k1_ecdsa_recoverable_signature_convert(context, sig, sigin)
+ if r == 0:
+ raise ValueError("Failed converting signature")
+ return sig
+
+
+@locked
+def ecdsa_recover(sig, msghash, context=_secp.ctx):
+ if len(sig) != 65:
+ raise ValueError("Recoverable signature should be 65 bytes long")
+ if len(msghash) != 32:
+ raise ValueError("Message should be 32 bytes long")
+ pub = bytes(64)
+ r = _secp.secp256k1_ecdsa_recover(context, pub, sig, msghash)
+ if r == 0:
+ raise ValueError("Failed to recover public key")
+ return pub
+
+
+# zkp modules
+
+
+@locked
+def pedersen_commitment_parse(inp, context=_secp.ctx):
+ if len(inp) != 33:
+ raise ValueError("Serialized commitment should be 33 bytes long")
+ commit = bytes(64)
+ r = _secp.secp256k1_pedersen_commitment_parse(context, commit, inp)
+ if r == 0:
+ raise ValueError("Failed to parse commitment")
+ return commit
+
+
+@locked
+def pedersen_commitment_serialize(commit, context=_secp.ctx):
+ if len(commit) != 64:
+ raise ValueError("Commitment should be 64 bytes long")
+ sec = bytes(33)
+ r = _secp.secp256k1_pedersen_commitment_serialize(context, sec, commit)
+ if r == 0:
+ raise ValueError("Failed to serialize commitment")
+ return sec
+
+
+@locked
+def pedersen_commit(vbf, value, gen, context=_secp.ctx):
+ if len(gen) != 64:
+ raise ValueError("Generator should be 64 bytes long")
+ if len(vbf) != 32:
+ raise ValueError(f"Blinding factor should be 32 bytes long, not {len(vbf)}")
+ commit = bytes(64)
+ r = _secp.secp256k1_pedersen_commit(context, commit, vbf, value, gen)
+ if r == 0:
+ raise ValueError("Failed to create commitment")
+ return commit
+
+
+@locked
+def pedersen_blind_generator_blind_sum(
+ values, gens, vbfs, num_inputs, context=_secp.ctx
+):
+ vals = (c_uint64 * len(values))(*values)
+ vbf = bytes(vbfs[-1])
+ p = c_char_p(vbf) # obtain a pointer of various types
+ address = cast(p, c_void_p).value
+
+ vbfs_joined = (c_char_p * len(vbfs))(*vbfs[:-1], address)
+ gens_joined = (c_char_p * len(gens))(*gens)
+ res = _secp.secp256k1_pedersen_blind_generator_blind_sum(
+ context, vals, gens_joined, vbfs_joined, len(values), num_inputs
+ )
+ if res == 0:
+ raise ValueError("Failed to get the last blinding factor.")
+ res = (c_char * 32).from_address(address).raw
+ assert len(res) == 32
+ return res
+
+
+@locked
+def pedersen_verify_tally(ins, outs, context=_secp.ctx):
+ in_ptr = (c_char_p * len(ins))(*ins)
+ out_ptr = (c_char_p * len(outs))(*outs)
+ res = _secp.secp256k1_pedersen_verify_tally(
+ context, in_ptr, len(in_ptr), out_ptr, len(out_ptr)
+ )
+ return bool(res)
+
+
+# generator
+@locked
+def generator_parse(inp, context=_secp.ctx):
+ if len(inp) != 33:
+ raise ValueError("Serialized generator should be 33 bytes long")
+ gen = bytes(64)
+ r = _secp.secp256k1_generator_parse(context, gen, inp)
+ if r == 0:
+ raise ValueError("Failed to parse generator")
+ return gen
+
+
+@locked
+def generator_generate(asset, context=_secp.ctx):
+ if len(asset) != 32:
+ raise ValueError("Asset should be 32 bytes long")
+ gen = bytes(64)
+ r = _secp.secp256k1_generator_generate(context, gen, asset)
+ if r == 0:
+ raise ValueError("Failed to generate generator")
+ return gen
+
+
+@locked
+def generator_generate_blinded(asset, abf, context=_secp.ctx):
+ if len(asset) != 32:
+ raise ValueError("Asset should be 32 bytes long")
+ if len(abf) != 32:
+ raise ValueError("Asset blinding factor should be 32 bytes long")
+ gen = bytes(64)
+ r = _secp.secp256k1_generator_generate_blinded(context, gen, asset, abf)
+ if r == 0:
+ raise ValueError("Failed to generate generator")
+ return gen
+
+
+@locked
+def generator_serialize(generator, context=_secp.ctx):
+ if len(generator) != 64:
+ raise ValueError("Generator should be 64 bytes long")
+ sec = bytes(33)
+ if _secp.secp256k1_generator_serialize(context, sec, generator) == 0:
+ raise RuntimeError("Failed to serialize generator")
+ return sec
+
+
+# rangeproof
+@locked
+def rangeproof_rewind(
+ proof,
+ nonce,
+ value_commitment,
+ script_pubkey,
+ generator,
+ message_length=64,
+ context=_secp.ctx,
+):
+ if len(generator) != 64:
+ raise ValueError("Generator should be 64 bytes long")
+ if len(nonce) != 32:
+ raise ValueError("Nonce should be 32 bytes long")
+ if len(value_commitment) != 64:
+ raise ValueError("Value commitment should be 64 bytes long")
+
+ pointer = POINTER(c_uint64)
+
+ msg = b"\x00" * message_length
+ msglen = pointer(c_uint64(len(msg)))
+
+ vbf_out = b"\x00" * 32
+ value_out = pointer(c_uint64(0))
+ min_value = pointer(c_uint64(0))
+ max_value = pointer(c_uint64(0))
+ res = _secp.secp256k1_rangeproof_rewind(
+ context,
+ vbf_out,
+ value_out,
+ msg,
+ msglen,
+ nonce,
+ min_value,
+ max_value,
+ value_commitment,
+ proof,
+ len(proof),
+ script_pubkey,
+ len(script_pubkey),
+ generator,
+ )
+ if res != 1:
+ raise RuntimeError("Failed to rewind the proof")
+ return (
+ value_out.contents.value,
+ vbf_out,
+ msg[: msglen.contents.value],
+ min_value.contents.value,
+ max_value.contents.value,
+ )
+
+
+# rangeproof
+
+
+@locked
+def rangeproof_verify(
+ proof, value_commitment, script_pubkey, generator, context=_secp.ctx
+):
+ if len(generator) != 64:
+ raise ValueError("Generator should be 64 bytes long")
+ if len(value_commitment) != 64:
+ raise ValueError("Value commitment should be 64 bytes long")
+
+ pointer = POINTER(c_uint64)
+ min_value = pointer(c_uint64(0))
+ max_value = pointer(c_uint64(0))
+ res = _secp.secp256k1_rangeproof_verify(
+ context,
+ min_value,
+ max_value,
+ value_commitment,
+ proof,
+ len(proof),
+ script_pubkey,
+ len(script_pubkey),
+ generator,
+ )
+ if res != 1:
+ raise RuntimeError("Failed to verify the proof")
+ return min_value.contents.value, max_value.contents.value
+
+
+@locked
+def rangeproof_sign(
+ nonce,
+ value,
+ value_commitment,
+ vbf,
+ message,
+ extra,
+ gen,
+ min_value=1,
+ exp=0,
+ min_bits=52,
+ context=_secp.ctx,
+):
+ if value == 0:
+ min_value = 0
+ if len(gen) != 64:
+ raise ValueError("Generator should be 64 bytes long")
+ if len(nonce) != 32:
+ raise ValueError("Nonce should be 32 bytes long")
+ if len(value_commitment) != 64:
+ raise ValueError("Value commitment should be 64 bytes long")
+ if len(vbf) != 32:
+ raise ValueError("Value blinding factor should be 32 bytes long")
+ proof = bytes(5134)
+ pointer = POINTER(c_uint64)
+ prooflen = pointer(c_uint64(len(proof)))
+ res = _secp.secp256k1_rangeproof_sign(
+ context,
+ proof,
+ prooflen,
+ min_value,
+ value_commitment,
+ vbf,
+ nonce,
+ exp,
+ min_bits,
+ value,
+ message,
+ len(message),
+ extra,
+ len(extra),
+ gen,
+ )
+ if res != 1:
+ raise RuntimeError("Failed to generate the proof")
+ return bytes(proof[: prooflen.contents.value])
+
+
+@locked
+def musig_pubkey_combine(*args, context=_secp.ctx):
+ pub = bytes(64)
+ # TODO: strange that behaviour is different from pubkey_combine...
+ pubkeys = b"".join(args) # (c_char_p * len(args))(*args)
+ res = _secp.secp256k1_musig_pubkey_combine(
+ context, None, pub, None, pubkeys, len(args)
+ )
+ if res == 0:
+ raise ValueError("Failed to combine pubkeys")
+ return pub
+
+
+# surjection proof
+@locked
+def surjectionproof_initialize(
+ in_tags, out_tag, seed, tags_to_use=None, iterations=100, context=_secp.ctx
+):
+ if tags_to_use is None:
+ tags_to_use = min(3, len(in_tags))
+ if seed is None:
+ seed = os.urandom(32)
+ proof = bytes(4 + 8 + 256 // 8 + 32 * 257)
+ pointer = POINTER(c_size_t)
+ input_index = pointer(c_size_t(0))
+ input_tags = b"".join(in_tags)
+ res = _secp.secp256k1_surjectionproof_initialize(
+ context,
+ proof,
+ input_index,
+ input_tags,
+ len(in_tags),
+ tags_to_use,
+ out_tag,
+ iterations,
+ seed,
+ )
+ if res == 0:
+ raise RuntimeError("Failed to initialize the proof")
+ return proof, input_index.contents.value
+
+
+@locked
+def surjectionproof_generate(
+ proof, in_idx, in_tags, out_tag, in_abf, out_abf, context=_secp.ctx
+):
+ res = _secp.secp256k1_surjectionproof_generate(
+ context,
+ proof,
+ b"".join(in_tags),
+ len(in_tags),
+ out_tag,
+ in_idx,
+ in_abf,
+ out_abf,
+ )
+ if not res:
+ raise RuntimeError("Failed to generate surjection proof")
+ return proof
+
+
+@locked
+def surjectionproof_verify(proof, in_tags, out_tag, context=_secp.ctx):
+ res = _secp.secp256k1_surjectionproof_verify(
+ context, proof, b"".join(in_tags), len(in_tags), out_tag
+ )
+ return bool(res)
+
+
+@locked
+def surjectionproof_serialize(proof, context=_secp.ctx):
+ s = _secp.secp256k1_surjectionproof_serialized_size(context, proof)
+ b = bytes(s)
+ pointer = POINTER(c_size_t)
+ sz = pointer(c_size_t(s))
+ _secp.secp256k1_surjectionproof_serialize(context, b, sz, proof)
+ if s != sz.contents.value:
+ raise RuntimeError("Failed to serialize surjection proof - size mismatch")
+ return b
+
+
+@locked
+def surjectionproof_parse(proof, context=_secp.ctx):
+ parsed_proof = bytes(4 + 8 + 256 // 8 + 32 * 257)
+ res = _secp.secp256k1_surjectionproof_parse(
+ context, parsed_proof, proof, len(proof)
+ )
+ if res == 0:
+ raise RuntimeError("Failed to parse surjection proof")
+ return parsed_proof
diff --git a/bitcoin_client/ledger_bitcoin/embit/util/key.py b/bitcoin_client/ledger_bitcoin/embit/util/key.py
new file mode 100644
index 000000000..13b01d955
--- /dev/null
+++ b/bitcoin_client/ledger_bitcoin/embit/util/key.py
@@ -0,0 +1,597 @@
+"""
+Copy-paste from key.py in bitcoin test_framework.
+This is a fallback option if the library can't do ctypes bindings to secp256k1 library.
+"""
+import random
+import hmac
+import hashlib
+
+
+def TaggedHash(tag, data):
+ ss = hashlib.sha256(tag.encode("utf-8")).digest()
+ ss += ss
+ ss += data
+ return hashlib.sha256(ss).digest()
+
+
+def modinv(a, n):
+ """Compute the modular inverse of a modulo n using the extended Euclidean
+ Algorithm. See https://en.wikipedia.org/wiki/Extended_Euclidean_algorithm#Modular_integers.
+ """
+ # TODO: Change to pow(a, -1, n) available in Python 3.8
+ t1, t2 = 0, 1
+ r1, r2 = n, a
+ while r2 != 0:
+ q = r1 // r2
+ t1, t2 = t2, t1 - q * t2
+ r1, r2 = r2, r1 - q * r2
+ if r1 > 1:
+ return None
+ if t1 < 0:
+ t1 += n
+ return t1
+
+
+def xor_bytes(b0, b1):
+ return bytes(x ^ y for (x, y) in zip(b0, b1))
+
+
+def jacobi_symbol(n, k):
+ """Compute the Jacobi symbol of n modulo k
+
+ See http://en.wikipedia.org/wiki/Jacobi_symbol
+
+ For our application k is always prime, so this is the same as the Legendre symbol.
+ """
+ assert k > 0 and k & 1, "jacobi symbol is only defined for positive odd k"
+ n %= k
+ t = 0
+ while n != 0:
+ while n & 1 == 0:
+ n >>= 1
+ r = k & 7
+ t ^= r == 3 or r == 5
+ n, k = k, n
+ t ^= n & k & 3 == 3
+ n = n % k
+ if k == 1:
+ return -1 if t else 1
+ return 0
+
+
+def modsqrt(a, p):
+ """Compute the square root of a modulo p when p % 4 = 3.
+
+ The Tonelli-Shanks algorithm can be used. See https://en.wikipedia.org/wiki/Tonelli-Shanks_algorithm
+
+ Limiting this function to only work for p % 4 = 3 means we don't need to
+ iterate through the loop. The highest n such that p - 1 = 2^n Q with Q odd
+ is n = 1. Therefore Q = (p-1)/2 and sqrt = a^((Q+1)/2) = a^((p+1)/4)
+
+ secp256k1's is defined over field of size 2**256 - 2**32 - 977, which is 3 mod 4.
+ """
+ if p % 4 != 3:
+ raise NotImplementedError("modsqrt only implemented for p % 4 = 3")
+ sqrt = pow(a, (p + 1) // 4, p)
+ if pow(sqrt, 2, p) == a % p:
+ return sqrt
+ return None
+
+
+class EllipticCurve:
+ def __init__(self, p, a, b):
+ """Initialize elliptic curve y^2 = x^3 + a*x + b over GF(p)."""
+ self.p = p
+ self.a = a % p
+ self.b = b % p
+
+ def affine(self, p1):
+ """Convert a Jacobian point tuple p1 to affine form, or None if at infinity.
+
+ An affine point is represented as the Jacobian (x, y, 1)"""
+ x1, y1, z1 = p1
+ if z1 == 0:
+ return None
+ inv = modinv(z1, self.p)
+ inv_2 = (inv**2) % self.p
+ inv_3 = (inv_2 * inv) % self.p
+ return ((inv_2 * x1) % self.p, (inv_3 * y1) % self.p, 1)
+
+ def has_even_y(self, p1):
+ """Whether the point p1 has an even Y coordinate when expressed in affine coordinates."""
+ return not (p1[2] == 0 or self.affine(p1)[1] & 1)
+
+ def negate(self, p1):
+ """Negate a Jacobian point tuple p1."""
+ x1, y1, z1 = p1
+ return (x1, (self.p - y1) % self.p, z1)
+
+ def on_curve(self, p1):
+ """Determine whether a Jacobian tuple p is on the curve (and not infinity)"""
+ x1, y1, z1 = p1
+ z2 = pow(z1, 2, self.p)
+ z4 = pow(z2, 2, self.p)
+ return (
+ z1 != 0
+ and (
+ pow(x1, 3, self.p)
+ + self.a * x1 * z4
+ + self.b * z2 * z4
+ - pow(y1, 2, self.p)
+ )
+ % self.p
+ == 0
+ )
+
+ def is_x_coord(self, x):
+ """Test whether x is a valid X coordinate on the curve."""
+ x_3 = pow(x, 3, self.p)
+ return jacobi_symbol(x_3 + self.a * x + self.b, self.p) != -1
+
+ def lift_x(self, x):
+ """Given an X coordinate on the curve, return a corresponding affine point for which the Y coordinate is even."""
+ x_3 = pow(x, 3, self.p)
+ v = x_3 + self.a * x + self.b
+ y = modsqrt(v, self.p)
+ if y is None:
+ return None
+ return (x, self.p - y if y & 1 else y, 1)
+
+ def double(self, p1):
+ """Double a Jacobian tuple p1
+
+ See https://en.wikibooks.org/wiki/Cryptography/Prime_Curve/Jacobian_Coordinates - Point Doubling
+ """
+ x1, y1, z1 = p1
+ if z1 == 0:
+ return (0, 1, 0)
+ y1_2 = (y1**2) % self.p
+ y1_4 = (y1_2**2) % self.p
+ x1_2 = (x1**2) % self.p
+ s = (4 * x1 * y1_2) % self.p
+ m = 3 * x1_2
+ if self.a:
+ m += self.a * pow(z1, 4, self.p)
+ m = m % self.p
+ x2 = (m**2 - 2 * s) % self.p
+ y2 = (m * (s - x2) - 8 * y1_4) % self.p
+ z2 = (2 * y1 * z1) % self.p
+ return (x2, y2, z2)
+
+ def add_mixed(self, p1, p2):
+ """Add a Jacobian tuple p1 and an affine tuple p2
+
+ See https://en.wikibooks.org/wiki/Cryptography/Prime_Curve/Jacobian_Coordinates - Point Addition (with affine point)
+ """
+ x1, y1, z1 = p1
+ x2, y2, z2 = p2
+ assert z2 == 1
+ # Adding to the point at infinity is a no-op
+ if z1 == 0:
+ return p2
+ z1_2 = (z1**2) % self.p
+ z1_3 = (z1_2 * z1) % self.p
+ u2 = (x2 * z1_2) % self.p
+ s2 = (y2 * z1_3) % self.p
+ if x1 == u2:
+ if y1 != s2:
+ # p1 and p2 are inverses. Return the point at infinity.
+ return (0, 1, 0)
+ # p1 == p2. The formulas below fail when the two points are equal.
+ return self.double(p1)
+ h = u2 - x1
+ r = s2 - y1
+ h_2 = (h**2) % self.p
+ h_3 = (h_2 * h) % self.p
+ u1_h_2 = (x1 * h_2) % self.p
+ x3 = (r**2 - h_3 - 2 * u1_h_2) % self.p
+ y3 = (r * (u1_h_2 - x3) - y1 * h_3) % self.p
+ z3 = (h * z1) % self.p
+ return (x3, y3, z3)
+
+ def add(self, p1, p2):
+ """Add two Jacobian tuples p1 and p2
+
+ See https://en.wikibooks.org/wiki/Cryptography/Prime_Curve/Jacobian_Coordinates - Point Addition
+ """
+ x1, y1, z1 = p1
+ x2, y2, z2 = p2
+ # Adding the point at infinity is a no-op
+ if z1 == 0:
+ return p2
+ if z2 == 0:
+ return p1
+ # Adding an Affine to a Jacobian is more efficient since we save field multiplications and squarings when z = 1
+ if z1 == 1:
+ return self.add_mixed(p2, p1)
+ if z2 == 1:
+ return self.add_mixed(p1, p2)
+ z1_2 = (z1**2) % self.p
+ z1_3 = (z1_2 * z1) % self.p
+ z2_2 = (z2**2) % self.p
+ z2_3 = (z2_2 * z2) % self.p
+ u1 = (x1 * z2_2) % self.p
+ u2 = (x2 * z1_2) % self.p
+ s1 = (y1 * z2_3) % self.p
+ s2 = (y2 * z1_3) % self.p
+ if u1 == u2:
+ if s1 != s2:
+ # p1 and p2 are inverses. Return the point at infinity.
+ return (0, 1, 0)
+ # p1 == p2. The formulas below fail when the two points are equal.
+ return self.double(p1)
+ h = u2 - u1
+ r = s2 - s1
+ h_2 = (h**2) % self.p
+ h_3 = (h_2 * h) % self.p
+ u1_h_2 = (u1 * h_2) % self.p
+ x3 = (r**2 - h_3 - 2 * u1_h_2) % self.p
+ y3 = (r * (u1_h_2 - x3) - s1 * h_3) % self.p
+ z3 = (h * z1 * z2) % self.p
+ return (x3, y3, z3)
+
+ def mul(self, ps):
+ """Compute a (multi) point multiplication
+
+ ps is a list of (Jacobian tuple, scalar) pairs.
+ """
+ r = (0, 1, 0)
+ for i in range(255, -1, -1):
+ r = self.double(r)
+ for p, n in ps:
+ if (n >> i) & 1:
+ r = self.add(r, p)
+ return r
+
+
+SECP256K1_FIELD_SIZE = 2**256 - 2**32 - 977
+SECP256K1 = EllipticCurve(SECP256K1_FIELD_SIZE, 0, 7)
+SECP256K1_G = (
+ 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798,
+ 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8,
+ 1,
+)
+SECP256K1_ORDER = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
+SECP256K1_ORDER_HALF = SECP256K1_ORDER // 2
+
+
+class ECPubKey:
+ """A secp256k1 public key"""
+
+ def __init__(self):
+ """Construct an uninitialized public key"""
+ self.valid = False
+
+ def set(self, data):
+ """Construct a public key from a serialization in compressed or uncompressed format"""
+ if len(data) == 65 and data[0] == 0x04:
+ p = (
+ int.from_bytes(data[1:33], "big"),
+ int.from_bytes(data[33:65], "big"),
+ 1,
+ )
+ self.valid = SECP256K1.on_curve(p)
+ if self.valid:
+ self.p = p
+ self.compressed = False
+ elif len(data) == 33 and (data[0] == 0x02 or data[0] == 0x03):
+ x = int.from_bytes(data[1:33], "big")
+ if SECP256K1.is_x_coord(x):
+ p = SECP256K1.lift_x(x)
+ # Make the Y coordinate odd if required (lift_x always produces
+ # a point with an even Y coordinate).
+ if data[0] & 1:
+ p = SECP256K1.negate(p)
+ self.p = p
+ self.valid = True
+ self.compressed = True
+ else:
+ self.valid = False
+ else:
+ self.valid = False
+
+ @property
+ def is_compressed(self):
+ return self.compressed
+
+ @property
+ def is_valid(self):
+ return self.valid
+
+ def get_bytes(self):
+ assert self.valid
+ p = SECP256K1.affine(self.p)
+ if p is None:
+ return None
+ if self.compressed:
+ return bytes([0x02 + (p[1] & 1)]) + p[0].to_bytes(32, "big")
+ else:
+ return bytes([0x04]) + p[0].to_bytes(32, "big") + p[1].to_bytes(32, "big")
+
+ def verify_ecdsa(self, sig, msg, low_s=True):
+ """Verify a strictly DER-encoded ECDSA signature against this pubkey.
+
+ See https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm for the
+ ECDSA verifier algorithm"""
+ assert self.valid
+
+ # Extract r and s from the DER formatted signature. Return false for
+ # any DER encoding errors.
+ if sig[1] + 2 != len(sig):
+ return False
+ if len(sig) < 4:
+ return False
+ if sig[0] != 0x30:
+ return False
+ if sig[2] != 0x02:
+ return False
+ rlen = sig[3]
+ if len(sig) < 6 + rlen:
+ return False
+ if rlen < 1 or rlen > 33:
+ return False
+ if sig[4] >= 0x80:
+ return False
+ if rlen > 1 and (sig[4] == 0) and not (sig[5] & 0x80):
+ return False
+ r = int.from_bytes(sig[4 : 4 + rlen], "big")
+ if sig[4 + rlen] != 0x02:
+ return False
+ slen = sig[5 + rlen]
+ if slen < 1 or slen > 33:
+ return False
+ if len(sig) != 6 + rlen + slen:
+ return False
+ if sig[6 + rlen] >= 0x80:
+ return False
+ if slen > 1 and (sig[6 + rlen] == 0) and not (sig[7 + rlen] & 0x80):
+ return False
+ s = int.from_bytes(sig[6 + rlen : 6 + rlen + slen], "big")
+
+ # Verify that r and s are within the group order
+ if r < 1 or s < 1 or r >= SECP256K1_ORDER or s >= SECP256K1_ORDER:
+ return False
+ if low_s and s >= SECP256K1_ORDER_HALF:
+ return False
+ z = int.from_bytes(msg, "big")
+
+ # Run verifier algorithm on r, s
+ w = modinv(s, SECP256K1_ORDER)
+ u1 = z * w % SECP256K1_ORDER
+ u2 = r * w % SECP256K1_ORDER
+ R = SECP256K1.affine(SECP256K1.mul([(SECP256K1_G, u1), (self.p, u2)]))
+ if R is None or (R[0] % SECP256K1_ORDER) != r:
+ return False
+ return True
+
+
+def generate_privkey():
+ """Generate a valid random 32-byte private key."""
+ return random.randrange(1, SECP256K1_ORDER).to_bytes(32, "big")
+
+
+class ECKey:
+ """A secp256k1 private key"""
+
+ def __init__(self):
+ self.valid = False
+
+ def set(self, secret, compressed):
+ """Construct a private key object with given 32-byte secret and compressed flag."""
+ assert len(secret) == 32
+ secret = int.from_bytes(secret, "big")
+ self.valid = secret > 0 and secret < SECP256K1_ORDER
+ if self.valid:
+ self.secret = secret
+ self.compressed = compressed
+
+ def generate(self, compressed=True):
+ """Generate a random private key (compressed or uncompressed)."""
+ self.set(generate_privkey(), compressed)
+
+ def get_bytes(self):
+ """Retrieve the 32-byte representation of this key."""
+ assert self.valid
+ return self.secret.to_bytes(32, "big")
+
+ @property
+ def is_valid(self):
+ return self.valid
+
+ @property
+ def is_compressed(self):
+ return self.compressed
+
+ def get_pubkey(self):
+ """Compute an ECPubKey object for this secret key."""
+ assert self.valid
+ ret = ECPubKey()
+ p = SECP256K1.mul([(SECP256K1_G, self.secret)])
+ ret.p = p
+ ret.valid = True
+ ret.compressed = self.compressed
+ return ret
+
+ def sign_ecdsa(self, msg, nonce_function=None, extra_data=None, low_s=True):
+ """Construct a DER-encoded ECDSA signature with this key.
+
+ See https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm for the
+ ECDSA signer algorithm."""
+ assert self.valid
+ z = int.from_bytes(msg, "big")
+ if nonce_function is None:
+ nonce_function = deterministic_k
+ k = nonce_function(self.secret, z, extra_data=extra_data)
+ R = SECP256K1.affine(SECP256K1.mul([(SECP256K1_G, k)]))
+ r = R[0] % SECP256K1_ORDER
+ s = (modinv(k, SECP256K1_ORDER) * (z + self.secret * r)) % SECP256K1_ORDER
+ if low_s and s > SECP256K1_ORDER_HALF:
+ s = SECP256K1_ORDER - s
+ # Represent in DER format. The byte representations of r and s have
+ # length rounded up (255 bits becomes 32 bytes and 256 bits becomes 33
+ # bytes).
+ rb = r.to_bytes((r.bit_length() + 8) // 8, "big")
+ sb = s.to_bytes((s.bit_length() + 8) // 8, "big")
+ return (
+ b"\x30"
+ + bytes([4 + len(rb) + len(sb), 2, len(rb)])
+ + rb
+ + bytes([2, len(sb)])
+ + sb
+ )
+
+
+def deterministic_k(secret, z, extra_data=None):
+ # RFC6979, optimized for secp256k1
+ k = b"\x00" * 32
+ v = b"\x01" * 32
+ if z > SECP256K1_ORDER:
+ z -= SECP256K1_ORDER
+ z_bytes = z.to_bytes(32, "big")
+ secret_bytes = secret.to_bytes(32, "big")
+ if extra_data is not None:
+ z_bytes += extra_data
+ k = hmac.new(k, v + b"\x00" + secret_bytes + z_bytes, "sha256").digest()
+ v = hmac.new(k, v, "sha256").digest()
+ k = hmac.new(k, v + b"\x01" + secret_bytes + z_bytes, "sha256").digest()
+ v = hmac.new(k, v, "sha256").digest()
+ while True:
+ v = hmac.new(k, v, "sha256").digest()
+ candidate = int.from_bytes(v, "big")
+ if candidate >= 1 and candidate < SECP256K1_ORDER:
+ return candidate
+ k = hmac.new(k, v + b"\x00", "sha256").digest()
+ v = hmac.new(k, v, "sha256").digest()
+
+
+def compute_xonly_pubkey(key):
+ """Compute an x-only (32 byte) public key from a (32 byte) private key.
+
+ This also returns whether the resulting public key was negated.
+ """
+
+ assert len(key) == 32
+ x = int.from_bytes(key, "big")
+ if x == 0 or x >= SECP256K1_ORDER:
+ return (None, None)
+ P = SECP256K1.affine(SECP256K1.mul([(SECP256K1_G, x)]))
+ return (P[0].to_bytes(32, "big"), not SECP256K1.has_even_y(P))
+
+
+def tweak_add_privkey(key, tweak):
+ """Tweak a private key (after negating it if needed)."""
+
+ assert len(key) == 32
+ assert len(tweak) == 32
+
+ x = int.from_bytes(key, "big")
+ if x == 0 or x >= SECP256K1_ORDER:
+ return None
+ if not SECP256K1.has_even_y(SECP256K1.mul([(SECP256K1_G, x)])):
+ x = SECP256K1_ORDER - x
+ t = int.from_bytes(tweak, "big")
+ if t >= SECP256K1_ORDER:
+ return None
+ x = (x + t) % SECP256K1_ORDER
+ if x == 0:
+ return None
+ return x.to_bytes(32, "big")
+
+
+def tweak_add_pubkey(key, tweak):
+ """Tweak a public key and return whether the result had to be negated."""
+
+ assert len(key) == 32
+ assert len(tweak) == 32
+
+ x_coord = int.from_bytes(key, "big")
+ if x_coord >= SECP256K1_FIELD_SIZE:
+ return None
+ P = SECP256K1.lift_x(x_coord)
+ if P is None:
+ return None
+ t = int.from_bytes(tweak, "big")
+ if t >= SECP256K1_ORDER:
+ return None
+ Q = SECP256K1.affine(SECP256K1.mul([(SECP256K1_G, t), (P, 1)]))
+ if Q is None:
+ return None
+ return (Q[0].to_bytes(32, "big"), not SECP256K1.has_even_y(Q))
+
+
+def verify_schnorr(key, sig, msg):
+ """Verify a Schnorr signature (see BIP 340).
+ - key is a 32-byte xonly pubkey (computed using compute_xonly_pubkey).
+ - sig is a 64-byte Schnorr signature
+ - msg is a 32-byte message
+ """
+ assert len(key) == 32
+ assert len(msg) == 32
+ assert len(sig) == 64
+
+ x_coord = int.from_bytes(key, "big")
+ if x_coord == 0 or x_coord >= SECP256K1_FIELD_SIZE:
+ return False
+ P = SECP256K1.lift_x(x_coord)
+ if P is None:
+ return False
+ r = int.from_bytes(sig[0:32], "big")
+ if r >= SECP256K1_FIELD_SIZE:
+ return False
+ s = int.from_bytes(sig[32:64], "big")
+ if s >= SECP256K1_ORDER:
+ return False
+ e = (
+ int.from_bytes(TaggedHash("BIP0340/challenge", sig[0:32] + key + msg), "big")
+ % SECP256K1_ORDER
+ )
+ R = SECP256K1.mul([(SECP256K1_G, s), (P, SECP256K1_ORDER - e)])
+ if not SECP256K1.has_even_y(R):
+ return False
+ if ((r * R[2] * R[2]) % SECP256K1_FIELD_SIZE) != R[0]:
+ return False
+ return True
+
+
+def sign_schnorr(key, msg, aux=None, flip_p=False, flip_r=False):
+ """Create a Schnorr signature (see BIP 340)."""
+
+ assert len(key) == 32
+ assert len(msg) == 32
+ if aux is not None:
+ assert len(aux) == 32
+
+ sec = int.from_bytes(key, "big")
+ if sec == 0 or sec >= SECP256K1_ORDER:
+ return None
+ P = SECP256K1.affine(SECP256K1.mul([(SECP256K1_G, sec)]))
+ if SECP256K1.has_even_y(P) == flip_p:
+ sec = SECP256K1_ORDER - sec
+ if aux is not None:
+ t = (sec ^ int.from_bytes(TaggedHash("BIP0340/aux", aux), "big")).to_bytes(
+ 32, "big"
+ )
+ else:
+ t = sec.to_bytes(32, "big")
+ kp = (
+ int.from_bytes(
+ TaggedHash("BIP0340/nonce", t + P[0].to_bytes(32, "big") + msg), "big"
+ )
+ % SECP256K1_ORDER
+ )
+ assert kp != 0
+ R = SECP256K1.affine(SECP256K1.mul([(SECP256K1_G, kp)]))
+ k = kp if SECP256K1.has_even_y(R) != flip_r else SECP256K1_ORDER - kp
+ e = (
+ int.from_bytes(
+ TaggedHash(
+ "BIP0340/challenge",
+ R[0].to_bytes(32, "big") + P[0].to_bytes(32, "big") + msg,
+ ),
+ "big",
+ )
+ % SECP256K1_ORDER
+ )
+ return R[0].to_bytes(32, "big") + ((k + e * sec) % SECP256K1_ORDER).to_bytes(
+ 32, "big"
+ )
diff --git a/bitcoin_client/ledger_bitcoin/embit/util/py_ripemd160.py b/bitcoin_client/ledger_bitcoin/embit/util/py_ripemd160.py
new file mode 100644
index 000000000..7eeaa56ca
--- /dev/null
+++ b/bitcoin_client/ledger_bitcoin/embit/util/py_ripemd160.py
@@ -0,0 +1,407 @@
+# Copyright (c) 2021 Pieter Wuille
+# Distributed under the MIT software license, see the accompanying
+# file COPYING or http://www.opensource.org/licenses/mit-license.php.
+"""Pure Python RIPEMD160 implementation."""
+
+# Message schedule indexes for the left path.
+ML = [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9,
+ 10,
+ 11,
+ 12,
+ 13,
+ 14,
+ 15,
+ 7,
+ 4,
+ 13,
+ 1,
+ 10,
+ 6,
+ 15,
+ 3,
+ 12,
+ 0,
+ 9,
+ 5,
+ 2,
+ 14,
+ 11,
+ 8,
+ 3,
+ 10,
+ 14,
+ 4,
+ 9,
+ 15,
+ 8,
+ 1,
+ 2,
+ 7,
+ 0,
+ 6,
+ 13,
+ 11,
+ 5,
+ 12,
+ 1,
+ 9,
+ 11,
+ 10,
+ 0,
+ 8,
+ 12,
+ 4,
+ 13,
+ 3,
+ 7,
+ 15,
+ 14,
+ 5,
+ 6,
+ 2,
+ 4,
+ 0,
+ 5,
+ 9,
+ 7,
+ 12,
+ 2,
+ 10,
+ 14,
+ 1,
+ 3,
+ 8,
+ 11,
+ 6,
+ 15,
+ 13,
+]
+
+# Message schedule indexes for the right path.
+MR = [
+ 5,
+ 14,
+ 7,
+ 0,
+ 9,
+ 2,
+ 11,
+ 4,
+ 13,
+ 6,
+ 15,
+ 8,
+ 1,
+ 10,
+ 3,
+ 12,
+ 6,
+ 11,
+ 3,
+ 7,
+ 0,
+ 13,
+ 5,
+ 10,
+ 14,
+ 15,
+ 8,
+ 12,
+ 4,
+ 9,
+ 1,
+ 2,
+ 15,
+ 5,
+ 1,
+ 3,
+ 7,
+ 14,
+ 6,
+ 9,
+ 11,
+ 8,
+ 12,
+ 2,
+ 10,
+ 0,
+ 4,
+ 13,
+ 8,
+ 6,
+ 4,
+ 1,
+ 3,
+ 11,
+ 15,
+ 0,
+ 5,
+ 12,
+ 2,
+ 13,
+ 9,
+ 7,
+ 10,
+ 14,
+ 12,
+ 15,
+ 10,
+ 4,
+ 1,
+ 5,
+ 8,
+ 7,
+ 6,
+ 2,
+ 13,
+ 14,
+ 0,
+ 3,
+ 9,
+ 11,
+]
+
+# Rotation counts for the left path.
+RL = [
+ 11,
+ 14,
+ 15,
+ 12,
+ 5,
+ 8,
+ 7,
+ 9,
+ 11,
+ 13,
+ 14,
+ 15,
+ 6,
+ 7,
+ 9,
+ 8,
+ 7,
+ 6,
+ 8,
+ 13,
+ 11,
+ 9,
+ 7,
+ 15,
+ 7,
+ 12,
+ 15,
+ 9,
+ 11,
+ 7,
+ 13,
+ 12,
+ 11,
+ 13,
+ 6,
+ 7,
+ 14,
+ 9,
+ 13,
+ 15,
+ 14,
+ 8,
+ 13,
+ 6,
+ 5,
+ 12,
+ 7,
+ 5,
+ 11,
+ 12,
+ 14,
+ 15,
+ 14,
+ 15,
+ 9,
+ 8,
+ 9,
+ 14,
+ 5,
+ 6,
+ 8,
+ 6,
+ 5,
+ 12,
+ 9,
+ 15,
+ 5,
+ 11,
+ 6,
+ 8,
+ 13,
+ 12,
+ 5,
+ 12,
+ 13,
+ 14,
+ 11,
+ 8,
+ 5,
+ 6,
+]
+
+# Rotation counts for the right path.
+RR = [
+ 8,
+ 9,
+ 9,
+ 11,
+ 13,
+ 15,
+ 15,
+ 5,
+ 7,
+ 7,
+ 8,
+ 11,
+ 14,
+ 14,
+ 12,
+ 6,
+ 9,
+ 13,
+ 15,
+ 7,
+ 12,
+ 8,
+ 9,
+ 11,
+ 7,
+ 7,
+ 12,
+ 7,
+ 6,
+ 15,
+ 13,
+ 11,
+ 9,
+ 7,
+ 15,
+ 11,
+ 8,
+ 6,
+ 6,
+ 14,
+ 12,
+ 13,
+ 5,
+ 14,
+ 13,
+ 13,
+ 7,
+ 5,
+ 15,
+ 5,
+ 8,
+ 11,
+ 14,
+ 14,
+ 6,
+ 14,
+ 6,
+ 9,
+ 12,
+ 9,
+ 12,
+ 5,
+ 15,
+ 8,
+ 8,
+ 5,
+ 12,
+ 9,
+ 12,
+ 5,
+ 14,
+ 6,
+ 8,
+ 13,
+ 6,
+ 5,
+ 15,
+ 13,
+ 11,
+ 11,
+]
+
+# K constants for the left path.
+KL = [0, 0x5A827999, 0x6ED9EBA1, 0x8F1BBCDC, 0xA953FD4E]
+
+# K constants for the right path.
+KR = [0x50A28BE6, 0x5C4DD124, 0x6D703EF3, 0x7A6D76E9, 0]
+
+
+def fi(x, y, z, i):
+ """The f1, f2, f3, f4, and f5 functions from the specification."""
+ if i == 0:
+ return x ^ y ^ z
+ elif i == 1:
+ return (x & y) | (~x & z)
+ elif i == 2:
+ return (x | ~y) ^ z
+ elif i == 3:
+ return (x & z) | (y & ~z)
+ elif i == 4:
+ return x ^ (y | ~z)
+ else:
+ assert False
+
+
+def rol(x, i):
+ """Rotate the bottom 32 bits of x left by i bits."""
+ return ((x << i) | ((x & 0xFFFFFFFF) >> (32 - i))) & 0xFFFFFFFF
+
+
+def compress(h0, h1, h2, h3, h4, block):
+ """Compress state (h0, h1, h2, h3, h4) with block."""
+ # Left path variables.
+ al, bl, cl, dl, el = h0, h1, h2, h3, h4
+ # Right path variables.
+ ar, br, cr, dr, er = h0, h1, h2, h3, h4
+ # Message variables.
+ x = [int.from_bytes(block[4 * i : 4 * (i + 1)], "little") for i in range(16)]
+
+ # Iterate over the 80 rounds of the compression.
+ for j in range(80):
+ rnd = j >> 4
+ # Perform left side of the transformation.
+ al = rol(al + fi(bl, cl, dl, rnd) + x[ML[j]] + KL[rnd], RL[j]) + el
+ al, bl, cl, dl, el = el, al, bl, rol(cl, 10), dl
+ # Perform right side of the transformation.
+ ar = rol(ar + fi(br, cr, dr, 4 - rnd) + x[MR[j]] + KR[rnd], RR[j]) + er
+ ar, br, cr, dr, er = er, ar, br, rol(cr, 10), dr
+
+ # Compose old state, left transform, and right transform into new state.
+ return h1 + cl + dr, h2 + dl + er, h3 + el + ar, h4 + al + br, h0 + bl + cr
+
+
+def ripemd160(data):
+ """Compute the RIPEMD-160 hash of data."""
+ # Initialize state.
+ state = (0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0)
+ # Process full 64-byte blocks in the input.
+ for b in range(len(data) >> 6):
+ state = compress(*state, data[64 * b : 64 * (b + 1)])
+ # Construct final blocks (with padding and size).
+ pad = b"\x80" + b"\x00" * ((119 - len(data)) & 63)
+ fin = data[len(data) & ~63 :] + pad + (8 * len(data)).to_bytes(8, "little")
+ # Process final blocks.
+ for b in range(len(fin) >> 6):
+ state = compress(*state, fin[64 * b : 64 * (b + 1)])
+ # Produce output.
+ return b"".join((h & 0xFFFFFFFF).to_bytes(4, "little") for h in state)
diff --git a/bitcoin_client/ledger_bitcoin/embit/util/py_secp256k1.py b/bitcoin_client/ledger_bitcoin/embit/util/py_secp256k1.py
new file mode 100644
index 000000000..851408635
--- /dev/null
+++ b/bitcoin_client/ledger_bitcoin/embit/util/py_secp256k1.py
@@ -0,0 +1,384 @@
+"""
+This is a fallback option if the library can't do ctypes bindings to secp256k1 library.
+Mimics the micropython bindings and internal representation of data structs in secp256k1.
+"""
+
+from . import key as _key
+
+# Flags to pass to context_create.
+CONTEXT_VERIFY = 0b0100000001
+CONTEXT_SIGN = 0b1000000001
+CONTEXT_NONE = 0b0000000001
+
+# Flags to pass to ec_pubkey_serialize
+EC_COMPRESSED = 0b0100000010
+EC_UNCOMPRESSED = 0b0000000010
+
+
+def context_randomize(seed, context=None):
+ pass
+
+
+def _reverse64(b):
+ """Converts (a,b) from big to little endian to be consistent with secp256k1"""
+ x = b[:32]
+ y = b[32:]
+ return x[::-1] + y[::-1]
+
+
+def _pubkey_serialize(pub):
+ """Returns pubkey representation like secp library"""
+ b = pub.get_bytes()[1:]
+ return _reverse64(b)
+
+
+def _pubkey_parse(b):
+ """Returns pubkey class instance"""
+ pub = _key.ECPubKey()
+ pub.set(b"\x04" + _reverse64(b))
+ return pub
+
+
+def ec_pubkey_create(secret, context=None):
+ if len(secret) != 32:
+ raise ValueError("Private key should be 32 bytes long")
+ pk = _key.ECKey()
+ pk.set(secret, compressed=False)
+ if not pk.is_valid:
+ raise ValueError("Invalid private key")
+ return _pubkey_serialize(pk.get_pubkey())
+
+
+def ec_pubkey_parse(sec, context=None):
+ if len(sec) != 33 and len(sec) != 65:
+ raise ValueError("Serialized pubkey should be 33 or 65 bytes long")
+ if len(sec) == 33:
+ if sec[0] != 0x02 and sec[0] != 0x03:
+ raise ValueError("Compressed pubkey should start with 0x02 or 0x03")
+ else:
+ if sec[0] != 0x04:
+ raise ValueError("Uncompressed pubkey should start with 0x04")
+ pub = _key.ECPubKey()
+ pub.set(sec)
+ pub.compressed = False
+ if not pub.is_valid:
+ raise ValueError("Failed parsing public key")
+ return _pubkey_serialize(pub)
+
+
+def ec_pubkey_serialize(pubkey, flag=EC_COMPRESSED, context=None):
+ if len(pubkey) != 64:
+ raise ValueError("Pubkey should be 64 bytes long")
+ if flag not in [EC_COMPRESSED, EC_UNCOMPRESSED]:
+ raise ValueError("Invalid flag")
+ pub = _pubkey_parse(pubkey)
+ if not pub.is_valid:
+ raise ValueError("Failed to serialize pubkey")
+ if flag == EC_COMPRESSED:
+ pub.compressed = True
+ return pub.get_bytes()
+
+
+def ecdsa_signature_parse_compact(compact_sig, context=None):
+ if len(compact_sig) != 64:
+ raise ValueError("Compact signature should be 64 bytes long")
+ sig = _reverse64(compact_sig)
+ return sig
+
+
+def ecdsa_signature_parse_der(der, context=None):
+ if der[1] + 2 != len(der):
+ raise ValueError("Failed parsing compact signature")
+ if len(der) < 4:
+ raise ValueError("Failed parsing compact signature")
+ if der[0] != 0x30:
+ raise ValueError("Failed parsing compact signature")
+ if der[2] != 0x02:
+ raise ValueError("Failed parsing compact signature")
+ rlen = der[3]
+ if len(der) < 6 + rlen:
+ raise ValueError("Failed parsing compact signature")
+ if rlen < 1 or rlen > 33:
+ raise ValueError("Failed parsing compact signature")
+ if der[4] >= 0x80:
+ raise ValueError("Failed parsing compact signature")
+ if rlen > 1 and (der[4] == 0) and not (der[5] & 0x80):
+ raise ValueError("Failed parsing compact signature")
+ r = int.from_bytes(der[4 : 4 + rlen], "big")
+ if der[4 + rlen] != 0x02:
+ raise ValueError("Failed parsing compact signature")
+ slen = der[5 + rlen]
+ if slen < 1 or slen > 33:
+ raise ValueError("Failed parsing compact signature")
+ if len(der) != 6 + rlen + slen:
+ raise ValueError("Failed parsing compact signature")
+ if der[6 + rlen] >= 0x80:
+ raise ValueError("Failed parsing compact signature")
+ if slen > 1 and (der[6 + rlen] == 0) and not (der[7 + rlen] & 0x80):
+ raise ValueError("Failed parsing compact signature")
+ s = int.from_bytes(der[6 + rlen : 6 + rlen + slen], "big")
+
+ # Verify that r and s are within the group order
+ if r < 1 or s < 1 or r >= _key.SECP256K1_ORDER or s >= _key.SECP256K1_ORDER:
+ raise ValueError("Failed parsing compact signature")
+ if s >= _key.SECP256K1_ORDER_HALF:
+ raise ValueError("Failed parsing compact signature")
+
+ return r.to_bytes(32, "little") + s.to_bytes(32, "little")
+
+
+def ecdsa_signature_serialize_der(sig, context=None):
+ if len(sig) != 64:
+ raise ValueError("Signature should be 64 bytes long")
+ r = int.from_bytes(sig[:32], "little")
+ s = int.from_bytes(sig[32:], "little")
+ rb = r.to_bytes((r.bit_length() + 8) // 8, "big")
+ sb = s.to_bytes((s.bit_length() + 8) // 8, "big")
+ return (
+ b"\x30"
+ + bytes([4 + len(rb) + len(sb), 2, len(rb)])
+ + rb
+ + bytes([2, len(sb)])
+ + sb
+ )
+
+
+def ecdsa_signature_serialize_compact(sig, context=None):
+ if len(sig) != 64:
+ raise ValueError("Signature should be 64 bytes long")
+ return _reverse64(sig)
+
+
+def ecdsa_signature_normalize(sig, context=None):
+ if len(sig) != 64:
+ raise ValueError("Signature should be 64 bytes long")
+ r = int.from_bytes(sig[:32], "little")
+ s = int.from_bytes(sig[32:], "little")
+ if s >= _key.SECP256K1_ORDER_HALF:
+ s = _key.SECP256K1_ORDER - s
+ return r.to_bytes(32, "little") + s.to_bytes(32, "little")
+
+
+def ecdsa_verify(sig, msg, pub, context=None):
+ if len(sig) != 64:
+ raise ValueError("Signature should be 64 bytes long")
+ if len(msg) != 32:
+ raise ValueError("Message should be 32 bytes long")
+ if len(pub) != 64:
+ raise ValueError("Public key should be 64 bytes long")
+ pubkey = _pubkey_parse(pub)
+ return pubkey.verify_ecdsa(ecdsa_signature_serialize_der(sig), msg)
+
+
+def ecdsa_sign(msg, secret, nonce_function=None, extra_data=None, context=None):
+ if len(msg) != 32:
+ raise ValueError("Message should be 32 bytes long")
+ if len(secret) != 32:
+ raise ValueError("Secret key should be 32 bytes long")
+ pk = _key.ECKey()
+ pk.set(secret, False)
+ sig = pk.sign_ecdsa(msg, nonce_function, extra_data)
+ return ecdsa_signature_parse_der(sig)
+
+
+def ec_seckey_verify(secret, context=None):
+ if len(secret) != 32:
+ raise ValueError("Secret should be 32 bytes long")
+ pk = _key.ECKey()
+ pk.set(secret, compressed=False)
+ return pk.is_valid
+
+
+def ec_privkey_negate(secret, context=None):
+ # negate in place
+ if len(secret) != 32:
+ raise ValueError("Secret should be 32 bytes long")
+ s = int.from_bytes(secret, "big")
+ s2 = _key.SECP256K1_ORDER - s
+ return s2.to_bytes(32, "big")
+
+
+def ec_pubkey_negate(pubkey, context=None):
+ if len(pubkey) != 64:
+ raise ValueError("Pubkey should be a 64-byte structure")
+ sec = ec_pubkey_serialize(pubkey)
+ return ec_pubkey_parse(bytes([0x05 - sec[0]]) + sec[1:])
+
+
+def ec_privkey_tweak_add(secret, tweak, context=None):
+ res = ec_privkey_add(secret, tweak)
+ for i in range(len(secret)):
+ secret[i] = res[i]
+
+
+def ec_pubkey_tweak_add(pub, tweak, context=None):
+ res = ec_pubkey_add(pub, tweak)
+ for i in range(len(pub)):
+ pub[i] = res[i]
+
+
+def ec_privkey_add(secret, tweak, context=None):
+ if len(secret) != 32 or len(tweak) != 32:
+ raise ValueError("Secret and tweak should both be 32 bytes long")
+ s = int.from_bytes(secret, "big")
+ t = int.from_bytes(tweak, "big")
+ r = (s + t) % _key.SECP256K1_ORDER
+ return r.to_bytes(32, "big")
+
+
+def ec_pubkey_add(pub, tweak, context=None):
+ if len(pub) != 64:
+ raise ValueError("Public key should be 64 bytes long")
+ if len(tweak) != 32:
+ raise ValueError("Tweak should be 32 bytes long")
+ pubkey = _pubkey_parse(pub)
+ pubkey.compressed = True
+ t = int.from_bytes(tweak, "big")
+ Q = _key.SECP256K1.affine(
+ _key.SECP256K1.mul([(_key.SECP256K1_G, t), (pubkey.p, 1)])
+ )
+ if Q is None:
+ return None
+ return Q[0].to_bytes(32, "little") + Q[1].to_bytes(32, "little")
+
+
+# def ec_privkey_tweak_mul(secret, tweak, context=None):
+# if len(secret)!=32 or len(tweak)!=32:
+# raise ValueError("Secret and tweak should both be 32 bytes long")
+# s = int.from_bytes(secret, 'big')
+# t = int.from_bytes(tweak, 'big')
+# if t > _key.SECP256K1_ORDER or s > _key.SECP256K1_ORDER:
+# raise ValueError("Failed to tweak the secret")
+# r = pow(s, t, _key.SECP256K1_ORDER)
+# res = r.to_bytes(32, 'big')
+# for i in range(len(secret)):
+# secret[i] = res[i]
+
+# def ec_pubkey_tweak_mul(pub, tweak, context=None):
+# if len(pub)!=64:
+# raise ValueError("Public key should be 64 bytes long")
+# if len(tweak)!=32:
+# raise ValueError("Tweak should be 32 bytes long")
+# if _secp.secp256k1_ec_pubkey_tweak_mul(context, pub, tweak) == 0:
+# raise ValueError("Failed to tweak the public key")
+
+# def ec_pubkey_combine(*args, context=None):
+# pub = bytes(64)
+# pubkeys = (c_char_p * len(args))(*args)
+# r = _secp.secp256k1_ec_pubkey_combine(context, pub, pubkeys, len(args))
+# if r == 0:
+# raise ValueError("Failed to negate pubkey")
+# return pub
+
+# schnorrsig
+
+
+def xonly_pubkey_from_pubkey(pubkey, context=None):
+ if len(pubkey) != 64:
+ raise ValueError("Pubkey should be 64 bytes long")
+ sec = ec_pubkey_serialize(pubkey)
+ parity = sec[0] == 0x03
+ pub = ec_pubkey_parse(b"\x02" + sec[1:33])
+ return pub, parity
+
+
+def schnorrsig_verify(sig, msg, pubkey, context=None):
+ assert len(sig) == 64
+ assert len(msg) == 32
+ assert len(pubkey) == 64
+ sec = ec_pubkey_serialize(pubkey)
+ return _key.verify_schnorr(sec[1:33], sig, msg)
+
+
+def keypair_create(secret, context=None):
+ pub = ec_pubkey_create(secret)
+ pub2, parity = xonly_pubkey_from_pubkey(pub)
+ keypair = secret + pub
+ return keypair
+
+
+def schnorrsig_sign(msg, keypair, nonce_function=None, extra_data=None, context=None):
+ assert len(msg) == 32
+ if len(keypair) == 32:
+ keypair = keypair_create(keypair, context=context)
+ assert len(keypair) == 96
+ return _key.sign_schnorr(keypair[:32], msg, extra_data)
+
+
+# recoverable
+
+
+def ecdsa_sign_recoverable(msg, secret, context=None):
+ sig = ecdsa_sign(msg, secret)
+ pub = ec_pubkey_create(secret)
+ # Search for correct index. Not efficient but I am lazy.
+ # For efficiency use c-bindings to libsecp256k1
+ for i in range(4):
+ if ecdsa_recover(sig + bytes([i]), msg) == pub:
+ return sig + bytes([i])
+ raise ValueError("Failed to sign")
+
+
+def ecdsa_recoverable_signature_serialize_compact(sig, context=None):
+ if len(sig) != 65:
+ raise ValueError("Recoverable signature should be 65 bytes long")
+ compact = ecdsa_signature_serialize_compact(sig[:64])
+ return compact, sig[64]
+
+
+def ecdsa_recoverable_signature_parse_compact(compact_sig, recid, context=None):
+ if len(compact_sig) != 64:
+ raise ValueError("Signature should be 64 bytes long")
+ # TODO: also check r value so recid > 2 makes sense
+ if recid < 0 or recid > 4:
+ raise ValueError("Failed parsing compact signature")
+ return ecdsa_signature_parse_compact(compact_sig) + bytes([recid])
+
+
+def ecdsa_recoverable_signature_convert(sigin, context=None):
+ if len(sigin) != 65:
+ raise ValueError("Recoverable signature should be 65 bytes long")
+ return sigin[:64]
+
+
+def ecdsa_recover(sig, msghash, context=None):
+ if len(sig) != 65:
+ raise ValueError("Recoverable signature should be 65 bytes long")
+ if len(msghash) != 32:
+ raise ValueError("Message should be 32 bytes long")
+ idx = sig[-1]
+ r = int.from_bytes(sig[:32], "little")
+ s = int.from_bytes(sig[32:64], "little")
+ z = int.from_bytes(msghash, "big")
+ # r = Rx mod N, so R can be 02x, 03x, 02(N+x), 03(N+x)
+ # two latter cases only if N+x < P
+ r_candidates = [
+ b"\x02" + r.to_bytes(32, "big"),
+ b"\x03" + r.to_bytes(32, "big"),
+ ]
+ if r + _key.SECP256K1_ORDER < _key.SECP256K1_FIELD_SIZE:
+ r2 = r + _key.SECP256K1_ORDER
+ r_candidates = r_candidates + [
+ b"\x02" + r2.to_bytes(32, "big"),
+ b"\x03" + r2.to_bytes(32, "big"),
+ ]
+ if idx >= len(r_candidates):
+ raise ValueError("Failed to recover public key")
+ R = _key.ECPubKey()
+ R.set(r_candidates[idx])
+ # s = (z + d * r)/k
+ # (R*s/r - z/r*G) = P
+ rinv = _key.modinv(r, _key.SECP256K1_ORDER)
+ u1 = (s * rinv) % _key.SECP256K1_ORDER
+ u2 = (z * rinv) % _key.SECP256K1_ORDER
+ P1 = _key.SECP256K1.mul([(R.p, u1)])
+ P2 = _key.SECP256K1.negate(_key.SECP256K1.mul([(_key.SECP256K1_G, u2)]))
+ P = _key.SECP256K1.affine(_key.SECP256K1.add(P1, P2))
+ result = P[0].to_bytes(32, "little") + P[1].to_bytes(32, "little")
+ # verify signature at the end
+ pubkey = _pubkey_parse(result)
+ if not pubkey.is_valid:
+ raise ValueError("Failed to recover public key")
+ if not ecdsa_verify(sig[:64], msghash, result):
+ raise ValueError("Failed to recover public key")
+ return result
diff --git a/bitcoin_client/ledger_bitcoin/embit/util/secp256k1.py b/bitcoin_client/ledger_bitcoin/embit/util/secp256k1.py
new file mode 100644
index 000000000..a3ed8d9a7
--- /dev/null
+++ b/bitcoin_client/ledger_bitcoin/embit/util/secp256k1.py
@@ -0,0 +1,12 @@
+try:
+ # if it's micropython
+ from micropython import const
+ from secp256k1 import *
+except:
+ # we are in python
+ try:
+ # try ctypes bindings
+ from .ctypes_secp256k1 import *
+ except:
+ # fallback to python version
+ from .py_secp256k1 import *
diff --git a/bitcoin_client/ledger_bitcoin/exception/device_exception.py b/bitcoin_client/ledger_bitcoin/exception/device_exception.py
index b20a57cc7..7596420ad 100644
--- a/bitcoin_client/ledger_bitcoin/exception/device_exception.py
+++ b/bitcoin_client/ledger_bitcoin/exception/device_exception.py
@@ -6,12 +6,14 @@
class DeviceException(Exception): # pylint: disable=too-few-public-methods
exc: Dict[int, Any] = {
+ 0x5515: SecurityStatusNotSatisfiedError, # returned by sdk in recent versions
0x6985: DenyError,
- 0x6982: SecurityStatusNotSatisfiedError,
+ 0x6982: SecurityStatusNotSatisfiedError, # used in older app versions
0x6A80: IncorrectDataError,
0x6A82: NotSupportedError,
0x6A86: WrongP1P2Error,
0x6A87: WrongDataLengthError,
+ 0x6B00: SwapError,
0x6D00: InsNotSupportedError,
0x6E00: ClaNotSupportedError,
0xB000: WrongResponseLengthError,
diff --git a/bitcoin_client/ledger_bitcoin/exception/errors.py b/bitcoin_client/ledger_bitcoin/exception/errors.py
index e875d08bb..d2ec53aa3 100644
--- a/bitcoin_client/ledger_bitcoin/exception/errors.py
+++ b/bitcoin_client/ledger_bitcoin/exception/errors.py
@@ -26,6 +26,10 @@ class WrongDataLengthError(Exception):
pass
+class SwapError(Exception):
+ pass
+
+
class InsNotSupportedError(Exception):
pass
diff --git a/bitcoin_client/ledger_bitcoin/psbt.py b/bitcoin_client/ledger_bitcoin/psbt.py
index abc40128f..57dac5869 100644
--- a/bitcoin_client/ledger_bitcoin/psbt.py
+++ b/bitcoin_client/ledger_bitcoin/psbt.py
@@ -19,6 +19,7 @@
Sequence,
Set,
Tuple,
+ Union,
)
from .key import KeyOriginInfo
@@ -1181,7 +1182,6 @@ def convert_to_v0(self) -> None:
self.tx = self.get_unsigned_tx()
self.explicit_version = False
-
def _deserialize_proprietary_record(self, f: Readable, key: bytes) -> bool:
"""
:meta private:
@@ -1204,3 +1204,20 @@ def _serialize_proprietary_records(self) -> bytes:
:returns: The serialized records or an empty byte string if there are none.
"""
return b""
+
+
+def normalize_psbt(psbt: Union[PSBT, bytes, str]) -> PSBT:
+ """
+ Deserializes a psbt given as an argument from a string or a byte array, if necessary.
+
+ :param psbt: Either an instance of PSBT, or binary-encoded psbt as `bytes`, or a base64-encoded psbt as a `str`.
+ :returns: the deserialized PSBT object. If `psbt` was already a `PSBT`, it is returned directly (without cloning).
+ """
+ if isinstance(psbt, bytes):
+ psbt = base64.b64encode(psbt).decode()
+
+ if isinstance(psbt, str):
+ psbt_obj = PSBT()
+ psbt_obj.deserialize(psbt)
+ psbt = psbt_obj
+ return psbt
diff --git a/tests-legacy/bitcoin_client/hwi/__init__.py b/bitcoin_client/ledger_bitcoin/py.typed
similarity index 100%
rename from tests-legacy/bitcoin_client/hwi/__init__.py
rename to bitcoin_client/ledger_bitcoin/py.typed
diff --git a/bitcoin_client/ledger_bitcoin/ripemd.py b/bitcoin_client/ledger_bitcoin/ripemd.py
new file mode 100644
index 000000000..ee08cc387
--- /dev/null
+++ b/bitcoin_client/ledger_bitcoin/ripemd.py
@@ -0,0 +1,115 @@
+# Copyright (c) 2021 Pieter Wuille
+# Distributed under the MIT software license, see the accompanying
+# file COPYING or http://www.opensource.org/licenses/mit-license.php.
+#
+# Taken from https://github.com/bitcoin/bitcoin/blob/124e75a41ea0f3f0e90b63b0c41813184ddce2ab/test/functional/test_framework/ripemd160.py
+
+"""
+Pure Python RIPEMD160 implementation.
+
+WARNING: This implementation is NOT constant-time.
+Do not use without understanding the implications.
+"""
+
+# Message schedule indexes for the left path.
+ML = [
+ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
+ 7, 4, 13, 1, 10, 6, 15, 3, 12, 0, 9, 5, 2, 14, 11, 8,
+ 3, 10, 14, 4, 9, 15, 8, 1, 2, 7, 0, 6, 13, 11, 5, 12,
+ 1, 9, 11, 10, 0, 8, 12, 4, 13, 3, 7, 15, 14, 5, 6, 2,
+ 4, 0, 5, 9, 7, 12, 2, 10, 14, 1, 3, 8, 11, 6, 15, 13
+]
+
+# Message schedule indexes for the right path.
+MR = [
+ 5, 14, 7, 0, 9, 2, 11, 4, 13, 6, 15, 8, 1, 10, 3, 12,
+ 6, 11, 3, 7, 0, 13, 5, 10, 14, 15, 8, 12, 4, 9, 1, 2,
+ 15, 5, 1, 3, 7, 14, 6, 9, 11, 8, 12, 2, 10, 0, 4, 13,
+ 8, 6, 4, 1, 3, 11, 15, 0, 5, 12, 2, 13, 9, 7, 10, 14,
+ 12, 15, 10, 4, 1, 5, 8, 7, 6, 2, 13, 14, 0, 3, 9, 11
+]
+
+# Rotation counts for the left path.
+RL = [
+ 11, 14, 15, 12, 5, 8, 7, 9, 11, 13, 14, 15, 6, 7, 9, 8,
+ 7, 6, 8, 13, 11, 9, 7, 15, 7, 12, 15, 9, 11, 7, 13, 12,
+ 11, 13, 6, 7, 14, 9, 13, 15, 14, 8, 13, 6, 5, 12, 7, 5,
+ 11, 12, 14, 15, 14, 15, 9, 8, 9, 14, 5, 6, 8, 6, 5, 12,
+ 9, 15, 5, 11, 6, 8, 13, 12, 5, 12, 13, 14, 11, 8, 5, 6
+]
+
+# Rotation counts for the right path.
+RR = [
+ 8, 9, 9, 11, 13, 15, 15, 5, 7, 7, 8, 11, 14, 14, 12, 6,
+ 9, 13, 15, 7, 12, 8, 9, 11, 7, 7, 12, 7, 6, 15, 13, 11,
+ 9, 7, 15, 11, 8, 6, 6, 14, 12, 13, 5, 14, 13, 13, 7, 5,
+ 15, 5, 8, 11, 14, 14, 6, 14, 6, 9, 12, 9, 12, 5, 15, 8,
+ 8, 5, 12, 9, 12, 5, 14, 6, 8, 13, 6, 5, 15, 13, 11, 11
+]
+
+# K constants for the left path.
+KL = [0, 0x5a827999, 0x6ed9eba1, 0x8f1bbcdc, 0xa953fd4e]
+
+# K constants for the right path.
+KR = [0x50a28be6, 0x5c4dd124, 0x6d703ef3, 0x7a6d76e9, 0]
+
+
+def fi(x, y, z, i):
+ """The f1, f2, f3, f4, and f5 functions from the specification."""
+ if i == 0:
+ return x ^ y ^ z
+ elif i == 1:
+ return (x & y) | (~x & z)
+ elif i == 2:
+ return (x | ~y) ^ z
+ elif i == 3:
+ return (x & z) | (y & ~z)
+ elif i == 4:
+ return x ^ (y | ~z)
+ else:
+ assert False
+
+
+def rol(x, i):
+ """Rotate the bottom 32 bits of x left by i bits."""
+ return ((x << i) | ((x & 0xffffffff) >> (32 - i))) & 0xffffffff
+
+
+def compress(h0, h1, h2, h3, h4, block):
+ """Compress state (h0, h1, h2, h3, h4) with block."""
+ # Left path variables.
+ al, bl, cl, dl, el = h0, h1, h2, h3, h4
+ # Right path variables.
+ ar, br, cr, dr, er = h0, h1, h2, h3, h4
+ # Message variables.
+ x = [int.from_bytes(block[4*i:4*(i+1)], 'little') for i in range(16)]
+
+ # Iterate over the 80 rounds of the compression.
+ for j in range(80):
+ rnd = j >> 4
+ # Perform left side of the transformation.
+ al = rol(al + fi(bl, cl, dl, rnd) + x[ML[j]] + KL[rnd], RL[j]) + el
+ al, bl, cl, dl, el = el, al, bl, rol(cl, 10), dl
+ # Perform right side of the transformation.
+ ar = rol(ar + fi(br, cr, dr, 4 - rnd) + x[MR[j]] + KR[rnd], RR[j]) + er
+ ar, br, cr, dr, er = er, ar, br, rol(cr, 10), dr
+
+ # Compose old state, left transform, and right transform into new state.
+ return h1 + cl + dr, h2 + dl + er, h3 + el + ar, h4 + al + br, h0 + bl + cr
+
+
+def ripemd160(data):
+ """Compute the RIPEMD-160 hash of data."""
+ # Initialize state.
+ state = (0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0)
+ # Process full 64-byte blocks in the input.
+ for b in range(len(data) >> 6):
+ state = compress(*state, data[64*b:64*(b+1)])
+ # Construct final blocks (with padding and size).
+ pad = b"\x80" + b"\x00" * ((119 - len(data)) & 63)
+ fin = data[len(data) & ~63:] + pad + (8 * len(data)).to_bytes(8, 'little')
+ # Process final blocks.
+ for b in range(len(fin) >> 6):
+ state = compress(*state, fin[64*b:64*(b+1)])
+ # Produce output.
+ return b"".join((h & 0xffffffff).to_bytes(4, 'little') for h in state)
diff --git a/bitcoin_client/ledger_bitcoin/segwit_addr.py b/bitcoin_client/ledger_bitcoin/segwit_addr.py
new file mode 100644
index 000000000..ef4174773
--- /dev/null
+++ b/bitcoin_client/ledger_bitcoin/segwit_addr.py
@@ -0,0 +1,137 @@
+# Copyright (c) 2017, 2020 Pieter Wuille
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+"""Reference implementation for Bech32/Bech32m and segwit addresses."""
+
+
+from enum import Enum
+
+class Encoding(Enum):
+ """Enumeration type to list the various supported encodings."""
+ BECH32 = 1
+ BECH32M = 2
+
+CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
+BECH32M_CONST = 0x2bc830a3
+
+def bech32_polymod(values):
+ """Internal function that computes the Bech32 checksum."""
+ generator = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]
+ chk = 1
+ for value in values:
+ top = chk >> 25
+ chk = (chk & 0x1ffffff) << 5 ^ value
+ for i in range(5):
+ chk ^= generator[i] if ((top >> i) & 1) else 0
+ return chk
+
+
+def bech32_hrp_expand(hrp):
+ """Expand the HRP into values for checksum computation."""
+ return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp]
+
+
+def bech32_verify_checksum(hrp, data):
+ """Verify a checksum given HRP and converted data characters."""
+ const = bech32_polymod(bech32_hrp_expand(hrp) + data)
+ if const == 1:
+ return Encoding.BECH32
+ if const == BECH32M_CONST:
+ return Encoding.BECH32M
+ return None
+
+def bech32_create_checksum(hrp, data, spec):
+ """Compute the checksum values given HRP and data."""
+ values = bech32_hrp_expand(hrp) + data
+ const = BECH32M_CONST if spec == Encoding.BECH32M else 1
+ polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ const
+ return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)]
+
+
+def bech32_encode(hrp, data, spec):
+ """Compute a Bech32 string given HRP and data values."""
+ combined = data + bech32_create_checksum(hrp, data, spec)
+ return hrp + '1' + ''.join([CHARSET[d] for d in combined])
+
+def bech32_decode(bech):
+ """Validate a Bech32/Bech32m string, and determine HRP and data."""
+ if ((any(ord(x) < 33 or ord(x) > 126 for x in bech)) or
+ (bech.lower() != bech and bech.upper() != bech)):
+ return (None, None, None)
+ bech = bech.lower()
+ pos = bech.rfind('1')
+ if pos < 1 or pos + 7 > len(bech) or len(bech) > 90:
+ return (None, None, None)
+ if not all(x in CHARSET for x in bech[pos+1:]):
+ return (None, None, None)
+ hrp = bech[:pos]
+ data = [CHARSET.find(x) for x in bech[pos+1:]]
+ spec = bech32_verify_checksum(hrp, data)
+ if spec is None:
+ return (None, None, None)
+ return (hrp, data[:-6], spec)
+
+def convertbits(data, frombits, tobits, pad=True):
+ """General power-of-2 base conversion."""
+ acc = 0
+ bits = 0
+ ret = []
+ maxv = (1 << tobits) - 1
+ max_acc = (1 << (frombits + tobits - 1)) - 1
+ for value in data:
+ if value < 0 or (value >> frombits):
+ return None
+ acc = ((acc << frombits) | value) & max_acc
+ bits += frombits
+ while bits >= tobits:
+ bits -= tobits
+ ret.append((acc >> bits) & maxv)
+ if pad:
+ if bits:
+ ret.append((acc << (tobits - bits)) & maxv)
+ elif bits >= frombits or ((acc << (tobits - bits)) & maxv):
+ return None
+ return ret
+
+
+def decode(hrp, addr):
+ """Decode a segwit address."""
+ hrpgot, data, spec = bech32_decode(addr)
+ if hrpgot != hrp:
+ return (None, None)
+ decoded = convertbits(data[1:], 5, 8, False)
+ if decoded is None or len(decoded) < 2 or len(decoded) > 40:
+ return (None, None)
+ if data[0] > 16:
+ return (None, None)
+ if data[0] == 0 and len(decoded) != 20 and len(decoded) != 32:
+ return (None, None)
+ if data[0] == 0 and spec != Encoding.BECH32 or data[0] != 0 and spec != Encoding.BECH32M:
+ return (None, None)
+ return (data[0], decoded)
+
+
+def encode(hrp, witver, witprog):
+ """Encode a segwit address."""
+ spec = Encoding.BECH32 if witver == 0 else Encoding.BECH32M
+ ret = bech32_encode(hrp, [witver] + convertbits(witprog, 8, 5), spec)
+ if decode(hrp, ret) == (None, None):
+ return None
+ return ret
\ No newline at end of file
diff --git a/bitcoin_client/ledger_bitcoin/transport.py b/bitcoin_client/ledger_bitcoin/transport.py
new file mode 100644
index 000000000..ed0b9bf43
--- /dev/null
+++ b/bitcoin_client/ledger_bitcoin/transport.py
@@ -0,0 +1,263 @@
+# extracted from ledgercomm in order to add the `path` parameter to the constructor.
+
+import enum
+import logging
+import struct
+from typing import Union, Tuple, Optional, Literal, cast
+
+from ledgercomm.interfaces.tcp_client import TCPClient
+from ledgercomm.interfaces.hid_device import HID
+from ledgercomm.log import LOG
+
+
+class TransportType(enum.Enum):
+ """Type of interface available."""
+
+ HID = 1
+ TCP = 2
+
+
+class Transport:
+ """Transport class to send APDUs.
+
+ Allow to communicate using HID device such as Nano S/X or through TCP
+ socket with the Speculos emulator.
+
+ Parameters
+ ----------
+ interface : str
+ Either "hid" or "tcp" for the underlying communication interface.
+ server : str
+ IP address of the TCP server if interface is "tcp".
+ port : int
+ Port of the TCP server if interface is "tcp".
+ path : Optional[str]
+ The path to use with HID if interface is "hid"; defaults to `None`.
+ hid : Optional[HID]
+ The HID instance to use if interface is "hid"; defaults to `None`.
+ If not None, the instance is already presumed open.
+ debug : bool
+ Whether you want debug logs or not.
+
+ Attributes
+ ----------
+ interface : TransportType
+ Either TransportType.HID or TransportType.TCP.
+ com : Union[TCPClient, HID]
+ Communication interface to send/receive APDUs.
+
+ """
+
+ def __init__(self,
+ interface: Literal["hid", "tcp"] = "tcp",
+ server: str = "127.0.0.1",
+ port: int = 9999,
+ path: Optional[str] = None,
+ hid: Optional[HID] = None,
+ debug: bool = False) -> None:
+ """Init constructor of Transport."""
+ if debug:
+ LOG.setLevel(logging.DEBUG)
+
+ self.inferface: TransportType
+
+ try:
+ self.interface = TransportType[interface.upper()]
+ except KeyError as exc:
+ raise Exception(f"Unknown interface '{interface}'!") from exc
+
+ if self.interface == TransportType.TCP:
+ self.com = TCPClient(
+ server=server, port=port)
+ self.com.open()
+ else:
+ if hid is not None:
+ self.com = hid
+ # we assume the instance is already open, when the `hid` parameter is given
+ else:
+ self.com = HID()
+ self.com.path = path
+ self.com.open()
+
+ @staticmethod
+ def apdu_header(cla: int,
+ ins: Union[int, enum.IntEnum],
+ p1: int = 0,
+ p2: int = 0,
+ opt: Optional[int] = None,
+ lc: int = 0) -> bytes:
+ """Pack the APDU header as bytes.
+
+ Parameters
+ ----------
+ cla : int
+ Instruction class: CLA (1 byte)
+ ins : Union[int, IntEnum]
+ Instruction code: INS (1 byte)
+ p1 : int
+ Instruction parameter: P1 (1 byte).
+ p2 : int
+ Instruction parameter: P2 (1 byte).
+ opt : Optional[int]
+ Optional parameter: Opt (1 byte).
+ lc : int
+ Number of bytes in the payload: Lc (1 byte).
+
+ Returns
+ -------
+ bytes
+ APDU header packed with parameters.
+
+ """
+ ins = cast(int, ins.value) if isinstance(
+ ins, enum.IntEnum) else cast(int, ins)
+
+ if opt:
+ return struct.pack("BBBBBB",
+ cla,
+ ins,
+ p1,
+ p2,
+ 1 + lc, # add option to length
+ opt)
+
+ return struct.pack("BBBBB",
+ cla,
+ ins,
+ p1,
+ p2,
+ lc)
+
+ def send(self,
+ cla: int,
+ ins: Union[int, enum.IntEnum],
+ p1: int = 0,
+ p2: int = 0,
+ option: Optional[int] = None,
+ cdata: bytes = b"") -> int:
+ """Send structured APDUs through `self.com`.
+
+ Parameters
+ ----------
+ cla : int
+ Instruction class: CLA (1 byte)
+ ins : Union[int, IntEnum]
+ Instruction code: INS (1 byte)
+ p1 : int
+ Instruction parameter: P1 (1 byte).
+ p2 : int
+ Instruction parameter: P2 (1 byte).
+ option : Optional[int]
+ Optional parameter: Opt (1 byte).
+ cdata : bytes
+ Command data (variable length).
+
+ Returns
+ -------
+ int
+ Total lenght of the APDU sent.
+
+ """
+ header: bytes = Transport.apdu_header(
+ cla, ins, p1, p2, option, len(cdata))
+
+ return self.com.send(header + cdata)
+
+ def send_raw(self, apdu: Union[str, bytes]) -> int:
+ """Send raw bytes `apdu` through `self.com`.
+
+ Parameters
+ ----------
+ apdu : Union[str, bytes]
+ Hexstring or bytes within APDU to be sent through `self.com`.
+
+ Returns
+ -------
+ Optional[int]
+ Total lenght of APDU sent if any.
+
+ """
+ if isinstance(apdu, str):
+ apdu = bytes.fromhex(apdu)
+
+ return self.com.send(apdu)
+
+ def recv(self) -> Tuple[int, bytes]:
+ """Receive data from `self.com`.
+
+ Blocking IO.
+
+ Returns
+ -------
+ Tuple[int, bytes]
+ A pair (sw, rdata) for the status word (2 bytes represented
+ as int) and the reponse data (variable lenght).
+
+ """
+ return self.com.recv()
+
+ def exchange(self,
+ cla: int,
+ ins: Union[int, enum.IntEnum],
+ p1: int = 0,
+ p2: int = 0,
+ option: Optional[int] = None,
+ cdata: bytes = b"") -> Tuple[int, bytes]:
+ """Send structured APDUs and wait to receive datas from `self.com`.
+
+ Parameters
+ ----------
+ cla : int
+ Instruction class: CLA (1 byte)
+ ins : Union[int, IntEnum]
+ Instruction code: INS (1 byte)
+ p1 : int
+ Instruction parameter: P1 (1 byte).
+ p2 : int
+ Instruction parameter: P2 (1 byte).
+ option : Optional[int]
+ Optional parameter: Opt (1 byte).
+ cdata : bytes
+ Command data (variable length).
+
+ Returns
+ -------
+ Tuple[int, bytes]
+ A pair (sw, rdata) for the status word (2 bytes represented
+ as int) and the reponse data (bytes of variable lenght).
+
+ """
+ header: bytes = Transport.apdu_header(
+ cla, ins, p1, p2, option, len(cdata))
+
+ return self.com.exchange(header + cdata)
+
+ def exchange_raw(self, apdu: Union[str, bytes]) -> Tuple[int, bytes]:
+ """Send raw bytes `apdu` and wait to receive datas from `self.com`.
+
+ Parameters
+ ----------
+ apdu : Union[str, bytes]
+ Hexstring or bytes within APDU to send through `self.com`.
+
+ Returns
+ -------
+ Tuple[int, bytes]
+ A pair (sw, rdata) for the status word (2 bytes represented
+ as int) and the reponse (bytes of variable lenght).
+
+ """
+ if isinstance(apdu, str):
+ apdu = bytes.fromhex(apdu)
+
+ return self.com.exchange(apdu)
+
+ def close(self) -> None:
+ """Close `self.com` interface.
+
+ Returns
+ -------
+ None
+
+ """
+ self.com.close()
diff --git a/bitcoin_client/ledger_bitcoin/tx.py b/bitcoin_client/ledger_bitcoin/tx.py
index 8cb1710f1..91b1215ff 100644
--- a/bitcoin_client/ledger_bitcoin/tx.py
+++ b/bitcoin_client/ledger_bitcoin/tx.py
@@ -138,7 +138,7 @@ def is_witness(self) -> Tuple[bool, int, bytes]:
def __repr__(self) -> str:
return "CTxOut(nValue=%i.%08i scriptPubKey=%s)" \
- % (self.nValue, self.nValue, self.scriptPubKey.hex())
+ % (self.nValue // 100_000_000, self.nValue % 100_000_000, self.scriptPubKey.hex())
class CScriptWitness(object):
diff --git a/bitcoin_client/ledger_bitcoin/wallet.py b/bitcoin_client/ledger_bitcoin/wallet.py
index 9a102355f..6005e96b3 100644
--- a/bitcoin_client/ledger_bitcoin/wallet.py
+++ b/bitcoin_client/ledger_bitcoin/wallet.py
@@ -1,3 +1,5 @@
+import re
+
from enum import IntEnum
from typing import List
@@ -8,18 +10,22 @@
from .merkle import MerkleTree, element_hash
class WalletType(IntEnum):
- POLICYMAP = 1
+ WALLET_POLICY_V1 = 1
+ WALLET_POLICY_V2 = 2
# should not be instantiated directly
-class Wallet:
- def __init__(self, name: str, wallet_type: WalletType) -> None:
+class WalletPolicyBase:
+ def __init__(self, name: str, version: WalletType) -> None:
self.name = name
- self.type = wallet_type
+ self.version = version
+
+ if (version != WalletType.WALLET_POLICY_V1 and version != WalletType.WALLET_POLICY_V2):
+ raise ValueError("Invalid wallet policy version")
def serialize(self) -> bytes:
return b"".join([
- self.type.value.to_bytes(1, byteorder="big"),
+ self.version.value.to_bytes(1, byteorder="big"),
serialize_str(self.name)
])
@@ -27,28 +33,28 @@ def serialize(self) -> bytes:
def id(self) -> bytes:
return sha256(self.serialize()).digest()
- def hmac(self, wallet_registration_key: bytes | bytearray) -> bytes:
+ def hmac(self, wallet_registration_key) -> bytes:
return hmac.new(wallet_registration_key, self.id, sha256).digest()
-class PolicyMapWallet(Wallet):
+class WalletPolicy(WalletPolicyBase):
"""
- Represents a wallet stored with a policy map and a number of keys_info.
- The wallet is serialized as follows:
- - 1 byte : wallet type
- - 1 byte : length of the wallet name (max 16)
+ Represents a wallet stored with a wallet policy.
+ For version V2, the wallet is serialized as follows:
+ - 1 byte : wallet version
+ - 1 byte : length of the wallet name (max 64)
- (var) : wallet name (ASCII string)
- - (varint) : length of the policy map, at most 74 bytes at this time
- - (var) : policy map
+ - (varint) : length of the descriptor template
+ - 32-bytes : sha256 hash of the descriptor template
- (varint) : number of keys (not larger than 252)
- 32-bytes : root of the Merkle tree of all the keys information.
The specific format of the keys is deferred to subclasses.
"""
- def __init__(self, name: str, policy_map: str, keys_info: List[str]):
- super().__init__(name, WalletType.POLICYMAP)
- self.policy_map = policy_map
+ def __init__(self, name: str, descriptor_template: str, keys_info: List[str], version: WalletType = WalletType.WALLET_POLICY_V2):
+ super().__init__(name, version)
+ self.descriptor_template = descriptor_template
self.keys_info = keys_info
@property
@@ -56,30 +62,39 @@ def n_keys(self) -> int:
return len(self.keys_info)
def serialize(self) -> bytes:
- keys_info_hashes = map(lambda k: element_hash(k.encode("latin-1")), self.keys_info)
+ keys_info_hashes = map(lambda k: element_hash(k.encode()), self.keys_info)
+
+ descriptor_template_sha256 = sha256(self.descriptor_template.encode()).digest()
return b"".join([
super().serialize(),
- write_varint(len(self.policy_map)),
- self.policy_map.encode("latin-1"),
+ write_varint(len(self.descriptor_template.encode())),
+ self.descriptor_template.encode() if self.version == WalletType.WALLET_POLICY_V1 else descriptor_template_sha256,
write_varint(len(self.keys_info)),
MerkleTree(keys_info_hashes).root
])
def get_descriptor(self, change: bool) -> str:
- desc = self.policy_map
+ desc = self.descriptor_template
for i in reversed(range(self.n_keys)):
key = self.keys_info[i]
- if "/**" in key:
- key = key.replace("/**", f"/{1 if change else 0}/*")
desc = desc.replace(f"@{i}", key)
+
+ # in V1, /** is part of the key; in V2, it's part of the policy map. This handles either
+ desc = desc.replace("/**", f"/{1 if change else 0}/*")
+
+ if self.version == WalletType.WALLET_POLICY_V2:
+ # V2, the / syntax is supported. Replace with M if not change, or with N if change
+ regex = r"/<(\d+);(\d+)>"
+ desc = re.sub(regex, "/\\2" if change else "/\\1", desc)
+
return desc
-class MultisigWallet(PolicyMapWallet):
- def __init__(self, name: str, address_type: AddressType, threshold: int, keys_info: List[str], sorted: bool = True) -> None:
+class MultisigWallet(WalletPolicy):
+ def __init__(self, name: str, address_type: AddressType, threshold: int, keys_info: List[str], sorted: bool = True, version: WalletType = WalletType.WALLET_POLICY_V2) -> None:
n_keys = len(keys_info)
- if not (1 <= threshold <= n_keys <= 15):
+ if not (1 <= threshold <= n_keys <= 16):
raise ValueError("Invalid threshold or number of keys")
multisig_op = "sortedmulti" if sorted else "multi"
@@ -96,14 +111,16 @@ def __init__(self, name: str, address_type: AddressType, threshold: int, keys_in
else:
raise ValueError(f"Unexpected address type: {address_type}")
- policy_map = "".join([
+ key_placeholder_suffix = "/**" if version == WalletType.WALLET_POLICY_V2 else ""
+
+ descriptor_template = "".join([
policy_prefix,
str(threshold) + ",",
- ",".join("@" + str(l) for l in range(n_keys)),
+ ",".join("@" + str(l) + key_placeholder_suffix for l in range(n_keys)),
policy_suffix
])
- super().__init__(name, policy_map, keys_info)
+ super().__init__(name, descriptor_template, keys_info, version)
self.threshold = threshold
@@ -117,7 +134,7 @@ def wrap_ct(policy_map: str, blinding_key: str = ""):
return "".join([f"ct({blinding_key},", policy_map, ")"])
-class BlindedWallet(PolicyMapWallet):
+class BlindedWallet(WalletPolicy):
"""Blinded wallet for Liquid application"""
def __init__(self, name: str, blinding_key: str, policy_map: str, keys_info: List[str]):
diff --git a/bitcoin_client/pyproject.toml b/bitcoin_client/pyproject.toml
index f7473453b..5020eb701 100644
--- a/bitcoin_client/pyproject.toml
+++ b/bitcoin_client/pyproject.toml
@@ -1,8 +1,8 @@
[build-system]
requires = [
- "typing-extensions>=3.7",
"ledgercomm>=1.1.0",
"setuptools>=42",
+ "typing-extensions>=3.7",
"wheel"
]
build-backend = "setuptools.build_meta"
diff --git a/bitcoin_client/setup.cfg b/bitcoin_client/setup.cfg
index 8ed37f9ce..9d44335f8 100644
--- a/bitcoin_client/setup.cfg
+++ b/bitcoin_client/setup.cfg
@@ -1,6 +1,6 @@
[metadata]
name = ledger_bitcoin
-version = 0.0.3
+version = attr: ledger_bitcoin.__version__
author = Ledger
author_email = hello@ledger.fr
description = Client for Ledger Nano Bitcoin application
@@ -16,10 +16,14 @@ classifiers =
[options]
packages = find:
-python_requires = >=3.6
+python_requires = >=3.7
install_requires=
typing-extensions>=3.7
ledgercomm>=1.1.0
+ packaging>=21.3
+
+[options.package_data]
+* = py.typed
[options.extras_require]
hid = hidapi>=0.9.0.post3
diff --git a/bitcoin_client/tests/requirements.txt b/bitcoin_client/tests/requirements.txt
index 58945f429..23a57de0e 100644
--- a/bitcoin_client/tests/requirements.txt
+++ b/bitcoin_client/tests/requirements.txt
@@ -1,7 +1,8 @@
pytest>=6.1.1,<7.0.0
+pytest-timeout>=2.1.0,<3.0.0
ledgercomm>=1.1.0,<1.2.0
ecdsa>=0.16.1,<0.17.0
typing-extensions>=3.7,<4.0
-embit>=0.4.10,<0.5.0
+embit>=0.7.0,<0.8.0
mnemonic==0.20
-bip32>=2.1,<3.0
\ No newline at end of file
+bip32>=3.4,<4.0
\ No newline at end of file
diff --git a/bitcoin_client/tests/test_client_legacy.py b/bitcoin_client/tests/test_client_legacy.py
new file mode 100644
index 000000000..03e7df928
--- /dev/null
+++ b/bitcoin_client/tests/test_client_legacy.py
@@ -0,0 +1,13 @@
+from pathlib import Path
+
+from bitcoin_client.ledger_bitcoin import Client
+from bitcoin_client.ledger_bitcoin.client_legacy import LegacyClient
+
+
+tests_root: Path = Path(__file__).parent
+
+
+def test_client_legacy(client: Client):
+ # tests that the library correctly instatiates the LegacyClient and not the new one,
+ # since the version of the app binary being tested is an old one
+ assert isinstance(client, LegacyClient)
diff --git a/bitcoin_client/tests/test_get_wallet_address_legacyapp.py b/bitcoin_client/tests/test_get_wallet_address_legacyapp.py
index 8caaf05f9..efa4e05e5 100644
--- a/bitcoin_client/tests/test_get_wallet_address_legacyapp.py
+++ b/bitcoin_client/tests/test_get_wallet_address_legacyapp.py
@@ -1,13 +1,13 @@
-from bitcoin_client.ledger_bitcoin import Client, PolicyMapWallet
+from bitcoin_client.ledger_bitcoin import Client, WalletPolicy
def test_get_wallet_address_singlesig_legacy(client: Client):
# legacy address (P2PKH)
- wallet = PolicyMapWallet(
+ wallet = WalletPolicy(
name="",
- policy_map="pkh(@0)",
+ descriptor_template="pkh(@0/**)",
keys_info=[
- f"[f5acc2fd/44'/1'/0']tpubDCwYjpDhUdPGP5rS3wgNg13mTrrjBuG8V9VpWbyptX6TRPbNoZVXsoVUSkCjmQ8jJycjuDKBb9eataSymXakTTaGifxR6kmVsfFehH1ZgJT/**",
+ f"[f5acc2fd/44'/1'/0']tpubDCwYjpDhUdPGP5rS3wgNg13mTrrjBuG8V9VpWbyptX6TRPbNoZVXsoVUSkCjmQ8jJycjuDKBb9eataSymXakTTaGifxR6kmVsfFehH1ZgJT",
],
)
assert client.get_wallet_address(wallet, None, 0, 0, False) == "mz5vLWdM1wHVGSmXUkhKVvZbJ2g4epMXSm"
@@ -16,11 +16,11 @@ def test_get_wallet_address_singlesig_legacy(client: Client):
def test_get_wallet_address_singlesig_wit(client: Client):
# bech32 address (P2WPKH)
- wallet = PolicyMapWallet(
+ wallet = WalletPolicy(
name="",
- policy_map="wpkh(@0)",
+ descriptor_template="wpkh(@0/**)",
keys_info=[
- f"[f5acc2fd/84'/1'/0']tpubDCtKfsNyRhULjZ9XMS4VKKtVcPdVDi8MKUbcSD9MJDyjRu1A2ND5MiipozyyspBT9bg8upEp7a8EAgFxNxXn1d7QkdbL52Ty5jiSLcxPt1P/**",
+ f"[f5acc2fd/84'/1'/0']tpubDCtKfsNyRhULjZ9XMS4VKKtVcPdVDi8MKUbcSD9MJDyjRu1A2ND5MiipozyyspBT9bg8upEp7a8EAgFxNxXn1d7QkdbL52Ty5jiSLcxPt1P",
],
)
assert client.get_wallet_address(wallet, None, 0, 0, False) == "tb1qzdr7s2sr0dwmkwx033r4nujzk86u0cy6fmzfjk"
@@ -29,11 +29,11 @@ def test_get_wallet_address_singlesig_wit(client: Client):
def test_get_wallet_address_singlesig_sh_wit(client: Client):
# wrapped segwit addresses (P2SH-P2WPKH)
- wallet = PolicyMapWallet(
+ wallet = WalletPolicy(
name="",
- policy_map="sh(wpkh(@0))",
+ descriptor_template="sh(wpkh(@0/**))",
keys_info=[
- f"[f5acc2fd/49'/1'/0']tpubDC871vGLAiKPcwAw22EjhKVLk5L98UGXBEcGR8gpcigLQVDDfgcYW24QBEyTHTSFEjgJgbaHU8CdRi9vmG4cPm1kPLmZhJEP17FMBdNheh3/**",
+ f"[f5acc2fd/49'/1'/0']tpubDC871vGLAiKPcwAw22EjhKVLk5L98UGXBEcGR8gpcigLQVDDfgcYW24QBEyTHTSFEjgJgbaHU8CdRi9vmG4cPm1kPLmZhJEP17FMBdNheh3",
],
)
assert client.get_wallet_address(wallet, None, 0, 0, False) == "2MyHkbusvLomaarGYMqyq7q9pSBYJRwWcsw"
diff --git a/bitcoin_client/tests/test_ripemd160.py b/bitcoin_client/tests/test_ripemd160.py
new file mode 100644
index 000000000..f4a403d0e
--- /dev/null
+++ b/bitcoin_client/tests/test_ripemd160.py
@@ -0,0 +1,24 @@
+from bitcoin_client.ledger_bitcoin.ripemd import ripemd160
+
+
+def test_ripemd160():
+ """RIPEMD-160 test vectors."""
+ # See https://homes.esat.kuleuven.be/~bosselae/ripemd160.html
+ for msg, hexout in [
+ (b"", "9c1185a5c5e9fc54612808977ee8f548b2258d31"),
+ (b"a", "0bdc9d2d256b3ee9daae347be6f4dc835a467ffe"),
+ (b"abc", "8eb208f7e05d987a9b044a8e98c6b087f15a0bfc"),
+ (b"message digest", "5d0689ef49d2fae572b881b123a85ffa21595f36"),
+ (b"abcdefghijklmnopqrstuvwxyz", "f71c27109c692c1b56bbdceb5b9d2865b3708dbc"),
+ (
+ b"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq",
+ "12a053384a9c0c88e405a06c27dcf49ada62eb2b",
+ ),
+ (
+ b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
+ "b0e20b6e3116640286ed3a87a5713079b21f5189",
+ ),
+ (b"1234567890" * 8, "9b752e45573d4b39f4dbd3323cab82bf63326bfb"),
+ (b"a" * 1000000, "52783243c1697bdbe16d37f97f68f08325dc1528"),
+ ]:
+ assert ripemd160(msg).hex() == hexout
diff --git a/bitcoin_client/tests/test_sign_psbt_legacyapp.py b/bitcoin_client/tests/test_sign_psbt_legacyapp.py
index e0d60d202..d52100b7c 100644
--- a/bitcoin_client/tests/test_sign_psbt_legacyapp.py
+++ b/bitcoin_client/tests/test_sign_psbt_legacyapp.py
@@ -1,6 +1,6 @@
from pathlib import Path
-from bitcoin_client.ledger_bitcoin import Client, PolicyMapWallet
+from bitcoin_client.ledger_bitcoin import Client, WalletPolicy, PartialSignature
from bitcoin_client.ledger_bitcoin.psbt import PSBT
@@ -26,11 +26,11 @@ def test_sign_psbt_singlesig_pkh_1to1(client: Client):
# PSBT for a legacy 1-input 1-output spend (no change address)
psbt = open_psbt_from_file(f"{tests_root}/psbt/singlesig/pkh-1to1.psbt")
- wallet = PolicyMapWallet(
+ wallet = WalletPolicy(
"",
- "pkh(@0)",
+ "pkh(@0/**)",
[
- "[f5acc2fd/44'/1'/0']tpubDCwYjpDhUdPGP5rS3wgNg13mTrrjBuG8V9VpWbyptX6TRPbNoZVXsoVUSkCjmQ8jJycjuDKBb9eataSymXakTTaGifxR6kmVsfFehH1ZgJT/**"
+ "[f5acc2fd/44'/1'/0']tpubDCwYjpDhUdPGP5rS3wgNg13mTrrjBuG8V9VpWbyptX6TRPbNoZVXsoVUSkCjmQ8jJycjuDKBb9eataSymXakTTaGifxR6kmVsfFehH1ZgJT"
],
)
@@ -40,11 +40,15 @@ def test_sign_psbt_singlesig_pkh_1to1(client: Client):
# "signature" : "3045022100e55b3ca788721aae8def2eadff710e524ffe8c9dec1764fdaa89584f9726e196022012a30fbcf9e1a24df31a1010356b794ab8de438b4250684757ed5772402540f401"
result = client.sign_psbt(psbt, wallet, None)
- assert result == {
- 0: bytes.fromhex(
- "3045022100e55b3ca788721aae8def2eadff710e524ffe8c9dec1764fdaa89584f9726e196022012a30fbcf9e1a24df31a1010356b794ab8de438b4250684757ed5772402540f401"
+ assert result == [(
+ 0,
+ PartialSignature(
+ pubkey=bytes.fromhex("02ee8608207e21028426f69e76447d7e3d5e077049f5e683c3136c2314762a4718"),
+ signature=bytes.fromhex(
+ "3045022100e55b3ca788721aae8def2eadff710e524ffe8c9dec1764fdaa89584f9726e196022012a30fbcf9e1a24df31a1010356b794ab8de438b4250684757ed5772402540f401"
+ )
)
- }
+ )]
@has_automation("automations/sign_with_wallet_accept.json")
@@ -53,11 +57,11 @@ def test_sign_psbt_singlesig_sh_wpkh_1to2(client: Client):
# PSBT for a wrapped segwit 1-input 2-output spend (1 change address)
psbt = open_psbt_from_file(f"{tests_root}/psbt/singlesig/sh-wpkh-1to2.psbt")
- wallet = PolicyMapWallet(
+ wallet = WalletPolicy(
"",
- "sh(wpkh(@0))",
+ "sh(wpkh(@0/**))",
[
- "[f5acc2fd/49'/1'/0']tpubDC871vGLAiKPcwAw22EjhKVLk5L98UGXBEcGR8gpcigLQVDDfgcYW24QBEyTHTSFEjgJgbaHU8CdRi9vmG4cPm1kPLmZhJEP17FMBdNheh3/**"
+ "[f5acc2fd/49'/1'/0']tpubDC871vGLAiKPcwAw22EjhKVLk5L98UGXBEcGR8gpcigLQVDDfgcYW24QBEyTHTSFEjgJgbaHU8CdRi9vmG4cPm1kPLmZhJEP17FMBdNheh3"
],
)
@@ -67,11 +71,17 @@ def test_sign_psbt_singlesig_sh_wpkh_1to2(client: Client):
# "signature" : "30440220720722b08489c2a50d10edea8e21880086c8e8f22889a16815e306daeea4665b02203fcf453fa490b76cf4f929714065fc90a519b7b97ab18914f9451b5a4b45241201"
result = client.sign_psbt(psbt, wallet, None)
- assert result == {
- 0: bytes.fromhex(
- "30440220720722b08489c2a50d10edea8e21880086c8e8f22889a16815e306daeea4665b02203fcf453fa490b76cf4f929714065fc90a519b7b97ab18914f9451b5a4b45241201"
+ assert len(result) == 1
+
+ assert result == [(
+ 0,
+ PartialSignature(
+ pubkey=bytes.fromhex("024ba3b77d933de9fa3f9583348c40f3caaf2effad5b6e244ece8abbfcc7244f67"),
+ signature=bytes.fromhex(
+ "30440220720722b08489c2a50d10edea8e21880086c8e8f22889a16815e306daeea4665b02203fcf453fa490b76cf4f929714065fc90a519b7b97ab18914f9451b5a4b45241201"
+ )
)
- }
+ )]
@has_automation("automations/sign_with_wallet_accept.json")
@@ -80,11 +90,11 @@ def test_sign_psbt_singlesig_wpkh_1to2(client: Client):
# PSBT for a legacy 1-input 2-output spend (1 change address)
psbt = open_psbt_from_file(f"{tests_root}/psbt/singlesig/wpkh-1to2.psbt")
- wallet = PolicyMapWallet(
+ wallet = WalletPolicy(
"",
- "wpkh(@0)",
+ "wpkh(@0/**)",
[
- "[f5acc2fd/84'/1'/0']tpubDCtKfsNyRhULjZ9XMS4VKKtVcPdVDi8MKUbcSD9MJDyjRu1A2ND5MiipozyyspBT9bg8upEp7a8EAgFxNxXn1d7QkdbL52Ty5jiSLcxPt1P/**"
+ "[f5acc2fd/84'/1'/0']tpubDCtKfsNyRhULjZ9XMS4VKKtVcPdVDi8MKUbcSD9MJDyjRu1A2ND5MiipozyyspBT9bg8upEp7a8EAgFxNxXn1d7QkdbL52Ty5jiSLcxPt1P"
],
)
@@ -95,11 +105,15 @@ def test_sign_psbt_singlesig_wpkh_1to2(client: Client):
# "pubkey" : "03ee2c3d98eb1f93c0a1aa8e5a4009b70eb7b44ead15f1666f136b012ad58d3068",
# "signature" : "3045022100ab44f34dd7e87c9054591297a101e8500a0641d1d591878d0d23cf8096fa79e802205d12d1062d925e27b57bdcf994ecf332ad0a8e67b8fe407bab2101255da632aa01"
- assert result == {
- 0: bytes.fromhex(
- "3045022100ab44f34dd7e87c9054591297a101e8500a0641d1d591878d0d23cf8096fa79e802205d12d1062d925e27b57bdcf994ecf332ad0a8e67b8fe407bab2101255da632aa01"
+ assert result == [(
+ 0,
+ PartialSignature(
+ pubkey=bytes.fromhex("03ee2c3d98eb1f93c0a1aa8e5a4009b70eb7b44ead15f1666f136b012ad58d3068"),
+ signature=bytes.fromhex(
+ "3045022100ab44f34dd7e87c9054591297a101e8500a0641d1d591878d0d23cf8096fa79e802205d12d1062d925e27b57bdcf994ecf332ad0a8e67b8fe407bab2101255da632aa01"
+ )
)
- }
+ )]
@has_automation("automations/sign_with_wallet_accept.json")
@@ -108,21 +122,30 @@ def test_sign_psbt_singlesig_wpkh_2to2(client: Client):
psbt = open_psbt_from_file(f"{tests_root}/psbt/singlesig/wpkh-2to2.psbt")
- wallet = PolicyMapWallet(
+ wallet = WalletPolicy(
"",
- "wpkh(@0)",
+ "wpkh(@0/**)",
[
- "[f5acc2fd/84'/1'/0']tpubDCtKfsNyRhULjZ9XMS4VKKtVcPdVDi8MKUbcSD9MJDyjRu1A2ND5MiipozyyspBT9bg8upEp7a8EAgFxNxXn1d7QkdbL52Ty5jiSLcxPt1P/**"
+ "[f5acc2fd/84'/1'/0']tpubDCtKfsNyRhULjZ9XMS4VKKtVcPdVDi8MKUbcSD9MJDyjRu1A2ND5MiipozyyspBT9bg8upEp7a8EAgFxNxXn1d7QkdbL52Ty5jiSLcxPt1P"
],
)
result = client.sign_psbt(psbt, wallet, None)
- assert result == {
- 0: bytes.fromhex(
- "304402206b3e877655f08c6e7b1b74d6d893a82cdf799f68a5ae7cecae63a71b0339e5ce022019b94aa3fb6635956e109f3d89c996b1bfbbaf3c619134b5a302badfaf52180e01"
- ),
- 1: bytes.fromhex(
- "3045022100e2e98e4f8c70274f10145c89a5d86e216d0376bdf9f42f829e4315ea67d79d210220743589fd4f55e540540a976a5af58acd610fa5e188a5096dfe7d36baf3afb94001"
- ),
- }
+ assert result == [(
+ 0,
+ PartialSignature(
+ pubkey=bytes.fromhex("03455ee7cedc97b0ba435b80066fc92c963a34c600317981d135330c4ee43ac7a3"),
+ signature=bytes.fromhex(
+ "304402206b3e877655f08c6e7b1b74d6d893a82cdf799f68a5ae7cecae63a71b0339e5ce022019b94aa3fb6635956e109f3d89c996b1bfbbaf3c619134b5a302badfaf52180e01"
+ )
+ )
+ ), (
+ 1,
+ PartialSignature(
+ pubkey=bytes.fromhex("0271b5b779ad870838587797bcf6f0c7aec5abe76a709d724f48d2e26cf874f0a0"),
+ signature=bytes.fromhex(
+ "3045022100e2e98e4f8c70274f10145c89a5d86e216d0376bdf9f42f829e4315ea67d79d210220743589fd4f55e540540a976a5af58acd610fa5e188a5096dfe7d36baf3afb94001"
+ )
+ )
+ )]
diff --git a/bitcoin_client_js/README.md b/bitcoin_client_js/README.md
index 8a1f487d9..efd4ab53c 100644
--- a/bitcoin_client_js/README.md
+++ b/bitcoin_client_js/README.md
@@ -2,15 +2,21 @@
## Overview
-TypeScript client for Ledger Bitcoin application. Supports versions 2.0.0 and above of the app.
+TypeScript client for Ledger Bitcoin application. Supports versions 2.1.0 and above of the app.
Main repository and documentation: https://github.com/LedgerHQ/app-bitcoin-new
-
+```bash
+$ yarn add ledger-bitcoin
+```
+
+Or if you prefer using npm:
+
+```bash
+$ npm install ledger-bitcoin
+```
## Building
@@ -24,10 +30,12 @@ $ yarn build
The following example showcases all the main methods of the `Client`'s interface.
-Testing the `signPsbt` method requires a valid PSBTv2, and provide the corresponding wallet policy; it is skipped by default in the following example.
+More examples can be found in the [test suite](src/__tests__/appClient.test.ts).
+
+Testing the `signPsbt` method requires a valid PSBT, and provide the corresponding wallet policy; it is skipped by default in the following example.
```javascript
-import { AppClient, DefaultWalletPolicy, WalletPolicy, PsbtV2 } from 'ledger-bitcoin';
+import { AppClient, DefaultWalletPolicy, WalletPolicy } from 'ledger-bitcoin';
import Transport from '@ledgerhq/hw-transport-node-hid';
// This examples assumes the Bitcoin Testnet app is running.
@@ -44,8 +52,8 @@ async function main(transport) {
// ==> Get and display on screen the first taproot address
const firstTaprootAccountPubkey = await app.getExtendedPubkey("m/86'/1'/0'");
const firstTaprootAccountPolicy = new DefaultWalletPolicy(
- "tr(@0)",
- `[${fpr}/86'/1'/0']${firstTaprootAccountPubkey}/**`
+ "tr(@0/**)",
+ `[${fpr}/86'/1'/0']${firstTaprootAccountPubkey}`
);
const firstTaprootAccountAddress = await app.getWalletAddress(
@@ -61,12 +69,12 @@ async function main(transport) {
// ==> Register a multisig wallet named "Cold storage"
const ourPubkey = await app.getExtendedPubkey("m/48'/1'/0'/2'");
- const ourKeyInfo = `[${fpr}/48'/1'/0'/2']${ourPubkey}/**`;
- const otherKeyInfo = "[76223a6e/48'/1'/0'/2']tpubDE7NQymr4AFtewpAsWtnreyq9ghkzQBXpCZjWLFVRAvnbf7vya2eMTvT2fPapNqL8SuVvLQdbUbMfWLVDCZKnsEBqp6UK93QEzL8Ck23AwF/**";
+ const ourKeyInfo = `[${fpr}/48'/1'/0'/2']${ourPubkey}`;
+ const otherKeyInfo = "[76223a6e/48'/1'/0'/2']tpubDE7NQymr4AFtewpAsWtnreyq9ghkzQBXpCZjWLFVRAvnbf7vya2eMTvT2fPapNqL8SuVvLQdbUbMfWLVDCZKnsEBqp6UK93QEzL8Ck23AwF";
const multisigPolicy = new WalletPolicy(
"Cold storage",
- "wsh(sortedmulti(2,@0,@1))", // a 2-of-2 multisig policy template
+ "wsh(sortedmulti(2,@0/**,@1/**))", // a 2-of-2 multisig policy template
[
otherKeyInfo, // some other bitcoiner
ourKeyInfo, // that's us
@@ -87,18 +95,19 @@ async function main(transport) {
// ==> Sign a psbt
// TODO: set a wallet policy and a valid psbt file in order to test psbt signing
- const rawPsbtBase64 = null; // a base64-encoded psbt file to sign
+ const psbt = null; // a base64-encoded psbt, or a binary psbt in a Buffer
const signingPolicy = null; // an instance of WalletPolicy
const signingPolicyHmac = null; // if not a default wallet policy, this must also be set
- if (!rawPsbtBase64 || !signingPolicy) {
+ if (!psbt || !signingPolicy) {
console.log("Nothing to sign :(");
await transport.close();
return;
}
- const psbt = new PsbtV2();
- psbt.deserialize(rawPsbtBase64);
-
+ // result will be a list of triples [i, partialSig], where:
+ // - i is the input index
+ // - partialSig is an instance of PartialSignature; it contains a pubkey and a signature,
+ // and it might contain a tapleaf_hash.
const result = await app.signPsbt(psbt, signingPolicy, signingPolicyHmac);
console.log("Returned signatures:");
@@ -110,4 +119,4 @@ async function main(transport) {
Transport.default.create()
.then(main)
.catch(console.log);
-```
\ No newline at end of file
+```
diff --git a/bitcoin_client_js/package.json b/bitcoin_client_js/package.json
index 848e09c56..362df99f0 100644
--- a/bitcoin_client_js/package.json
+++ b/bitcoin_client_js/package.json
@@ -1,10 +1,10 @@
{
"name": "ledger-bitcoin",
- "version": "0.0.1",
+ "version": "0.2.3",
"description": "Ledger Hardware Wallet Bitcoin Application Client",
"main": "build/main/index.js",
"typings": "build/main/index.d.ts",
- "repository": "https://github.com/LedgerHW/app-bitcoin-new",
+ "repository": "https://github.com/LedgerHQ/app-bitcoin-new",
"license": "Apache-2.0",
"keywords": [
"Ledger",
@@ -29,10 +29,11 @@
"node": ">=14"
},
"dependencies": {
+ "@bitcoinerlab/descriptors": "^1.0.2",
+ "@bitcoinerlab/secp256k1": "^1.0.5",
"@ledgerhq/hw-transport": "^6.20.0",
"bip32-path": "^0.4.2",
- "bitcoinjs-lib": "^6.0.1",
- "tiny-secp256k1": "^2.1.2"
+ "bitcoinjs-lib": "^6.1.3"
},
"devDependencies": {
"@ledgerhq/hw-transport-node-speculos-http": "^6.24.1",
@@ -71,4 +72,4 @@
"prettier": {
"singleQuote": true
}
-}
\ No newline at end of file
+}
diff --git a/bitcoin_client_js/src/__tests__/appClient.test.ts b/bitcoin_client_js/src/__tests__/appClient.test.ts
index 9ea60f469..a1784646b 100644
--- a/bitcoin_client_js/src/__tests__/appClient.test.ts
+++ b/bitcoin_client_js/src/__tests__/appClient.test.ts
@@ -103,6 +103,7 @@ describe("test AppClient", () => {
sp = spawn(speculos_path, [
repoRootPath + "/bin/app.elf",
'-k', '2.1',
+ '--model', 'nanos',
'--display', 'headless'
]);
@@ -127,6 +128,11 @@ describe("test AppClient", () => {
await killProcess(sp);
});
+ it("can retrieve the app's version", async () => {
+ const result = await app.getAppAndVersion();
+ expect(result.name).toEqual("Bitcoin Test");
+ expect(result.version.split(".")[0]).toEqual("2")
+ });
it("can retrieve the master fingerprint", async () => {
const result = await app.getMasterFingerprint();
@@ -149,64 +155,64 @@ describe("test AppClient", () => {
}[] = [
// legacy
{
- policy: new DefaultWalletPolicy("pkh(@0)", "[f5acc2fd/44'/1'/0']tpubDCwYjpDhUdPGP5rS3wgNg13mTrrjBuG8V9VpWbyptX6TRPbNoZVXsoVUSkCjmQ8jJycjuDKBb9eataSymXakTTaGifxR6kmVsfFehH1ZgJT/**"),
+ policy: new DefaultWalletPolicy("pkh(@0/**)", "[f5acc2fd/44'/1'/0']tpubDCwYjpDhUdPGP5rS3wgNg13mTrrjBuG8V9VpWbyptX6TRPbNoZVXsoVUSkCjmQ8jJycjuDKBb9eataSymXakTTaGifxR6kmVsfFehH1ZgJT"),
change: 0,
addrIndex: 0,
expResult: "mz5vLWdM1wHVGSmXUkhKVvZbJ2g4epMXSm",
},
{
- policy: new DefaultWalletPolicy("pkh(@0)", "[f5acc2fd/44'/1'/0']tpubDCwYjpDhUdPGP5rS3wgNg13mTrrjBuG8V9VpWbyptX6TRPbNoZVXsoVUSkCjmQ8jJycjuDKBb9eataSymXakTTaGifxR6kmVsfFehH1ZgJT/**"),
+ policy: new DefaultWalletPolicy("pkh(@0/**)", "[f5acc2fd/44'/1'/0']tpubDCwYjpDhUdPGP5rS3wgNg13mTrrjBuG8V9VpWbyptX6TRPbNoZVXsoVUSkCjmQ8jJycjuDKBb9eataSymXakTTaGifxR6kmVsfFehH1ZgJT"),
change: 1,
addrIndex: 15,
expResult: "myFCUBRCKFjV7292HnZtiHqMzzHrApobpT",
},
// native segwit
{
- policy: new DefaultWalletPolicy("wpkh(@0)", "[f5acc2fd/84'/1'/0']tpubDCtKfsNyRhULjZ9XMS4VKKtVcPdVDi8MKUbcSD9MJDyjRu1A2ND5MiipozyyspBT9bg8upEp7a8EAgFxNxXn1d7QkdbL52Ty5jiSLcxPt1P/**"),
+ policy: new DefaultWalletPolicy("wpkh(@0/**)", "[f5acc2fd/84'/1'/0']tpubDCtKfsNyRhULjZ9XMS4VKKtVcPdVDi8MKUbcSD9MJDyjRu1A2ND5MiipozyyspBT9bg8upEp7a8EAgFxNxXn1d7QkdbL52Ty5jiSLcxPt1P"),
change: 0,
addrIndex: 0,
expResult: "tb1qzdr7s2sr0dwmkwx033r4nujzk86u0cy6fmzfjk",
},
{
- policy: new DefaultWalletPolicy("wpkh(@0)", "[f5acc2fd/84'/1'/0']tpubDCtKfsNyRhULjZ9XMS4VKKtVcPdVDi8MKUbcSD9MJDyjRu1A2ND5MiipozyyspBT9bg8upEp7a8EAgFxNxXn1d7QkdbL52Ty5jiSLcxPt1P/**"),
+ policy: new DefaultWalletPolicy("wpkh(@0/**)", "[f5acc2fd/84'/1'/0']tpubDCtKfsNyRhULjZ9XMS4VKKtVcPdVDi8MKUbcSD9MJDyjRu1A2ND5MiipozyyspBT9bg8upEp7a8EAgFxNxXn1d7QkdbL52Ty5jiSLcxPt1P"),
change: 1,
addrIndex: 15,
expResult: "tb1qlrvzyx8jcjfj2xuy69du9trtxnsvjuped7e289",
},
// wrapped segwit
{
- policy: new DefaultWalletPolicy("sh(wpkh(@0))", "[f5acc2fd/49'/1'/0']tpubDC871vGLAiKPcwAw22EjhKVLk5L98UGXBEcGR8gpcigLQVDDfgcYW24QBEyTHTSFEjgJgbaHU8CdRi9vmG4cPm1kPLmZhJEP17FMBdNheh3/**"),
+ policy: new DefaultWalletPolicy("sh(wpkh(@0/**))", "[f5acc2fd/49'/1'/0']tpubDC871vGLAiKPcwAw22EjhKVLk5L98UGXBEcGR8gpcigLQVDDfgcYW24QBEyTHTSFEjgJgbaHU8CdRi9vmG4cPm1kPLmZhJEP17FMBdNheh3"),
change: 0,
addrIndex: 0,
expResult: "2MyHkbusvLomaarGYMqyq7q9pSBYJRwWcsw",
},
{
- policy: new DefaultWalletPolicy("sh(wpkh(@0))", "[f5acc2fd/49'/1'/0']tpubDC871vGLAiKPcwAw22EjhKVLk5L98UGXBEcGR8gpcigLQVDDfgcYW24QBEyTHTSFEjgJgbaHU8CdRi9vmG4cPm1kPLmZhJEP17FMBdNheh3/**"),
+ policy: new DefaultWalletPolicy("sh(wpkh(@0/**))", "[f5acc2fd/49'/1'/0']tpubDC871vGLAiKPcwAw22EjhKVLk5L98UGXBEcGR8gpcigLQVDDfgcYW24QBEyTHTSFEjgJgbaHU8CdRi9vmG4cPm1kPLmZhJEP17FMBdNheh3"),
change: 1,
addrIndex: 15,
expResult: "2NAbM4FSeBQG4o85kbXw2YNfKypcnEZS9MR",
},
// taproot
{
- policy: new DefaultWalletPolicy("tr(@0)", "[f5acc2fd/86'/1'/0']tpubDDKYE6BREvDsSWMazgHoyQWiJwYaDDYPbCFjYxN3HFXJP5fokeiK4hwK5tTLBNEDBwrDXn8cQ4v9b2xdW62Xr5yxoQdMu1v6c7UDXYVH27U/**"),
+ policy: new DefaultWalletPolicy("tr(@0/**)", "[f5acc2fd/86'/1'/0']tpubDDKYE6BREvDsSWMazgHoyQWiJwYaDDYPbCFjYxN3HFXJP5fokeiK4hwK5tTLBNEDBwrDXn8cQ4v9b2xdW62Xr5yxoQdMu1v6c7UDXYVH27U"),
change: 0,
addrIndex: 0,
expResult: "tb1pws8wvnj99ca6acf8kq7pjk7vyxknah0d9mexckh5s0vu2ccy68js9am6u7",
},
{
- policy: new DefaultWalletPolicy("tr(@0)", "[f5acc2fd/86'/1'/0']tpubDDKYE6BREvDsSWMazgHoyQWiJwYaDDYPbCFjYxN3HFXJP5fokeiK4hwK5tTLBNEDBwrDXn8cQ4v9b2xdW62Xr5yxoQdMu1v6c7UDXYVH27U/**"),
+ policy: new DefaultWalletPolicy("tr(@0/**)", "[f5acc2fd/86'/1'/0']tpubDDKYE6BREvDsSWMazgHoyQWiJwYaDDYPbCFjYxN3HFXJP5fokeiK4hwK5tTLBNEDBwrDXn8cQ4v9b2xdW62Xr5yxoQdMu1v6c7UDXYVH27U"),
change: 0,
addrIndex: 9,
expResult: "tb1psl7eyk2jyjzq6evqvan854fts7a5j65rth25yqahkd2a765yvj0qggs5ne",
},
{
- policy: new DefaultWalletPolicy("tr(@0)", "[f5acc2fd/86'/1'/0']tpubDDKYE6BREvDsSWMazgHoyQWiJwYaDDYPbCFjYxN3HFXJP5fokeiK4hwK5tTLBNEDBwrDXn8cQ4v9b2xdW62Xr5yxoQdMu1v6c7UDXYVH27U/**"),
+ policy: new DefaultWalletPolicy("tr(@0/**)", "[f5acc2fd/86'/1'/0']tpubDDKYE6BREvDsSWMazgHoyQWiJwYaDDYPbCFjYxN3HFXJP5fokeiK4hwK5tTLBNEDBwrDXn8cQ4v9b2xdW62Xr5yxoQdMu1v6c7UDXYVH27U"),
change: 1,
addrIndex: 0,
expResult: "tb1pmr60r5vfjmdkrwcu4a2z8h39mzs7a6wf2rfhuml6qgcp940x9cxs7t9pdy",
},
{
- policy: new DefaultWalletPolicy("tr(@0)", "[f5acc2fd/86'/1'/0']tpubDDKYE6BREvDsSWMazgHoyQWiJwYaDDYPbCFjYxN3HFXJP5fokeiK4hwK5tTLBNEDBwrDXn8cQ4v9b2xdW62Xr5yxoQdMu1v6c7UDXYVH27U/**"),
+ policy: new DefaultWalletPolicy("tr(@0/**)", "[f5acc2fd/86'/1'/0']tpubDDKYE6BREvDsSWMazgHoyQWiJwYaDDYPbCFjYxN3HFXJP5fokeiK4hwK5tTLBNEDBwrDXn8cQ4v9b2xdW62Xr5yxoQdMu1v6c7UDXYVH27U"),
change: 1,
addrIndex: 9,
expResult: "tb1p98d6s9jkf0la8ras4nnm72zme5r03fexn29e3pgz4qksdy84ndpqgjak72",
@@ -215,16 +221,16 @@ describe("test AppClient", () => {
{
policy: new WalletPolicy(
"Cold storage",
- "wsh(sortedmulti(2,@0,@1))",
+ "wsh(sortedmulti(2,@0/**,@1/**))",
[
- "[76223a6e/48'/1'/0'/2']tpubDE7NQymr4AFtewpAsWtnreyq9ghkzQBXpCZjWLFVRAvnbf7vya2eMTvT2fPapNqL8SuVvLQdbUbMfWLVDCZKnsEBqp6UK93QEzL8Ck23AwF/**",
- "[f5acc2fd/48'/1'/0'/2']tpubDFAqEGNyad35aBCKUAXbQGDjdVhNueno5ZZVEn3sQbW5ci457gLR7HyTmHBg93oourBssgUxuWz1jX5uhc1qaqFo9VsybY1J5FuedLfm4dK/**",
+ "[76223a6e/48'/1'/0'/2']tpubDE7NQymr4AFtewpAsWtnreyq9ghkzQBXpCZjWLFVRAvnbf7vya2eMTvT2fPapNqL8SuVvLQdbUbMfWLVDCZKnsEBqp6UK93QEzL8Ck23AwF",
+ "[f5acc2fd/48'/1'/0'/2']tpubDFAqEGNyad35aBCKUAXbQGDjdVhNueno5ZZVEn3sQbW5ci457gLR7HyTmHBg93oourBssgUxuWz1jX5uhc1qaqFo9VsybY1J5FuedLfm4dK",
]
),
change: 0,
addrIndex: 0,
expResult: "tb1qmyauyzn08cduzdqweexgna2spwd0rndj55fsrkefry2cpuyt4cpsn2pg28",
- walletHmac: Buffer.from("d6434852fb3caa7edbd1165084968f1691444b3cfc10cf1e431acbbc7f48451f", "hex")
+ walletHmac: Buffer.from("d7c7a60b4ab4a14c1bf8901ba627d72140b2fb907f2b4e35d2e693bce9fbb371", "hex")
},
];
@@ -238,10 +244,75 @@ describe("test AppClient", () => {
it("can register a multisig wallet", async () => {
const walletPolicy = new WalletPolicy(
"Cold storage",
- "wsh(sortedmulti(2,@0,@1))",
+ "wsh(sortedmulti(2,@0/**,@1/**))",
+ [
+ "[76223a6e/48'/1'/0'/2']tpubDE7NQymr4AFtewpAsWtnreyq9ghkzQBXpCZjWLFVRAvnbf7vya2eMTvT2fPapNqL8SuVvLQdbUbMfWLVDCZKnsEBqp6UK93QEzL8Ck23AwF",
+ "[f5acc2fd/48'/1'/0'/2']tpubDFAqEGNyad35aBCKUAXbQGDjdVhNueno5ZZVEn3sQbW5ci457gLR7HyTmHBg93oourBssgUxuWz1jX5uhc1qaqFo9VsybY1J5FuedLfm4dK",
+ ]
+ );
+
+ const automation = JSON.parse(fs.readFileSync('src/__tests__/automations/register_wallet_accept.json').toString());
+ await setSpeculosAutomation(transport, automation);
+
+ const [walletId, walletHmac] = await app.registerWallet(walletPolicy);
+
+ expect(walletId).toEqual(walletPolicy.getId());
+ expect(walletHmac.length).toEqual(32);
+ });
+
+ //https://wizardsardine.com/blog/ledger-vulnerability-disclosure/
+ it('can generate a correct address or throw on a:X', async () => {
+ for (const template of [
+ 'wsh(and_b(pk(@0/**),a:1))',
+ 'wsh(and_b(pk(@0/<0;1>/*),a:1))'
+ ]) {
+ try {
+ const walletPolicy = new WalletPolicy('Fixed Vulnerability', template, [
+ "[f5acc2fd/84'/1'/0']tpubDCtKfsNyRhULjZ9XMS4VKKtVcPdVDi8MKUbcSD9MJDyjRu1A2ND5MiipozyyspBT9bg8upEp7a8EAgFxNxXn1d7QkdbL52Ty5jiSLcxPt1P"
+ ]);
+
+ const automation = JSON.parse(
+ fs
+ .readFileSync(
+ 'src/__tests__/automations/register_wallet_accept.json'
+ )
+ .toString()
+ );
+ await setSpeculosAutomation(transport, automation);
+
+ const [walletId, walletHmac] = await app.registerWallet(walletPolicy);
+
+ expect(walletId).toEqual(walletPolicy.getId());
+ expect(walletHmac.length).toEqual(32);
+
+ const address = await app.getWalletAddress(
+ walletPolicy,
+ walletHmac,
+ 0,
+ 0,
+ false
+ );
+ //version > 2.1.1
+ expect(address).toEqual(
+ 'tb1q5lyn9807ygs7pc52980mdeuwl9wrq5c8n3kntlhy088h6fqw4gzspw9t9m'
+ );
+ } catch (error) {
+ //version <= 2.1.1
+ expect(error.message).toMatch(
+ /^Third party address validation mismatch/
+ );
+ }
+ }
+ });
+
+ it("can register a miniscript wallet", async () => {
+ const walletPolicy = new WalletPolicy(
+ "Decaying 3-of-3",
+ "wsh(thresh(3,pk(@0/**),s:pk(@1/**),s:pk(@2/**),sln:older(12960)))",
[
- "[76223a6e/48'/1'/0'/2']tpubDE7NQymr4AFtewpAsWtnreyq9ghkzQBXpCZjWLFVRAvnbf7vya2eMTvT2fPapNqL8SuVvLQdbUbMfWLVDCZKnsEBqp6UK93QEzL8Ck23AwF/**",
- "[f5acc2fd/48'/1'/0'/2']tpubDFAqEGNyad35aBCKUAXbQGDjdVhNueno5ZZVEn3sQbW5ci457gLR7HyTmHBg93oourBssgUxuWz1jX5uhc1qaqFo9VsybY1J5FuedLfm4dK/**",
+ "[76223a6e/48'/1'/0'/2']tpubDE7NQymr4AFtewpAsWtnreyq9ghkzQBXpCZjWLFVRAvnbf7vya2eMTvT2fPapNqL8SuVvLQdbUbMfWLVDCZKnsEBqp6UK93QEzL8Ck23AwF",
+ "[f5acc2fd/48'/1'/0'/2']tpubDFAqEGNyad35aBCKUAXbQGDjdVhNueno5ZZVEn3sQbW5ci457gLR7HyTmHBg93oourBssgUxuWz1jX5uhc1qaqFo9VsybY1J5FuedLfm4dK",
+ "tpubDCoDDpHR1MYXcFrarTcwBufQvWPXSSZpGxjnhRaW612TMxs5TWDEPdbYRHtQdZ9z1UqtKGQKVQ4FqejzbFSdvQvJsD75yrgh7thVoFho6jE",
]
);
@@ -265,23 +336,121 @@ describe("test AppClient", () => {
await setSpeculosAutomation(transport, automation);
const walletPolicy = new DefaultWalletPolicy(
- "wpkh(@0)",
- "[f5acc2fd/84'/1'/0']tpubDCtKfsNyRhULjZ9XMS4VKKtVcPdVDi8MKUbcSD9MJDyjRu1A2ND5MiipozyyspBT9bg8upEp7a8EAgFxNxXn1d7QkdbL52Ty5jiSLcxPt1P/**"
+ "wpkh(@0/**)",
+ "[f5acc2fd/84'/1'/0']tpubDCtKfsNyRhULjZ9XMS4VKKtVcPdVDi8MKUbcSD9MJDyjRu1A2ND5MiipozyyspBT9bg8upEp7a8EAgFxNxXn1d7QkdbL52Ty5jiSLcxPt1P"
);
const psbt = new PsbtV2();
psbt.deserialize(psbtBuf);
const result = await app.signPsbt(psbt, walletPolicy, null, () => {});
- expect(result.size).toEqual(2);
- expect(result.get(0)).toEqual(Buffer.from(
+ expect(result.length).toEqual(2);
+
+ expect(result[0][0]).toEqual(0);
+ expect(result[0][1].pubkey).toEqual(Buffer.from(
+ "03455ee7cedc97b0ba435b80066fc92c963a34c600317981d135330c4ee43ac7a3",
+ "hex"
+ ));
+ expect(result[0][1].signature).toEqual(Buffer.from(
"304402206b3e877655f08c6e7b1b74d6d893a82cdf799f68a5ae7cecae63a71b0339e5ce022019b94aa3fb6635956e109f3d89c996b1bfbbaf3c619134b5a302badfaf52180e01",
"hex"
));
- expect(result.get(1)).toEqual(Buffer.from(
+
+
+ expect(result[1][0]).toEqual(1);
+ expect(result[1][1].pubkey).toEqual(Buffer.from(
+ "0271b5b779ad870838587797bcf6f0c7aec5abe76a709d724f48d2e26cf874f0a0",
+ "hex"
+ ));
+ expect(result[1][1].signature).toEqual(Buffer.from(
"3045022100e2e98e4f8c70274f10145c89a5d86e216d0376bdf9f42f829e4315ea67d79d210220743589fd4f55e540540a976a5af58acd610fa5e188a5096dfe7d36baf3afb94001",
"hex"
));
+ expect(result[1][1].tapleafHash).toBeUndefined();
+ });
+
+ it("can sign a psbt for a taproot script path", async () => {
+ // psbt from test_sign_psbt_tr_script_pk_sighash_all in the main test suite, converted to PSBTv2
+ const psbtBuf = Buffer.from(
+ "cHNidP8BAgQCAAAAAQMEAAAAAAEEAQEBBQEBAfsEAgAAAAABAStMBgAAAAAAACJRIPwKENMIx+QbS7w2Qvj9isKJhTsc51WgxtDUlfA9ny2kAQMEAQAAACIVwVAXEIvs6o3txTALsiOGs6swNnrCYvnOXlgybrg+OiL1IyBrFujB+Xn6TMDwW2owCv//lBRZtvIN533lWwFg745MrKzAIRZQFxCL7OqN7cUwC7IjhrOrMDZ6wmL5zl5YMm64Pjoi9R0AdiI6bjAAAIABAACAAAAAgAIAAIAAAAAAAAAAACEWaxbowfl5+kzA8FtqMAr//5QUWbbyDed95VsBYO+OTKw9AQku2gM2F+IQ7n99DjeKQErqHEi1aqEDAivs93RuRwCk9azC/TAAAIABAACAAAAAgAIAAIAAAAAAAAAAAAEXIFAXEIvs6o3txTALsiOGs6swNnrCYvnOXlgybrg+OiL1ARggCS7aAzYX4hDuf30ON4pASuocSLVqoQMCK+z3dG5HAKQBDiAfwcxXccuDhgzFbZS8/tk4YIwX9jZiQ1tB6cRP/P0xQgEPBAEAAAABEAT9////AAEDCDkFAAAAAAAAAQQWABSqjvN0yvrfynaQLdtc9hxgu/2dhQA=",
+ "base64"
+ );
+
+ const automation = JSON.parse(fs.readFileSync('src/__tests__/automations/sign_with_wallet_accept.json').toString());
+ await setSpeculosAutomation(transport, automation);
+
+ const walletPolicy = new WalletPolicy(
+ "Taproot foreign internal key, and our script key",
+ "tr(@0/**,pk(@1/**))",
+ [
+ "[76223a6e/48'/1'/0'/2']tpubDE7NQymr4AFtewpAsWtnreyq9ghkzQBXpCZjWLFVRAvnbf7vya2eMTvT2fPapNqL8SuVvLQdbUbMfWLVDCZKnsEBqp6UK93QEzL8Ck23AwF",
+ "[f5acc2fd/48'/1'/0'/2']tpubDFAqEGNyad35aBCKUAXbQGDjdVhNueno5ZZVEn3sQbW5ci457gLR7HyTmHBg93oourBssgUxuWz1jX5uhc1qaqFo9VsybY1J5FuedLfm4dK",
+ ]
+ );
+
+ const psbt = new PsbtV2();
+ psbt.deserialize(psbtBuf);
+ const hmac = Buffer.from("dae925660e20859ed8833025d46444483ce264fdb77e34569aabe9d590da8fb7", "hex");
+ const result = await app.signPsbt(psbt, walletPolicy, hmac);
+
+ expect(result.length).toEqual(1);
+
+ expect(result[0][0]).toEqual(0);
+ expect(result[0][1].pubkey).toEqual(Buffer.from(
+ "6b16e8c1f979fa4cc0f05b6a300affff941459b6f20de77de55b0160ef8e4cac",
+ "hex"
+ ));
+ expect(result[0][1].tapleafHash).toEqual(Buffer.from(
+ "092eda033617e210ee7f7d0e378a404aea1c48b56aa103022becf7746e4700a4",
+ "hex"
+ ));
+
+ // We could test the validity of the signature, but this is already done in the corresponding python test.
+ // Here we're only interested in testing that the JS library returns the correct values.
+ expect(result[0][1].signature.length).toEqual(65); // 65 because it's SIGHASH_ALL and not SIGHASH_DEFAULT
+ });
+
+ it("can sign a psbt passed as a base64 string", async () => {
+ const automation = JSON.parse(fs.readFileSync('src/__tests__/automations/sign_with_wallet_accept.json').toString());
+ await setSpeculosAutomation(transport, automation);
+
+ const walletPolicy = new WalletPolicy(
+ "Taproot foreign internal key, and our script key",
+ "tr(@0/**,pk(@1/**))",
+ [
+ "[76223a6e/48'/1'/0'/2']tpubDE7NQymr4AFtewpAsWtnreyq9ghkzQBXpCZjWLFVRAvnbf7vya2eMTvT2fPapNqL8SuVvLQdbUbMfWLVDCZKnsEBqp6UK93QEzL8Ck23AwF",
+ "[f5acc2fd/48'/1'/0'/2']tpubDFAqEGNyad35aBCKUAXbQGDjdVhNueno5ZZVEn3sQbW5ci457gLR7HyTmHBg93oourBssgUxuWz1jX5uhc1qaqFo9VsybY1J5FuedLfm4dK",
+ ]
+ );
+
+ const hmac = Buffer.from("dae925660e20859ed8833025d46444483ce264fdb77e34569aabe9d590da8fb7", "hex");
+ const psbtBase64 = "cHNidP8BAgQCAAAAAQMEAAAAAAEEAQEBBQEBAfsEAgAAAAABAStMBgAAAAAAACJRIPwKENMIx+QbS7w2Qvj9isKJhTsc51WgxtDUlfA9ny2kAQMEAQAAACIVwVAXEIvs6o3txTALsiOGs6swNnrCYvnOXlgybrg+OiL1IyBrFujB+Xn6TMDwW2owCv//lBRZtvIN533lWwFg745MrKzAIRZQFxCL7OqN7cUwC7IjhrOrMDZ6wmL5zl5YMm64Pjoi9R0AdiI6bjAAAIABAACAAAAAgAIAAIAAAAAAAAAAACEWaxbowfl5+kzA8FtqMAr//5QUWbbyDed95VsBYO+OTKw9AQku2gM2F+IQ7n99DjeKQErqHEi1aqEDAivs93RuRwCk9azC/TAAAIABAACAAAAAgAIAAIAAAAAAAAAAAAEXIFAXEIvs6o3txTALsiOGs6swNnrCYvnOXlgybrg+OiL1ARggCS7aAzYX4hDuf30ON4pASuocSLVqoQMCK+z3dG5HAKQBDiAfwcxXccuDhgzFbZS8/tk4YIwX9jZiQ1tB6cRP/P0xQgEPBAEAAAABEAT9////AAEDCDkFAAAAAAAAAQQWABSqjvN0yvrfynaQLdtc9hxgu/2dhQA="
+ const result = await app.signPsbt(psbtBase64, walletPolicy, hmac);
+
+ expect(result.length).toEqual(1);
+ });
+
+ it("can sign a psbt passed as binary buffer string", async () => {
+ const automation = JSON.parse(fs.readFileSync('src/__tests__/automations/sign_with_wallet_accept.json').toString());
+ await setSpeculosAutomation(transport, automation);
+
+ const walletPolicy = new WalletPolicy(
+ "Taproot foreign internal key, and our script key",
+ "tr(@0/**,pk(@1/**))",
+ [
+ "[76223a6e/48'/1'/0'/2']tpubDE7NQymr4AFtewpAsWtnreyq9ghkzQBXpCZjWLFVRAvnbf7vya2eMTvT2fPapNqL8SuVvLQdbUbMfWLVDCZKnsEBqp6UK93QEzL8Ck23AwF",
+ "[f5acc2fd/48'/1'/0'/2']tpubDFAqEGNyad35aBCKUAXbQGDjdVhNueno5ZZVEn3sQbW5ci457gLR7HyTmHBg93oourBssgUxuWz1jX5uhc1qaqFo9VsybY1J5FuedLfm4dK",
+ ]
+ );
+
+ const hmac = Buffer.from("dae925660e20859ed8833025d46444483ce264fdb77e34569aabe9d590da8fb7", "hex");
+ const psbtBuf = Buffer.from(
+ "cHNidP8BAgQCAAAAAQMEAAAAAAEEAQEBBQEBAfsEAgAAAAABAStMBgAAAAAAACJRIPwKENMIx+QbS7w2Qvj9isKJhTsc51WgxtDUlfA9ny2kAQMEAQAAACIVwVAXEIvs6o3txTALsiOGs6swNnrCYvnOXlgybrg+OiL1IyBrFujB+Xn6TMDwW2owCv//lBRZtvIN533lWwFg745MrKzAIRZQFxCL7OqN7cUwC7IjhrOrMDZ6wmL5zl5YMm64Pjoi9R0AdiI6bjAAAIABAACAAAAAgAIAAIAAAAAAAAAAACEWaxbowfl5+kzA8FtqMAr//5QUWbbyDed95VsBYO+OTKw9AQku2gM2F+IQ7n99DjeKQErqHEi1aqEDAivs93RuRwCk9azC/TAAAIABAACAAAAAgAIAAIAAAAAAAAAAAAEXIFAXEIvs6o3txTALsiOGs6swNnrCYvnOXlgybrg+OiL1ARggCS7aAzYX4hDuf30ON4pASuocSLVqoQMCK+z3dG5HAKQBDiAfwcxXccuDhgzFbZS8/tk4YIwX9jZiQ1tB6cRP/P0xQgEPBAEAAAABEAT9////AAEDCDkFAAAAAAAAAQQWABSqjvN0yvrfynaQLdtc9hxgu/2dhQA=",
+ "base64"
+ );
+ const result = await app.signPsbt(psbtBuf, walletPolicy, hmac);
+
+ expect(result.length).toEqual(1);
});
it("can sign a message", async () => {
@@ -294,4 +463,4 @@ describe("test AppClient", () => {
const result = await app.signMessage(Buffer.from(msg, "ascii"), path)
expect(result).toEqual("H4frM6TYm5ty1MAf9o/Zz9Qiy3VEldAYFY91SJ/5nYMAZY1UUB97fiRjKW8mJit2+V4OCa1YCqjDqyFnD9Fw75k=");
});
-});
\ No newline at end of file
+});
diff --git a/bitcoin_client_js/src/__tests__/automations/register_wallet_accept.json b/bitcoin_client_js/src/__tests__/automations/register_wallet_accept.json
index 6371aa704..fa2d0db14 100644
--- a/bitcoin_client_js/src/__tests__/automations/register_wallet_accept.json
+++ b/bitcoin_client_js/src/__tests__/automations/register_wallet_accept.json
@@ -2,7 +2,7 @@
"version": 1,
"rules": [
{
- "regexp": "Register wallet|Policy map|Key",
+ "regexp": "Register wallet|Wallet name|Wallet policy|Key",
"actions": [
["button", 2, true],
["button", 2, false]
diff --git a/bitcoin_client_js/src/__tests__/automations/sign_message_accept.json b/bitcoin_client_js/src/__tests__/automations/sign_message_accept.json
index 505f5e39c..a72520e17 100644
--- a/bitcoin_client_js/src/__tests__/automations/sign_message_accept.json
+++ b/bitcoin_client_js/src/__tests__/automations/sign_message_accept.json
@@ -2,7 +2,7 @@
"version": 1,
"rules": [
{
- "regexp": "Path|Message hash",
+ "regexp": "Path|Message hash|Message content",
"actions": [
["button", 2, true],
["button", 2, false]
diff --git a/bitcoin_client_js/src/__tests__/automations/sign_with_wallet_accept.json b/bitcoin_client_js/src/__tests__/automations/sign_with_wallet_accept.json
index 37a08b741..81c5db84c 100644
--- a/bitcoin_client_js/src/__tests__/automations/sign_with_wallet_accept.json
+++ b/bitcoin_client_js/src/__tests__/automations/sign_with_wallet_accept.json
@@ -2,14 +2,14 @@
"version": 1,
"rules": [
{
- "regexp": "Spend from|Review|Amount|Address|Confirm|Fees",
+ "regexp": "Spend from|Wallet name|Review|Amount|Address|Fees",
"actions": [
["button", 2, true],
["button", 2, false]
]
},
{
- "regexp": "Approve|Accept",
+ "regexp": "Approve|Continue|Sign",
"actions": [
[ "button", 1, true ],
[ "button", 2, true ],
diff --git a/bitcoin_client_js/src/__tests__/psbtv2.test.ts b/bitcoin_client_js/src/__tests__/psbtv2.test.ts
new file mode 100644
index 000000000..3aa2b103b
--- /dev/null
+++ b/bitcoin_client_js/src/__tests__/psbtv2.test.ts
@@ -0,0 +1,33 @@
+import { PsbtV2 } from "../lib/psbtv2";
+
+describe("PsbtV2", () => {
+ it("deserializes a psbtV2 and reserializes it unchanged", async () => {
+ const psbtBuf = Buffer.from(
+ "cHNidP8BAAoBAAAAAAAAAAAAAQIEAgAAAAEDBAAAAAABBAECAQUBAgH7BAIAAAAAAQBxAgAAAAGTarLgEHL3k8/kyXdU3hth/gPn22U2yLLyHdC1dCxIRQEAAAAA/v///wLe4ccAAAAAABYAFOt418QL8QY7Dj/OKcNWW2ichVmrECcAAAAAAAAWABQjGNZvhP71xIdfkzsDjcY4MfjaE/mXHgABAR8QJwAAAAAAABYAFCMY1m+E/vXEh1+TOwONxjgx+NoTIgYDRV7nztyXsLpDW4AGb8ksljo0xgAxeYHRNTMMTuQ6x6MY9azC/VQAAIABAACAAAAAgAAAAAABAAAAAQ4gniz+J/Cth7eKI31ddAXUowZmyjYdWFpGew3+QiYrTbQBDwQBAAAAARAE/f///wESBAAAAAAAAQBxAQAAAAEORx706Sway1HvyGYPjT9pk26pybK/9y/5vIHFHvz0ZAEAAAAAAAAAAAJgrgoAAAAAABYAFDXG4N1tPISxa6iF3Kc6yGPQtZPsrwYyAAAAAAAWABTcKG4M0ua9N86+nsNJ+18IkFZy/AAAAAABAR9grgoAAAAAABYAFDXG4N1tPISxa6iF3Kc6yGPQtZPsIgYCcbW3ea2HCDhYd5e89vDHrsWr52pwnXJPSNLibPh08KAY9azC/VQAAIABAACAAAAAgAEAAAAAAAAAAQ4gr7+uBlkPdB/xr1m2rEYRJjNqTEqC21U99v76tzesM/MBDwQAAAAAARAE/f///wESBAAAAAAAIgICKexHcnEx7SWIogxG7amrt9qm9J/VC6/nC5xappYcTswY9azC/VQAAIABAACAAAAAgAEAAAAKAAAAAQMIqDoGAAAAAAABBBYAFOs4+puBKPgfJule2wxf+uqDaQ/kAAEDCOCTBAAAAAAAAQQiACA/qWbJ3c3C/ZbkpeG8dlufr2zos+tPEQSq1r33cyTlvgA=",
+ "base64"
+ );
+
+ const psbt = new PsbtV2();
+ psbt.deserialize(psbtBuf);
+
+ expect(psbt.serialize()).toEqual(psbtBuf);
+ });
+
+ it("deserializes a psbtV0 and reserializes it as a valid psbtV2", async () => {
+ const psbtV0 = Buffer.from(
+ "cHNidP8BAFICAAAAAR/BzFdxy4OGDMVtlLz+2ThgjBf2NmJDW0HpxE/8/TFCAQAAAAD9////ATkFAAAAAAAAFgAUqo7zdMr638p2kC3bXPYcYLv9nYUAAAAAAAEBK0wGAAAAAAAAIlEg/AoQ0wjH5BtLvDZC+P2KwomFOxznVaDG0NSV8D2fLaQBAwQBAAAAIhXBUBcQi+zqje3FMAuyI4azqzA2esJi+c5eWDJuuD46IvUjIGsW6MH5efpMwPBbajAK//+UFFm28g3nfeVbAWDvjkysrMAhFlAXEIvs6o3txTALsiOGs6swNnrCYvnOXlgybrg+OiL1HQB2IjpuMAAAgAEAAIAAAACAAgAAgAAAAAAAAAAAIRZrFujB+Xn6TMDwW2owCv//lBRZtvIN533lWwFg745MrD0BCS7aAzYX4hDuf30ON4pASuocSLVqoQMCK+z3dG5HAKT1rML9MAAAgAEAAIAAAACAAgAAgAAAAAAAAAAAARcgUBcQi+zqje3FMAuyI4azqzA2esJi+c5eWDJuuD46IvUBGCAJLtoDNhfiEO5/fQ43ikBK6hxItWqhAwIr7Pd0bkcApAAA",
+ "base64"
+ );
+
+ // the same psbt converted to V2, with keys sorted in lexicographical order
+ const psbtV2 = Buffer.from(
+ "cHNidP8BAgQCAAAAAQMEAAAAAAEEAQEBBQEBAfsEAgAAAAABAStMBgAAAAAAACJRIPwKENMIx+QbS7w2Qvj9isKJhTsc51WgxtDUlfA9ny2kAQMEAQAAAAEOIB/BzFdxy4OGDMVtlLz+2ThgjBf2NmJDW0HpxE/8/TFCAQ8EAQAAAAEQBP3///8iFcFQFxCL7OqN7cUwC7IjhrOrMDZ6wmL5zl5YMm64Pjoi9SMgaxbowfl5+kzA8FtqMAr//5QUWbbyDed95VsBYO+OTKyswCEWUBcQi+zqje3FMAuyI4azqzA2esJi+c5eWDJuuD46IvUdAHYiOm4wAACAAQAAgAAAAIACAACAAAAAAAAAAAAhFmsW6MH5efpMwPBbajAK//+UFFm28g3nfeVbAWDvjkysPQEJLtoDNhfiEO5/fQ43ikBK6hxItWqhAwIr7Pd0bkcApPWswv0wAACAAQAAgAAAAIACAACAAAAAAAAAAAABFyBQFxCL7OqN7cUwC7IjhrOrMDZ6wmL5zl5YMm64Pjoi9QEYIAku2gM2F+IQ7n99DjeKQErqHEi1aqEDAivs93RuRwCkAAEDCDkFAAAAAAAAAQQWABSqjvN0yvrfynaQLdtc9hxgu/2dhQA=",
+ "base64"
+ );
+
+ const psbt = new PsbtV2();
+ psbt.deserialize(psbtV0);
+
+ expect(psbt.serialize()).toEqual(psbtV2);
+ });
+});
diff --git a/bitcoin_client_js/src/index.ts b/bitcoin_client_js/src/index.ts
index 7f2f4f995..01a2be10c 100644
--- a/bitcoin_client_js/src/index.ts
+++ b/bitcoin_client_js/src/index.ts
@@ -1,7 +1,18 @@
-import AppClient from './lib/appClient';
-import { DefaultWalletPolicy, WalletPolicy } from './lib/policy';
+import AppClient, { PartialSignature } from './lib/appClient';
+import {
+ DefaultDescriptorTemplate,
+ DefaultWalletPolicy,
+ WalletPolicy
+} from './lib/policy';
import { PsbtV2 } from './lib/psbtv2';
-export { AppClient, PsbtV2, DefaultWalletPolicy, WalletPolicy };
+export {
+ AppClient,
+ PsbtV2,
+ DefaultDescriptorTemplate,
+ DefaultWalletPolicy,
+ PartialSignature,
+ WalletPolicy
+};
export default AppClient;
diff --git a/bitcoin_client_js/src/lib/appClient.ts b/bitcoin_client_js/src/lib/appClient.ts
index 664779d84..bb369779f 100644
--- a/bitcoin_client_js/src/lib/appClient.ts
+++ b/bitcoin_client_js/src/lib/appClient.ts
@@ -1,4 +1,8 @@
+import * as descriptors from '@bitcoinerlab/descriptors';
+import * as secp256k1 from '@bitcoinerlab/secp256k1';
+const { Descriptor } = descriptors.DescriptorsFactory(secp256k1);
import Transport from '@ledgerhq/hw-transport';
+import { networks } from 'bitcoinjs-lib';
import { pathElementsToBuffer, pathStringToArray } from './bip32';
import { ClientCommandInterpreter } from './clientCommands';
@@ -6,11 +10,13 @@ import { MerkelizedPsbt } from './merkelizedPsbt';
import { hashLeaf, Merkle } from './merkle';
import { WalletPolicy } from './policy';
import { PsbtV2 } from './psbtv2';
-import { createVarint } from './varint';
+import { createVarint, parseVarint } from './varint';
const CLA_BTC = 0xe1;
const CLA_FRAMEWORK = 0xf8;
+const CURRENT_PROTOCOL_VERSION = 1; // supported from version 2.1.0 of the app
+
enum BitcoinIns {
GET_PUBKEY = 0x00,
REGISTER_WALLET = 0x02,
@@ -24,6 +30,42 @@ enum FrameworkIns {
CONTINUE_INTERRUPTED = 0x01,
}
+/**
+ * This class represents a partial signature produced by the app during signing.
+ * It always contains the `signature` and the corresponding `pubkey` whose private key
+ * was used for signing; in the case of taproot script paths, it also contains the
+ * tapleaf hash.
+ */
+export class PartialSignature {
+ readonly pubkey: Buffer;
+ readonly signature: Buffer;
+ readonly tapleafHash?: Buffer;
+
+ constructor(pubkey: Buffer, signature: Buffer, tapleafHash?: Buffer) {
+ this.pubkey = pubkey;
+ this.signature = signature;
+ this.tapleafHash = tapleafHash;
+ }
+}
+
+/**
+ * Creates an instance of `PartialSignature` from the returned raw augmented pubkey and signature.
+ * @param pubkeyAugm the public key, concatenated with the tapleaf hash in the case of taproot script path spend.
+ * @param signature the signature
+ * @returns an instance of `PartialSignature`.
+ */
+function makePartialSignature(pubkeyAugm: Buffer, signature: Buffer): PartialSignature {
+ if (pubkeyAugm.length == 64) {
+ // tapscript spend: concatenation of 32-bytes x-only pubkey and 32-bytes tapleaf_hash
+ return new PartialSignature(pubkeyAugm.slice(0, 32), signature, pubkeyAugm.slice(32, 64));
+ } else if (pubkeyAugm.length == 32 || pubkeyAugm.length == 33) {
+ // legacy, segwit or taproot keypath spend: pubkeyAugm is just the pubkey
+ return new PartialSignature(pubkeyAugm, signature);
+ } else {
+ throw new Error(`Invalid length for pubkeyAugm: ${pubkeyAugm.length} bytes.`);
+ }
+}
+
/**
* This class encapsulates the APDU protocol documented at
* https://github.com/LedgerHQ/app-bitcoin-new/blob/master/doc/bitcoin.md
@@ -44,7 +86,7 @@ export class AppClient {
CLA_BTC,
ins,
0,
- 0,
+ CURRENT_PROTOCOL_VERSION,
data,
[0x9000, 0xe000]
);
@@ -68,6 +110,34 @@ export class AppClient {
return response.slice(0, -2); // drop the status word (can only be 0x9000 at this point)
}
+ /**
+ * Returns an object containing the currently running app's name, version and the device status flags.
+ *
+ * @returns an object with app name, version and device status flags.
+ */
+ public async getAppAndVersion(): Promise<{
+ name: string;
+ version: string;
+ flags: number | Buffer;
+ }> {
+ const r = await this.transport.send(0xb0, 0x01, 0x00, 0x00);
+ let i = 0;
+ const format = r[i++];
+ if (format !== 1) throw new Error("Unexpected response")
+
+ const nameLength = r[i++];
+ const name = r.slice(i, (i += nameLength)).toString("ascii");
+ const versionLength = r[i++];
+ const version = r.slice(i, (i += versionLength)).toString("ascii");
+ const flagLength = r[i++];
+ const flags = r.slice(i, (i += flagLength));
+ return {
+ name,
+ version,
+ flags,
+ };
+ };
+
/**
* Requests the BIP-32 extended pubkey to the hardware wallet.
* If `display` is `false`, only standard paths will be accepted; an error is returned if an unusual path is
@@ -109,14 +179,12 @@ export class AppClient {
async registerWallet(
walletPolicy: WalletPolicy
): Promise {
- const serializedWalletPolicy = walletPolicy.serialize();
const clientInterpreter = new ClientCommandInterpreter();
- clientInterpreter.addKnownPreimage(serializedWalletPolicy);
- clientInterpreter.addKnownList(
- walletPolicy.keys.map((k) => Buffer.from(k, 'ascii'))
- );
+ clientInterpreter.addKnownWalletPolicy(walletPolicy);
+
+ const serializedWalletPolicy = walletPolicy.serialize();
const response = await this.makeRequest(
BitcoinIns.REGISTER_WALLET,
Buffer.concat([
@@ -131,8 +199,20 @@ export class AppClient {
`Invalid response length. Expected 64 bytes, got ${response.length}`
);
}
+ const walletId = response.subarray(0, 32);
+ const walletHMAC = response.subarray(32);
+
+ // sanity check: derive and validate the first address with a 3rd party
+ const firstAddrDevice = await this.getWalletAddress(
+ walletPolicy,
+ walletHMAC,
+ 0,
+ 0,
+ false
+ );
+ await this.validateAddress(firstAddrDevice, walletPolicy, 0, 0);
- return [response.subarray(0, 32), response.subarray(32)];
+ return [walletId, walletHMAC];
}
/**
@@ -163,10 +243,8 @@ export class AppClient {
}
const clientInterpreter = new ClientCommandInterpreter();
- clientInterpreter.addKnownList(
- walletPolicy.keys.map((k) => Buffer.from(k, 'ascii'))
- );
- clientInterpreter.addKnownPreimage(walletPolicy.serialize());
+
+ clientInterpreter.addKnownWalletPolicy(walletPolicy);
const addressIndexBuffer = Buffer.alloc(4);
addressIndexBuffer.writeUInt32BE(addressIndex, 0);
@@ -183,27 +261,41 @@ export class AppClient {
clientInterpreter
);
- return response.toString('ascii');
+ const address = response.toString('ascii');
+ await this.validateAddress(address, walletPolicy, change, addressIndex);
+ return address;
}
/**
* Signs a psbt using a (standard or registered) `WalletPolicy`. This is an interactive command, as user validation
* is necessary using the device's secure screen.
* On success, a map of input indexes and signatures is returned.
- * @param psbt an instance of `PsbtV2`
+ * @param psbt a base64-encoded string, or a psbt in a binary Buffer. Using the `PsbtV2` type is deprecated.
* @param walletPolicy the `WalletPolicy` to use for signing
* @param walletHMAC the 32-byte hmac obtained during wallet policy registration, or `null` for a standard policy
* @param progressCallback optionally, a callback that will be called every time a signature is produced during
* the signing process. The callback does not receive any argument, but can be used to track progress.
- * @returns a map from numbers to signatures. For each input index `i` that is a key of the returned map, the
- * corresponding value is the signature for the `i`-th input of the `psbt`.
+ * @returns an array of of tuples with 2 elements containing:
+ * - the index of the input being signed;
+ * - an instance of PartialSignature
*/
async signPsbt(
- psbt: PsbtV2,
+ psbt: PsbtV2 | string | Buffer,
walletPolicy: WalletPolicy,
walletHMAC: Buffer | null,
progressCallback?: () => void
- ): Promise